Commit Graph

979 Commits

Author SHA1 Message Date
David Montero Crespo f5ae4853eb fix(circuit-sim): stop() guard + drop edges with no V-source post-rebuild
Two related bugs that surfaced as "Vitest worker exited unexpectedly /
Timeout terminating forks worker" on the circuit-simulation-service
test file.

Bug 1 — tick() recursively re-schedules itself in its finally block.
After afterEach disposes the scheduler via __resetMixedModeScheduler(),
those re-scheduled ticks throw "call loadCircuit first", get caught by
the console.warn, and the finally schedules ANOTHER tick. Infinite
Promise loop survives until the worker OOMs.

  Fix: add CircuitSimulationService.stop() that flips a `stopped` flag
  short-circuiting tick() + handleMcuEdge(). The test harness now
  tracks each started service in _activeServices and calls stop() in
  afterEach alongside the existing unsubscribe sweep.

Bug 2 — when an MCU edge fires on a pin that's NOT wired into any net
(buildNetlist skips it because netLookup returns null), handleMcuEdge
sees hasSource=false, self-heals by queueing the edge + tick().  The
rebuild still doesn't emit the V-source (no wire), so tick.finally
replays the edge → self-heal again → tick again → infinite loop AT
RUNTIME, not just in tests. A user toggling a digital pin without a
wire freezes the whole circuit simulation.

  Fix: in tick.finally's pendingMcuEdges replay loop, check whether
  the rebuilt netlist now contains a V-source for each pending edge's
  pin. If not, drop the edge silently — a future canvas tick triggered
  by adding the wire will pick it up via the normal subscription path.

Also fix the "coalesces an edge with an in-flight full solve" test
fixture: simpleBoardWithBoard leaves pin 9 unwired, so V_uno_9 was
never emitted and the test was racing the (now-bounded) self-heal
rebuild. Replaced with an inline fixture wiring pin 9 → resistor →
GND, mirroring the wired fixture used by the alter+republish test
right above it.

Full vitest --shard 1/2 + 2/2 pass cleanly (1886 tests, 22-29s per
shard) with no worker-exit warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 03:49:29 +02:00
David Montero Crespo 95a60e1d01 test: align photoresistor-sensor fixtures + snapshots with 603b791 alias
Three failures introduced by 603b791 (which added
MAPPERS['photoresistor-sensor'] = MAPPERS['photoresistor'] so the
metadata-id 'photoresistor-sensor' resolves to a SPICE mapper):

- component-to-spice.test.ts "every mapped metadataId has a test fixture"
  flagged photoresistor-sensor as missing. Added a fixture entry that
  mirrors the photoresistor one — the part is electrically identical.

- examples-netlist-snapshot.test.ts > photoresistor-light and
  > nano-sensor-station snapshots now contain R_ldr_ldr + R_ldr_pull
  cards (correct LDR + 10k pull) instead of the previous
  R_autopull_n0 100M stub. This is the intended behaviour change:
  before the alias the LDR was unmapped and the netlist autopulled the
  net to ground with a 100M dummy; after the alias the SPICE deck
  carries the real divider topology. Regenerated only these two
  snapshot entries (vitest -u on the single file).

No production code changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:29:21 -03:00
David Montero Crespo 603b791daa feat(attiny85+customchip): full ATtiny85 ADC/Timer0 + custom-chip pipeline fixes
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>
2026-05-19 18:10:08 -03:00
David Montero Crespo a9285c6698 fix(ui): narrow pointer-passthrough whitelist; restore DHT22/HC-SR04 dialog
The previous fix (dd22bcf) used `isInteractive` to decide whether to let
the wokwi component own the pointerdown. That heuristic was too broad —
DHT22, HC-SR04, NTC, photoresistor, LED all register `attachEvents` for
the SPICE/sensor-update bridge but have NO internal pointer handlers, so
clicks on them got silently swallowed by the wokwi shadow DOM and the
property dialog never opened.

Replace with an explicit whitelist of wokwi tags that ACTUALLY own
pointerdown (rotary knobs, pushbuttons, slide switches, joysticks,
keypads, encoders, rotary dialer). Every other component, including
sensors/displays/LEDs with attachEvents, falls through to the canvas
which decides between drag-to-rearrange and click-to-open-dialog.

Documented the model in docs/wiki/component-interaction.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:16:47 -03:00
David Montero Crespo dd22bcfe50 fix(ui+spice+example): interactive wokwi components, NTC formula, photoresistor alias
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>
2026-05-19 15:10:42 -03:00
David Montero Crespo 2b668d882c
Merge pull request #201 from davidmonterocrespo24/fix/vitest-forks-toplevel-and-heap
fix(tests): migrate forks config to vitest-4 top-level + restore NODE…
2026-05-19 13:17:53 -03:00
davidmonterocrespo24 1f23066f47 fix(tests): migrate forks config to vitest-4 top-level + restore NODE_OPTIONS
Two issues in PR #200 became obvious from the next CI run:

1. **`poolOptions.forks.execArgv` had no effect.** Vitest 4 removed
   `test.poolOptions` entirely — every key under it moved to
   top-level `test.*`. The runner emits this banner on every run:

       DEPRECATED `test.poolOptions` was removed in Vitest 4.
       All previous poolOptions are now top-level options.

   So `test.poolOptions.forks.execArgv: ['--max-old-space-size=8192']`
   was silently ignored. Move it to `test.forks.execArgv` (and
   `test.forks.singleFork`).

2. **NODE_OPTIONS got dropped when the shard step was rewritten.**
   PR #199 added `env: NODE_OPTIONS: --max-old-space-size=8192` to
   the `npm test` step; PR #200 replaced the step with `npx vitest
   run --shard X/2` but did not carry the env-var across. Combined
   with #1, the workers reverted to Node's 4 GB default and shard 2
   (58 files) still OOMs at exactly 4128 MB heap.

Restore NODE_OPTIONS on the workflow step as belt-and-braces — it
gets honored by the parent vitest process, and the migrated config
covers the forked children.

With 8 GB heap × 2 shards × 58 files each, the cumulative state
from ngspice WASM + singletons fits comfortably and the suite
should exit cleanly.
2026-05-19 18:09:34 +02:00
David Montero Crespo fd6f1a43f7
Merge pull request #200 from davidmonterocrespo24/fix/frontend-tests-shard
fix(ci): shard frontend tests across two matrix legs
2026-05-19 12:54:29 -03:00
davidmonterocrespo24 a32d282b98 fix(ci): shard frontend tests across two matrix legs
The execArgv fix (PR #200) made vitest actually honor the 8 GB heap
cap — and the next CI run promptly proved that 8 GB is still not
enough. Log: every test passes but the worker hits
"Ineffective mark-compacts near heap limit" at exactly 8011 MB,
the new ceiling. Doubling again to 16 GB would be near the
GitHub-runner total RAM (16 GB) and start swapping.

The real culprit is per-file leak accumulation: 117 test files
share one vitest fork; each file lazy-loads ngspice WASM
(~24 MB), wires up MixedModeScheduler / zustand singletons, and
leaves some of that state alive in module-level closures even
after the file finishes. Sum over the suite ≈ 8 GB+ retained.

Split the run with vitest's built-in `--shard N/M`:

  - matrix.shard: [1, 2] alongside matrix.node-version: [20, 22]
    = 4 parallel runners
  - each runner executes `npx vitest run --shard ${shard}/2`
  - vitest hashes file paths into deterministic shards (same
    flaky file always lands in the same shard)
  - each runner only carries ~60 files of leak state → fits in
    the existing 8 GB cap from poolOptions.forks.execArgv

Coverage upload gated to shard 1 / node 22 to avoid the two
shards racing to overwrite the same artifact name. Coverage
itself runs once on the full suite (best-effort, may OOM, but
`continue-on-error: true` keeps it non-blocking).

The real fix is dispose hooks on the leaking singletons, but
that's a multi-PR cleanup of code paths I haven't touched in
this work item; sharding unblocks CI in the meantime.
2026-05-19 17:18:05 +02:00
David Montero Crespo 9f368fd382
Merge pull request #199 from davidmonterocrespo24/fix/vitest-forks-heap-execargv
fix(tests): bump fork heap via poolOptions.execArgv (NODE_OPTIONS ign…
2026-05-19 12:07:39 -03:00
davidmonterocrespo24 32d00ae0a7 fix(tests): bump fork heap via poolOptions.execArgv (NODE_OPTIONS ignored)
PR #198 added NODE_OPTIONS=--max-old-space-size=8192 to the
frontend-tests workflow assuming vitest's forks pool would inherit
it. It does NOT. Vitest 4's forks pool spawns workers via
child_process.fork() with an explicit execArgv list and ignores
the parent shell's NODE_OPTIONS env var — verified by reading the
post-merge GHA log: the Node OOM still fires at ~4.0 GB heap,
exactly the default v8 ceiling.

Set the heap cap at the pool level instead so the workers actually
see it. This is the canonical vitest 4 idiom for raising worker
limits — `poolOptions.forks.execArgv` is forwarded verbatim to
each forked child.

Independent of: the gpio_matrix_cb SIGSEGV fix in qemu-lcgamboa
(now landed) which addresses the Backend E2E failure mode. This
PR is exclusively the Frontend Tests heap fix.

This also serves as the trivial commit needed to re-trigger the
master CI run against the now-fixed libqemu binaries (v1.1.1
served from the license endpoint).
2026-05-19 17:05:20 +02:00
David Montero Crespo 11d08612da
Merge pull request #198 from davidmonterocrespo24/fix/esp32-worker-callback-iothread-and-fe-heap
fix(ci+esp32): unblock backend e2e + bump frontend node heap
2026-05-19 11:08:01 -03:00
davidmonterocrespo24 cfde1eb27c fix(ci+esp32): unblock backend e2e + bump frontend node heap
Two CI failures landed after PR #196 (esp32-gpio-matrix-cb-callback)
merged. Both are independent and fixed here together.

1) **Backend E2E: ESP32 hangs at bootloader handoff.**
   PR #196 added picsimlab_gpio_matrix_cb which fires on QEMU's
   iothread. The handler did `_emit({...})` for every routing
   change — and the ESP-IDF bootloader writes to gpio_out_sel
   *hundreds* of times during early boot (each peripheral init
   configures its matrix slot). Each emit acquires _stdout_lock
   and writes to the worker→manager pipe. If the manager drains
   even briefly slow, the pipe fills, write blocks, and the
   iothread stalls — symptom: ESP32 reports `entry 0x400805e4`
   then no Arduino setup() output for 75 s.

   Fix: the iothread callback now ONLY mutates the SignalRouter
   snapshot. It never emits. The 10 Hz poll thread
   (_refresh_signal_routing) stays as the sole emitter, so the
   wire-format event stream is unchanged. Benefit of having the
   callback over poll-only is reduced worst-case routing-emit
   latency (next poll tick vs up to 100 ms) and a warmer
   snapshot dict for cheaper poll diffs.

2) **Frontend Tests: Node OOM at end of suite.**
   117 test files run in one forks-pool worker. Several lazy-load
   the ngspice emscripten module (~30 MB), the MixedModeScheduler
   singleton, and other heavy modules whose dispose hooks aren't
   reached because singletons leak across files. Cumulative heap
   pressure exceeds Node's 4 GB default; the worker hits "Ineffective
   mark-compacts near heap limit" AFTER all 1881 tests pass and
   the OOM kill is reported by vitest as "Worker exited unexpectedly
   / Timeout terminating forks worker". This is not a real test
   failure — every individual test passes.

   Quick fix: pass NODE_OPTIONS=--max-old-space-size=8192 to the
   `npm test` step. Long-term, the singletons should add dispose
   hooks that test fixtures call in afterAll(), or the suite
   should shard into multiple `vitest run --shard` invocations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:04:50 +02:00
David Montero Crespo be55c97cce
Merge pull request #197 from davidmonterocrespo24/fix/components-metadata-stale-and-vitest-worker-hang
fix: regenerate components-metadata + plug vitest worker leak
2026-05-19 10:52:28 -03:00
davidmonterocrespo24 7f0f72862c fix: regenerate components-metadata + plug vitest worker leak
Two CI failures landed together on master after PR #194 merged:

1. **components-metadata.json stale.** The `power-supply` thumbnail
   in scripts/component-overrides.json was updated (grey placeholder
   → branded PSU SVG with voltage/current labels) but the generated
   JSON wasn't regenerated. The pre-merge check
   `git diff --quiet frontend/public/components-metadata.json` now
   fails on master. Fix: `cd frontend && npm run generate:metadata`,
   commit the result.

2. **Frontend Tests > test (20/22): vitest worker hang.**
   `circuit-simulation-service.test.ts` had been calling
   `service.start()` in ~10 tests without storing the returned
   unsubscribe handle. Each call subscribes the service to the
   simStore; the listener captures the service + scheduler in
   its closure. After all tests complete, vitest's forks pool
   tries to terminate the worker but the still-active listeners
   keep the event loop pinned, producing:
       "Worker exited unexpectedly / Timeout terminating forks worker"
   All assertions actually pass — only the worker shutdown hangs.

   Fix: introduce a `startTracked(service)` helper that records
   the unsubscribe in a module-level array, plus an `afterEach`
   that drains the array. `__resetMixedModeScheduler()` still runs
   after to dispose the scheduler singleton. Replaced all 9 raw
   `service.start()` callsites.

Both are independent of any production code change. The fix is
test/scaffolding only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:38:22 +02:00
David Montero Crespo 2e0e92f4f6
Merge pull request #196 from davidmonterocrespo24/feat/esp32-gpio-matrix-cb-callback
feat(esp32-worker): register picsimlab_gpio_matrix_cb callback
2026-05-19 10:30:05 -03:00
davidmonterocrespo24 437f5f83bc feat(esp32-worker): register picsimlab_gpio_matrix_cb callback
Wires the new synchronous GPIO Matrix callback exposed by
libqemu-{xtensa,riscv32} 1.1.0 (lcgamboa/qemu commit e178ff5).
Whenever the firmware writes GPIO_FUNCx_OUT_SEL_CFG_REG, the C
plugin now fires picsimlab_gpio_matrix_cb(gpio, signal_id) inline.

The handler:
- Treats signal_id == 0x100 or 0 as "matrix routing cleared" and
  emits gpio_routing_clear.
- For LEDC HS/LS range signals (the only ones the frontend
  SignalRouter currently consumes), updates the mirror and emits
  gpio_routing.
- Drops other signals — the mirror does not need to track them
  yet, and emitting them would only fatten WS frames.

Backwards compat:
- Older libqemu (<1.1.0) doesn't expose the new field; the
  picsimlab_gpio_matrix_cb placeholder runs (no-op) and the
  100 ms _refresh_signal_routing() poll thread continues to feed
  the mirror. WS event shape is identical either way.

Burn-in: keeping the poll thread active in parallel with the
callback for now. Once telemetry confirms parity (per phase 4 doc
in velxio-prod/project/esp32-gpio-matrix-cb/), the poll thread
gets retired in a follow-up commit.
2026-05-19 08:24:52 +02:00
David Montero Crespo bd23a05d23
Merge pull request #195 from davidmonterocrespo24/feat/chip-programmable-rom
Feat/chip programmable rom
2026-05-19 02:28:49 -03:00
David Montero Crespo 321997715d feat(editor): Monaco syntax highlight for 8080/Z80 assembly
When the editor opens a .s or .asm file (the chip-program files routed
to /api/compile-rom), Monaco now colorizes 8080/Z80 mnemonics, registers,
hex/binary literals, comments, and directives. Same highlighter covers
both ISAs since most mnemonics overlap.

- frontend/src/components/editor/retroAsmLanguage.ts: a Monarch tokenizer
  + LanguageConfiguration + idempotent registration helper. Recognises
  the full 8080 ISA, all the Z80 additions (LD/JR/DJNZ/EXX/EX/IM/LDIR/
  bit ops/index ops), the directives ORG/DB/DW/EQU/END, and registers
  including condition codes (NZ/Z/NC/etc.) and IX/IY.

- CodeEditor.tsx: maps `.s` and `.asm` to the new `retro-asm` language
  and calls `registerRetroAsm(monaco)` in beforeMount so the language
  exists by the time the editor first paints. Other extensions
  (.ino/.cpp/.c/.py/.json/.md) behave exactly as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:49:51 -03:00
David Montero Crespo 0e2f0790db feat(chips): C-to-Z80 compile via SDCC + LED chaser example
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>
2026-05-19 00:31:10 -03:00
David Montero Crespo 96ef12b585 feat(chips): programmable Z80 chip + Larson scanner example
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>
2026-05-19 00:21:08 -03:00
David Montero Crespo b4358786d6
Merge pull request #194 from davidmonterocrespo24/fix/pinmanager-test-mocks
fix(tests): update mocks + assertions for PinManager API changes
2026-05-19 00:16:21 -03:00
davidmonterocrespo24 666f9c4008 fix(tests): update mocks + assertions for PinManager API changes
PR #192 (spice-led-pipeline) added two PinManager changes that were
not reflected in the test mocks / assertions:

- `setPinState(pin, state)` gained an optional `source: 'mcu' |
  'external'` third arg. Production ESP32-C3 / RISC-V simulators
  now pass `'mcu'` to mark the call as an MCU output (so the SPICE
  collector emits a V-source). The esp32c3-blink and esp32c3-simulation
  tests asserted on the old 2-arg shape.
- `resetPinStates()` is a new public method on PinManager called by
  `stopBoard` / `resetBoard` to clear cached pin states. The mocks in
  esp32-integration.test.ts and multi-board-integration.test.ts did
  not add it, so any test that ran stopBoard hit
  `TypeError: getBoardPinManager(...)?.resetPinStates is not a function`.

This commit:
- Adds `'mcu'` to the two ESP32-C3 setPinState assertions.
- Adds `this.resetPinStates = vi.fn()` to both integration mocks.

These are pure test fixups — no production code touched. The
`circuit-simulation-service.test.ts > handleMcuEdge` failure
(`expected 1 to be 2`) is a separate regression in production code
introduced by PR #192 and is NOT fixed here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:52:30 +02:00
David Montero Crespo bbf8cd0303 feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.

SDK:
- velxio-chip.h gets two new host imports:
    uint32_t vx_rom_size(void);
    void     vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
  CPU-emulator chips call these in chip_setup to pull their program out
  of the host's romBytes property.

Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
  imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
  it through.
- Component registry declares three new custom-chip properties:
  romBytes (base64), programFile (matching project filename), and
  programTarget (cpu name).

New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
  Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
  loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
  32 KB of external ROM.

Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
  chip-program source into ROM bytes. 8080 ASM is assembled by the
  in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
  Intel HEX records are parsed; raw .bin is passed through. Future targets
  (z80, 8086, 4004) are scaffolded but not wired yet.

EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
  routes to compile-rom instead of arduino-cli. The compiled bytes are
  injected into every custom-chip on the canvas whose programFile property
  matches the active filename (or is empty).

Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
  the programmable i8080-cpu chip. killbits.s is shipped as a project
  file alongside sketch.ino; the user clicks Compile then Run and the
  LED walks across 8 outputs, buttons kill it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:38:18 -03:00
David Montero Crespo ca1bf00597
Merge pull request #193 from davidmonterocrespo24/esp32-cleanup-broadcast-pwm
refactor(esp32): retire ledc_update + broadcastPwm + channelGpioMemo
2026-05-18 23:23:05 -03:00
davidmonterocrespo24 ba59fd4b4a refactor(esp32): retire ledc_update + broadcastPwm + channelGpioMemo
The SignalRouter path has been in prod through Phase 2.5 / Phase 3.3
deploys without regressions, so the temporary fallback shipped in
commit 77bf897 can come out. Closes #101.

Backend (esp32_worker.py + esp32_lib_manager.py):
- Stop emitting `ledc_update` from the 0x5000 LEDC callback and from
  the polling thread. Only `ledc_duty` (channel + duty_pct) and the
  GPIO matrix routing events ship now.
- Drop the channel→gpio reverse-lookup that fed the legacy event.

Frontend:
- Delete `PinManager.broadcastPwm` and `PinManager.pwmListenerPinCount`.
- Delete `makeLedcUpdateHandler` + its `channelGpioMemo`.
- Delete `Esp32Bridge.onLedcUpdate` field + the `case 'ledc_update':`
  message handler + the `LedcUpdate` type.
- Strip `this.onLedcUpdate = null` from 14 test mocks.
- Rewrite the `does not call broadcastPwm` guard in
  esp32-multi-servo-gpio-matrix.test.ts to assert the method itself
  no longer exists on PinManager (stronger regression guard than the
  spy version, and doesn't need vi).
- Remove the `PinManager.broadcastPwm fallback` describe block from
  esp32-servo-pot.test.ts — every test in it exercised the deleted
  fallback path.

Docs (ESP32_EMULATION.md):
- Replace `ledc_update` rows in the events / implementation tables
  with the SignalRouter trio (`ledc_duty`, `gpio_routing`,
  `gpio_routing_clear`).
- Update the visual flow diagram + the "why this matters" paragraph
  to past-tense the broadcastPwm bug.

Tests: 1886 frontend tests pass (the previously-failing
board-kinds-coverage test that needed the new Pi Zero/1/2 kinds is
also green). Backend unit suite: 279 pass, the 11 espidf_real_paths
prereq failures are environment-dependent (need arduino-cli libs in
the local shell) and unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:05:34 +02:00
David Montero Crespo b98b1bf1df
Merge pull request #192 from davidmonterocrespo24/fix/spice-led-pipeline
Fix/spice led pipeline
2026-05-18 22:48:21 -03:00
David Montero Crespo d898f122ec test(visual-led): add RGB + 7-segment leafCheck assertions
Two new harness modes that would have caught the PinTracer signature
bug fixed in 55b3dd2:

- `leafCheck: 'rgbLed'` — samples wokwi-rgb-led.ledRed/Green/Blue 16
  times across a fade cycle and asserts each channel takes ≥2 distinct
  values. The buggy version stayed at {0} for every channel because the
  resolver locked itself to FLOATING and onChange never fired.

- `leafCheck: 'sevenSegment'` — samples wokwi-7segment.values 12 times
  and asserts ≥4 distinct segment patterns. Counter sketches naturally
  hit 10+ patterns when working; ≤1 means the segment subscribers never
  saw an edge.

Both checks are now in the default suite alongside Blink, Button,
Traffic-Light, Fade. Result with current main: 6/6 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:39:01 -03:00
David Montero Crespo 55b3dd29e5 fix(DynamicComponent): pass componentId through to PinTracer
`PinTracer` signature is `(componentId, componentPinName) => number | null`
but the local `getArduinoPin` lambda only accepted one arg and used the
closure-captured `id`. When `createDefaultPinResolver` passed both args
(per the typed signature), JS bound the FIRST arg (the componentId) into
the lambda's single `componentPinName` parameter. `traceDetailed` then
looked up a pin literally named "rgb-led-1" on component "rgb-led-1",
returned null, and the resolver locked itself into 'FLOATING' state —
its onChange path never subscribed and the wokwi-rgb-led element's
ledRed/ledGreen/ledBlue stayed at 0 forever even as the SPICE side
correctly cycled through R, G, B, Y, C, M, W via analogWrite().

Same bug latent for any multi-pin component that goes through the
PinResolver path (multi-pin LEDs, RGB strips, 7-seg drivers, anything
that calls `getPinResolver(<pinName>)` for several pin names).

Fix: lambda now accepts both shapes — `getArduinoPin(pinName)` (legacy
single-arg used by every PartSimulationRegistry handler) AND
`getArduinoPin(componentId, pinName)` (PinTracer 2-arg form used by
createDefaultPinResolver / createSpiceResolvedPinResolver). Picks the
right componentId in either case.

Verified via the rgb-led example: ledRed/ledGreen/ledBlue now cycle
0→255→0 in sync with the SPICE node voltages on pins 9/10/11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:08:06 -03:00
David Montero Crespo 677d3a673a
Merge pull request #191 from davidmonterocrespo24/fix/spice-led-pipeline
Fix/spice led pipeline
2026-05-18 21:55:26 -03:00
David Montero Crespo 81837eedb9 fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples
End-to-end pipeline fixes uncovered while auditing the /examples gallery.
Each bug shipped past green unit + snapshot tests because none of those run
firmware + render LEDs. Added scripts/visual-led-test.mjs as a CDP-driven
visual harness that loads each example, runs the simulator, samples
`wokwi-led.brightness`, and asserts toggle / gradient / initial-off
invariants — exits non-zero on any regression.

Frontend simulator
- PinManager.updatePort: new optional ddrMask param. A pin is added to
  `outputPins` only if the DDR bit is set, so the PORTx write that
  enables INPUT_PULLUP (DDR=0, PORT=1) no longer falsely marks the pin
  as MCU output. AVRSimulator now reads DDRB/C/D (0x24/0x27/0x2A on
  Uno/Nano, 0x37 on ATtiny85, per-port table on Mega) and forwards it.
- AVRSimulator: pass DDR mask alongside every port-listener fire.
- BasicParts pushbutton{,-6mm}: seed pin HIGH in attachEvents so
  `digitalRead()` returns HIGH while idle. avr8js doesn't auto-simulate
  INPUT_PULLUP — without this the firmware reads LOW from boot and
  thinks the button is permanently pressed (the "LED is always on,
  pressing does nothing" UX bug).
- connectMcuEdgesToService: suppress synthetic digital edges on pins
  with active PWM, AND subscribe to onPwmChange to re-tick the netlist
  on duty changes. Fade-LED now produces a true gradient (6 distinct
  brightness levels across a fade cycle) instead of a binary 0/full
  toggle.
- CircuitSimulationService.handleMcuEdge: replace single-slot
  pendingMcuEdge with a per-pin Map. Multiple pins toggling during the
  same in-flight tick used to overwrite each other; now every pin's
  most-recent edge replays after the tick. Fixes Traffic-Light RED→
  YELLOW→GREEN sequencing.
- NetlistBuilder: new sanitizeSpiceId() helper replaces hyphens with
  underscores in V-source names. ngspice's interactive `alter` command
  treats `-` as an operator and silently no-ops on hyphenated source
  names, so mid-simulation MCU pin transitions stopped propagating
  after the first solve. MixedModeScheduler.onMcuPinChange and
  CircuitSimulationService self-heal use the same sanitizer so names
  stay consistent across emit/alter/lookup. Also added a regex-based
  fallback in step 2 so any board pin matching `GND.\d+` canonicalises
  to net "0" — ESP32-C3 dev kits expose up to 10 GND pins and the
  per-board `groundPinNames` list missed several, leaving wires
  floating instead of grounded.
- collectPinStates: emit V-sources only for pins in `outputPins`, not
  every wired board pin. Leaves INPUT pins (analog sensors on A0,
  pull-down dividers, etc.) free for the SPICE solver instead of being
  shorted to 0 V by an ideal MCU V-source.
- start.ts: extended __spiceDebug to also expose outputPinsByBoard +
  nodeVoltages + pinNetMapEntries for the visual harness.
- ESP32 / RP2040 / RISC-V / C3 simulators: pass `'mcu'` source flag to
  triggerPinChange / setPinState so the new outputPins tracking fires
  on those boards too (was AVR-only before).
- useSimulatorStore: stopBoard/resetBoard call pm.resetPinStates() so
  outputPins clears between runs; Esp32Bridge.onPinChange passes the
  `'mcu'` flag in all three places it's wired.
- types/board.ts: ATtiny85 FQBN `clock=internal16mhz` →
  `clock=16pll` (ATTinyCore 1.5.2 renamed the option).

Backend
- esp-idf-template/main/CMakeLists.txt: skip the
  `-DLED_BUILTIN=2` fallback for esp32c3 and esp32s3 targets. Both
  variants already define LED_BUILTIN in pins_arduino.h via a
  self-define macro (`#define LED_BUILTIN LED_BUILTIN` + `static const
  uint8_t LED_BUILTIN = ...;`). Pre-defining the symbol from the
  command line expanded the static-const declaration to
  `static const uint8_t 2 = ...;` — a syntax error that broke every
  ESP32-C3 / S3 build (`expected unqualified-id before numeric
  constant`).

Examples
- examples.ts: bulk-fix 72 wire endpoints that referenced
  `componentId: 'nano-rp2040'` / `'esp32-c3'` etc. (boards that don't
  exist on the canvas). Replaced with `'arduino-uno'` (the canvas
  board-id convention) and converted `D<n>` pin names to `GP<n>` for
  Pico-style boards. Affects pico-blink, pico-i2c-scanner,
  pico-i2c-rtc-read, pico-spi-loopback, c3-blink and others.

Tests
- scripts/visual-led-test.mjs: CDP-driven harness. Default suite covers
  Blink (single-pin), Button (idle-OFF invariant — catches the
  INPUT_PULLUP regression), Traffic-Light (multi-pin sequencing),
  Fade-LED (PWM gradient — ≥3 distinct levels), RGB-LED (≥3 PWM pins
  driven). Run via `npm --prefix frontend run test:visual` against a
  Chrome on `:9222` + vite on `:5174` + backend on `:8001`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:52:07 -03:00
David Montero Crespo a179f8492e Refactor code structure for improved readability and maintainability 2026-05-18 21:50:45 -03:00
David Montero Crespo 14613f152f feat(compile): ESP-IDF compile options + request dedup
Backend:
- api/routes/compile.py            accepts board-specific compile options
                                   and dedups in-flight identical requests
- services/espidf_compiler.py      expanded ESP-IDF wrapper with the new
                                   options surface (sdkconfig.defaults.in
                                   template added)
- services/arduino_cli.py          honour the new options envelope
- services/esp32_lib_bridge.py     thread board options through to QEMU

Tests:
- tests/test_compile_request_dedup.py  end-to-end dedup behaviour
- tests/test_espidf_options.py     covers the new options parsing

Frontend:
- services/compilation.ts          client-side mirror — sends the new
                                   options field on every compile request

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:50:45 -03:00
David Montero Crespo 16f168ba7f feat(board-options): per-board options modal + persistence
Adds a new BoardOptionsModal accessible from the EditorToolbar that exposes
per-board options (currently used for board-specific compile flags). Wires
the modal through:

- types/boardOptions.ts            new BoardOptions shape
- types/board.ts                   BoardInstance gains `boardOptions` + `spiffsFiles`
- store/useSimulatorStore.ts       boardOptions persisted in loadProjectState
- components/editor/EditorToolbar.tsx     button to open the modal
- components/simulator/BoardOptionsModal.{tsx,css}  the modal itself
- components/simulator/SimulatorCanvas.tsx  passes the options through
- utils/projectPayload.ts          board options serialised in saved projects
- pages/ProjectByIdPage.tsx        re-includes the by-id loader needed for
                                   project URLs that reference boards with
                                   their persisted options.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:50:45 -03:00
David Montero Crespo 6a1e506db6
Merge pull request #190 from davidmonterocrespo24/pi-phase-3.3-armhf
feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators
2026-05-18 19:42:09 -03:00
David Montero Crespo 9c5d3fe973
Merge pull request #189 from davidmonterocrespo24/pi-phase-2.5-test-fix
fix(test): pi3 bme280 attach test sys.path + block-read race
2026-05-18 19:41:41 -03:00
davidmonterocrespo24 18a582455c feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators
Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to
kill init" panic as `mount /dev/vda` failing with EINVAL — Debian
armmp does not have ext4 builtin (only fuseblk in /proc/filesystems).

- qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries.
  All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio
  virtio transport (arm-32 virt PCI fails -75 due to missing reg DT
  property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile;
  Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus
  for virtio-blk-pci vs virtio-blk-device (and serial likewise).
- manifest.json: new `raspberry-pi-armhf` image_set wiring three
  assets (kernel + initramfs + zstd rootfs).
- Frontend BoardKind gains the three new kinds + an isPiBoardKind()
  helper. Replaces the eight scattered `=== 'raspberry-pi-3' ||
  === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in
  useSimulatorStore, Interconnect, loadExample, boardProtocols.
  ComponentRegistry gets three new picker entries.
- board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds
  (backend boards have no canvas examples).

The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes
live in velxio-prod's scripts/ (private overlay) — the upstream
kernel build script only knows about arm64; armhf is built in the
private repo because the assets ship through the license endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:23:48 +02:00
davidmonterocrespo24 ebace63ae5 fix(test): pi3 bme280 attach test sys.path + block-read race
Two small fixes after running the test inside the prod container for
the first time:

- The prod image lays out the backend at /app/app/, not /app/backend/app/
  (the Dockerfile.standalone COPYs only the inner package). Use /app
  as the sys.path root so `from app.pro.services import ...` resolves.
- The CHIP=0x60 and BLOCK= prints race against the socket drain. The
  test was treating "saw CHIP= but BLOCK= not in buffer yet" as a
  hard failure and exiting before the second I2C read finished.
  Gate the success path on both markers present and keep polling
  otherwise.

Verified end-to-end in the prod container:

    [proto] >>> ['I2C', '1', '76', 'RR', 'd0', '1']
    [proto] <<< I2C_DATA 1 76 60
    [proto] >>> ['I2C', '1', '76', 'RR', 'f7', '8']
    [proto] <<< I2C_DATA 1 76 530280155e607b50
    [test] OK — guest read chip ID = 0x60
    [test] OK — block read BLOCK=530280155e607b50

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:39:12 +02:00
David Montero Crespo 33cca9f08a
Merge pull request #188 from davidmonterocrespo24/pi-phase-2.5
feat(pi): pluggable slave handler + canvas wire detection for I2C/SPI…
2026-05-18 15:25:35 -03:00
David Montero 391817d2c9 fix(components-metadata): sync power-supply override + regenerate
Two pieces of drift introduced in 305170a (Regulated Power Supply):

1. The committed components-metadata.json carries a custom rich
   thumbnail SVG (showing 5.00V / 1.00A / PSU labels), but the
   _customComponents override has no `thumbnail` field — so any
   regen via `npm run generate:metadata` replaces it with the
   generic placeholder. CI catches the drift and fails.
   Fix: lift the rich SVG into the override entry.

2. The committed metadata description is a short one-liner while
   the override description is the longer explanatory version.
   The override is the source of truth, so the metadata now
   matches: longer description wins.

Verified locally: regenerator now produces zero diff against the
committed metadata.
2026-05-18 19:32:34 +02:00
David Montero Crespo 7ce184c4c6 fix(spice): always emit V-source per wired GPIO pin (root cause of intermittent dark-LED)
Bug reproduced via CDP probe across 5 Run/Stop cycles: cycle 1
worked (LED toggled), cycles 2-5 LED stayed dark — but exactly the
same code, same canvas, same circuit.

Tracing the live electrical store via __spiceDebug() showed:
  cycle-1 after-run: branchCurrentCount=3 (pin13 V-source present)
  cycle-2 after-run: branchCurrentCount=2 (pin13 V-source MISSING)
  cycle-3..5 after-run: branchCurrentCount=2

The flow:
  1. User clicks Run -> board.boards reference changes -> service ticks.
  2. runSolve calls collectPinStates(board, ...) to snapshot output pins.
  3. collectPinStates was emitting an entry ONLY when pinManager.getPinState(pin)
     was currently TRUE. If the pin was LOW at that exact instant
     (which is most of the time for a Blink sketch — 50% duty), no
     pinStates entry, no V-source card in the netlist.
  4. AVR runs, digitalWrite(13, HIGH) fires, handleMcuEdge calls
     scheduler.onMcuPinChange -> solver.alterSource('V_arduino-uno_13', 5).
  5. ngspice gets 'alter V_arduino-uno_13 dc 5' but that V-source
     doesn't exist in the deck. Silent no-op. branchCurrents never
     updates. LED stays dark forever.

The 'sometimes it works' impression came from cycle 1: the cold-boot
AVR happened to land on a HIGH state precisely when the tick fired,
so the V-source got emitted and every subsequent edge alter worked.
The other cycles caught the AVR in LOW.

Fix: always emit a digital PinSourceState — with v=0 when LOW, v=vcc
when HIGH — so the NetlistBuilder always produces V_<board>_<pin>
cards for every wired GPIO. alterSource then has a target to bind
to no matter what state the pin was in at solve time.

Verified live via CDP probe (_probe_blink.mjs in working tree):
  pin13 toggles 0V<->5V at the Blink frequency
  LED anode follows at 0V<->1.838V (matches manual calculation:
    (5 - 1.84) / 220 = 14.4 mA forward current through the red LED)
  branchCurrentCount = 3 stable across all cycles
2026-05-18 13:32:31 -03:00
davidmonterocrespo24 2072011fa4 feat(pi): pluggable slave handler + canvas wire detection for I2C/SPI/UART
Adds the public extension points the velxio-prod overlay uses to bind
real canvas-side I2C/SPI/UART models (BME280, future MCP23017, etc.)
to a running Pi guest's protocol shims:

- qemu_manager: set_pi_slave_handler(fn) / get_pi_slave_handler() for
  pi_attach_slave + pi_detach_slave WebSocket messages. OSS image
  leaves the hook unset so the messages are silently dropped.
- simulation route: parses the two new WS message types and forwards
  them to the registered handler when present.
- RaspberryPi3Bridge: attachSlave(spec) / detachSlave(spec) frontend
  side of the protocol.
- piSlaveScanner: at simulation start walks components + wires,
  identifies I2C/SPI/UART peers wired to Pi protocol pins (40-pin
  header physical-pin numbering), and emits one attach per
  bus/address pair (deduped across SDA+SCL wires).
- RaspberryPiWorkspace: invokes the scanner once the bridge is open,
  with retries to ride out the WS-still-connecting race.
- integration test: pi3_bme280_attach.py boots the Pi, pre-attaches a
  BME280 via the slave handler, runs a host-side proto loop, runs
  guest python smbus2.read_byte_data(0x76, 0xD0) and asserts the
  console reads back CHIP=0x60.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:26:37 +02:00
David Montero 683a7f31e6 test(coverage): accept raspberry-pi-4 and raspberry-pi-5 as uncovered
Pi 4 and Pi 5 were added to the BoardKind union in db5e3a8 ("feat(pi3
phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS") but no gallery example
ships for them — same situation as Pi 3, which is already accepted.
All three boot a full Linux image under QEMU on the backend; there is
no in-browser canvas demo to register.
2026-05-18 15:56:54 +02:00
David Montero Crespo 5f378ed411 fix(spice): allow hyphens in voltage-source name regex (root cause of dark-LED bug)
The user-reported 'LED with proper series resistor shows 1.84 V at
the anode but never visually lights up' had its root cause here,
not in ngspice / not in the LED brightness handler / not in any
component id naming choice. ngspice parses 'V_led-builtin_sense'
just fine; the diode conducts and the node voltage is exactly what
you'd compute by hand.

What breaks is the JS regex that scans the emitted netlist to
collect voltage-source names so CircuitSimulationService can ask
the scheduler to read their branch-current vectors:

  const m = card.match(/^([Vv][_\w]*)\s/);

[_\w]* doesn't accept '-'. For a card 'V_led-builtin_sense …' the
capture is 'V_led' (truncated at the hyphen). The voltageSources
array gets the wrong name; CircuitSimulationService pushes
'i(v_led)' into extraVectorsOfInterest; ngspice has no such vector
so the readVec promise rejects silently; branchCurrents['v_led-builtin_sense']
is never populated; the LED handler in BasicParts.ts sees raw =
undefined, the SPICE-memo path is skipped, and the digital fallback
runs but only sets the LED on when the PinResolver classifies the
anode as a direct GPIO connection (which it does NOT when an
intermediate resistor is in series). Dark LED.

Fix is adding '-' to the character class. One character. All five
existing examples I previously 'fixed' by just adding a series
resistor will now light up correctly without renaming any of their
component ids. Same for any saved user project with hyphenated ids
and for the auto-generated picker ids that used to contain hyphens.

The earlier underscore-id workarounds (default canvas + picker
template) stay in place as defense in depth — they don't break
anything and they keep the SPICE side clear of avoidable special
characters.
2026-05-18 10:45:06 -03:00
davidmonterocrespo24 db5e3a8623 feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS
Backend: extract per-board config into a PI_CONFIGS dict keyed by
board_type. Pi 3/4/5 share the same arm64 image set (kernel +
initramfs + rootfs) and differ only in QEMU -cpu and -m:

  raspberry-pi-3 → cortex-a53  + 1G  (BCM2837, ARMv8 64-bit)
  raspberry-pi-4 → cortex-a72  + 2G  (BCM2711, ARMv8 64-bit)
  raspberry-pi-5 → cortex-a76  + 2G  (BCM2712, ARMv8 64-bit)

PiInstance now carries board_type so the per-board lookup happens
once at start_instance time. Unknown board_type falls back to
DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for
back-compat with older clients).

Pre-warm hook walks every unique image_set in PI_CONFIGS so the
provider only downloads each set once even when several Pi models
are registered.

Frontend:
- BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'.
- BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards
  (FQBN null since they use the Pi VFS + Python toolchain like Pi 3).
- ComponentRegistry inserts two new component metadata entries
  cloning the Pi 3 board art with different thumbnail colours.
  Tag name reused so the same velxio-raspberry-pi-3 web element
  draws the board on the canvas — the 40-pin GPIO layout is
  identical across Pi 3/4/5.
- boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table
  (PI3_BCM) since the 40-pin header layout is identical.
- loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS
  ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike.
- Interconnect.isPi3Bridge() recognises all three Pi family members
  so Arduino↔Pi serial routing keeps working.
- RaspberryPi3Bridge constructor gained a boardKind parameter
  defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message
  now ships the actual board kind so the backend knows which
  PI_CONFIGS entry to use.
- useSimulatorStore.addBoard wires bridge construction for all
  three Pi family members.

Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel
package + armhf rootfs build, no change here.

Smoke-tested inside the prod container:
  Pi 4 (cortex-a72) → reached agetty login on hvc0
  Pi 5 (cortex-a76) → reached agetty login on hvc0
Both show 'aarch64' in uname -m.
2026-05-18 15:41:29 +02:00
David Montero Crespo 32f5b407af fix(simulator): underscore-separated component ids for SPICE safety
The user reported the default editor canvas — Arduino Uno + LED +
220Ω resistor — was correctly powered (1.84 V at the LED anode,
14 mA through the diode) but the LED visual stayed dark. Only the
built-in pin-13 LED on the wokwi-arduino-uno element lit up.

Root cause: ngspice's WASM build truncates branch-current vector
keys at the first hyphen. A sense source named V_led-builtin_sense
ends up exposed under a key like v_led#branch rather than the
expected v_led-builtin_sense#branch. CircuitSimulationService and
BasicParts.ts both look up the FULL key, miss, and the LED's
brightness update treats raw as undefined → digital-fallback path
runs but the SPICE memo timestamp is fresh so HOLD keeps zero
brightness. Visible symptom: a perfectly conducting LED that never
lights.

Fix in two places:
  - Default canvas (useSimulatorStore.ts): rename 'led-builtin' /
    'r-builtin' to 'led_builtin' / 'r_builtin' (and the matching
    wire ids).
  - DynamicComponent.tsx makeNewComponent: the id template was
    'metadata.id-timestamp-rand' producing hyphens for every
    user-added component too. Switched to underscores, AND replace
    any hyphens already in metadata.id (e.g. 'led-bar-graph') so
    the prefix doesn't reintroduce the bug.

Existing saved projects whose ids contain hyphens are not migrated
here — those will keep the visual bug until either the operator
edits the components or we add a sanitisation step inside
componentToSpice + BasicParts. The next follow-up commit can add
that if you confirm this default-canvas fix works.
2026-05-18 10:01:48 -03:00
davidmonterocrespo24 ca2b76f1f8 test(pi3): fix Phase 2 E2E test + bump rootfs to shims-final
The Phase 2 E2E test was sending the Python GPIO command via
'python3 -c "..."' but bash quote-nesting silently corrupted the
script — the python process started, printed nothing, exited 0, and
the test asserted 'GPIO_SETUP 17 out' was missing in proto bytes
(it never got sent because the python script never ran).

Switch the test to base64-encode the script + pipe through base64 -d
into a file, then execute. Verified end-to-end now:

  [test] proto received 36 bytes:
      GPIO_SETUP 17 out pud_off
      GPIO 17 1
  [test] ✓ shim → proto pipeline works

Also bump the rootfs manifest entry to the final Phase 2 build
(d6d4a274 raw / debd1c33 zst, version 2026.05+phase2-shims-final).

Earlier auto-discovery in _transport.py was hanging at import time
on some glob/sysfs interaction. Now hardcoded /dev/vport1p1 which
is the empirical path under -M virt + virtio-blk-pci on slot 0.
2026-05-18 15:00:21 +02:00
David Montero Crespo 5c5336acc0
Merge pull request #187 from davidmonterocrespo24/feat/retro-intel-cpus
feat(chips): port retro Intel/Zilog CPUs as Velxio custom chips + 2 d…
2026-05-18 02:30:23 -03:00
David Montero Crespo b714c79e3d feat(chips): port retro Intel/Zilog CPUs as Velxio custom chips + 2 demos
Adds 17 chips from the test/test_intel clean-room research to the Custom
Chip gallery, all sourced from manufacturer datasheets and validated by
the existing 129-test vitest harness (CPUDIAG end-to-end for the 8080,
ZEXDOC for the Z80).

CPUs: 4004, 4040, 8080, 8086, Z80 (categoria retro-cpu)
Bus chips: rom-32k, ram-64k, rom-1m, latch-8282, 4001-rom, 4002-ram,
           8255-ppi, 8251-usart, 8259-pic, 8253-pit (retro-bus)

Two bundled "mini-computer" demos under retro-bundle that drop on the
canvas as a single chip and run real 8080 code out of an embedded ROM:

  * i8080-repl     8080 + RAM + ROM + UART, prints a banner and an
                   "uptime ticks: 0xNN" counter every ~50 ms via a real
                   DCR/JNZ busy-wait. Visible in Serial Monitor.

  * i8080-counter  8080 + RAM + ROM + 8 LED pins + 2 button pins.
                   Counts up in binary on BTN_INC, clears on BTN_RST.

Two example projects under /examples reuse these chips end-to-end:

  * /examples/i8080-banner-streamer
  * /examples/i8080-button-counter

The bundled chips inline a 328 / 34-byte 8080 ROM produced by a new
two-pass 8080 assembler in Python (scripts/asm8080.py) from the .s
sources in scripts/. Both ROMs are pre-assembled and committed under
scripts/*.txt so contributors can rebuild deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 02:27:53 -03:00
davidmonterocrespo24 eac845005c feat(pi3 phase 2): bump rootfs to autodiscovery shims + E2E test
The Phase 2 shim originally hardcoded CHARDEV_PATH = /dev/vport0p2.
With QEMU 10 -M virt + virtio-blk-pci consuming slot 0, the proto
port actually lands at /dev/vport1p1 (or further). _transport.py
now walks /sys/class/virtio-ports/*/name looking for the literal
qemu name 'velxio-protocol' set by qemu_manager.

test/pi3_protocols/test_pi3_protocols.py end-to-end runner:
  1. Boot QEMU virt + pipe chardev (same args as production)
  2. Connect cons TCP socket + open both ends of the FIFO pair
  3. Wait for agetty autologin
  4. Run 'import RPi.GPIO; setup(17, OUT); output(17, 1)' in guest
  5. Read FIFO.out, assert 'GPIO_SETUP 17 out' + 'GPIO 17 1' appeared.

Catches: shim path resolution wrong, pipe chardev regression,
mux protocol drift, site-packages overlay broken.
2026-05-18 07:05:12 +02:00