Commit Graph

48 Commits

Author SHA1 Message Date
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 4cb5748dce feat(custom-chip): make chip pins first-class circuit nodes (digital + SPICE)
A custom-chip output pin wired directly to a component (LED, resistor, ...)
had no Arduino pin on its net, so the chip could drive nothing and the pin
resolved to null. Now:

- Layer A (digital): such chip pins get a stable synthetic pin number
  (syntheticPins.ts). traceDetailed resolves a chip<->component net to that
  shared number, so the chip's PinManager drive reaches the wired components
  through the existing digital event flow. A real board pin still wins.
- Layer B (analog/SPICE): a custom-chip mapper in componentToSpice emits a DC
  voltage source on each driven output pin's net (recorded in chipPinDrives by
  ChipRuntime), exactly like a board GPIO, and the chip requests an electrical
  re-solve when it toggles a pin (electricalResolveHook -> service.tick).
  So LEDs / resistors / analog parts wired to a chip output are driven by
  ngspice too.

This makes the bundled Z80 / i8080 chip examples actually animate their LEDs,
and lets any custom chip drive components, passives and analog circuits from
its own pins. Non-chip circuits are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 05:40:49 +02:00
David Montero f619a2cc7e feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring)
The BoardKind type and the QEMU backend already supported
raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing
the Pi 3 arm64 image set, but the frontend had no way to actually
select either: the board picker, the canvas renderer, the serial
monitor, the oscilloscope channel list, and the editor toolbar all
hard-coded "raspberry-pi-3" as the only Pi entry.  ComponentRegistry
even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3
custom-element tag — a placeholder that meant both boards rendered as
a Pi 3 in the picker thumbnail and on the canvas.

Add dedicated boards top-to-bottom:

  * `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style
    schematic SVG (authored from scratch, not traced).  Pi 4 is the
    green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI;
    Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge,
    2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power
    button.  Both carry a small "velxio" mark in the corner.

  * `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that
    returns the 40-pin BCM layout.  Every Pi from the 1B+ onwards
    uses the same physical pin positions and same BCM GPIO
    assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this
    helper and example wires drawn against one model transfer to
    the others without re-routing.

  * React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the
    custom elements at absolute positions (mirrors how
    RaspberryPi3.tsx handles the Pi 3 illustration).

  * Wire-up across the editor surface:
      - BoardOnCanvas: BOARD_SIZE entry + switch case.
      - BoardPickerModal: description, icon, kinds list.
      - ComponentPickerModal: thumbnails now instantiate the dedicated
        custom element (was velxio-raspberry-pi-3 fallback).
      - SerialMonitor / EditorToolbar: pill labels, icons, colours.
      - Oscilloscope: GPIO channel list (28 BCM pins).
      - SimulatorCanvas: remote-boards filter for run/stop sync.
      - SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3.
      - boardPinToNumber: accepts physical pin numbers ("1"-"40"),
        BCM names ("GPIO14") and power labels for any Pi 3/4/5 id.
      - ComponentRegistry: dedicated tagNames + per-board thumbnails
        (green for Pi 4, darker green for Pi 5).

  * EditorToolbar's Pi 3 special cases (Linux/Python compile path,
    Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5
    inherit the same behaviour automatically, and any future Pi
    family member (Zero / 1 / 2) lands in the right code paths the
    moment its backend boots.

QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry
'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes
both boards selectable end-to-end without any backend follow-up.
2026-05-23 04:46:59 +02:00
David Montero Crespo 4ff71765e7
Merge pull request #202 from davidmonterocrespo24/fix/circuit-sim-service-stop
fix(circuit-sim): stop() guard + drop edges with no V-source post-reb…
2026-05-19 22:52:08 -03:00
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 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 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 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 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
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
David Montero Crespo 305170aeb9 feat(components): Regulated Power Supply with per-instance current limit
Adds a new picker entry 'Regulated Power Supply' under the analog
category. Conceptually fills the gap between wokwi-battery (fixed
DC) and wokwi-signal-generator (waveform focus): user chooses
voltage + mode (dc / ac) + currentLimit, no need to think about
battery chemistry or signal amplitudes.

Properties:
  mode:         'dc' | 'ac'   (default 'dc')
  voltage:      V             (default 5)
  frequency:    Hz            (default 50, only for AC)
  currentLimit: A             (default 1)

Design notes:
  - No new Web Component. The tagName piggy-backs on
    wokwi-signal-generator so the canvas renders the familiar
    bench-instrument chrome — saves shipping a second 100+ LOC
    Web Component for an identical 2-pin shape.
  - SPICE: ideal V-source + ESR sized so a near-short reads
    I ≈ 1.5·limit. ngspice has no native foldback so the limit
    is a circuitVerifier rule, not a hard SPICE constraint.
  - circuitVerifier: extends sourceComponents regex to include
    power-supply AND honors the per-instance currentLimit
    property as the threshold. Real bench supplies behave this
    way — a 100mA-limited supply trips at 100mA, a 5A supply
    tolerates 5A before flagging. The error code is
    'source-overload' (not 'short-circuit') so the modal copy
    matches what the user just configured.

The board GND / VCC pins of Arduino / ESP32 / etc. already act
as voltage sources via BOARD_PIN_GROUPS canonicalisation (the
NetlistBuilder maps wires to the right rail). So the user's
companion request — 'board pins should already work' — is the
existing behaviour; this commit only adds the standalone bench
supply for boardless circuits or for testing with a different
voltage.
2026-05-18 00:00:45 -03:00
davidmonterocrespo24 37f35488c7 feat(sim): Phase 1d #10 + #11 + #16 — observable + perf + UX touch-ups
#10 — ESP32 ADC clipping warning: `pushEsp32Waveforms` now counts
how many samples land outside the 0-3.3 V ADC range.  If > 10% of a
pin's waveform clips, console.warn once per pin with the observed
range.  Helps diagnose "my analog read is stuck at 4095" from
canvases without a divider / clamp.

#11 — PinManager subscriptions scoped to circuit pins.  Previously
`connectMcuEdgesToService.subscribeBoard` attached listeners to all
64 Arduino pins per board, justified as "free if unused".  True
for AVR; spammy for ESP32 with 40+ GPIOs × multi-board setups
(thousands of dead listeners).  Now reads from useElectricalStore's
pinNetMap and only subscribes to pins the circuit references.
Re-subscribes when pinNetMap changes (new wire added/removed).

#16 — `__spiceDebug()` window helper.  Restored after the legacy
subscribeToStore deletion in Phase 1c.  Logs analysis mode,
voltage count, pin-net-map sample, last-solve ms — useful for
DevTools investigation of "why isn't my circuit solving?" reports.

1461 tests pass.

#8 (FQP27P06 → VDMOS) deferred — model not in the local LTSpice
library; requires external sourcing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:34:29 +02:00
davidmonterocrespo24 6c6dea3326 feat(sim): Phase 1d #6 — listCurrentVectors in Worker adapter, no more heuristic parsing
`runNetlist` was guessing what vectors to read by regex-matching
`V*/R*/L*/C*/D*/Q*/M*` lines in the netlist string.  Fragile —
missed extra-card nets, custom prefixes, subckt-internal nets.

This commit gives the Worker adapter the same enumeration surface
the Node adapter already had:

  • New `listVectors` message type in the worker, calling
    `ngSpice_AllVecs(curPlot)` and decoding the NULL-terminated
    char** result.  Case-preserved (getVecInfo lookups are
    case-sensitive for source-current vectors).
  • `NgSpiceInteractive.listVectors()` exposes it to the adapter.
  • `NgSpiceWorkerAdapter.listCurrentVectors()` + the higher-level
    `readAllCurrentVectors()` — single-call enumerate + read.
  • `runNetlist.ts` simplified: ONE solve, then read every vector
    via the adapter.  No more regex parsing.  No more guess-set.

`readAllCurrentVectors` exists on both adapters now with identical
shape — domain code can swap them freely.

1461 tests pass.  Both `examples-gallery-smoke` (68 examples) and
`circuit-verifier` (8 pre-flight checks) green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:31:08 +02:00
davidmonterocrespo24 29d76348aa feat(sim): Phase 1d #3 + #5 — WASM pre-boot on mount + delete dead wire* utils
#3: `start.ts` now kicks `scheduler.start()` (lazy-boot the WASM
engine) right when the editor mounts.  Without this, the first
solve — typically the user's first canvas edit — paid 2-5 s of
WASM init while the canvas appeared frozen.  Now the Worker boots
while the user looks at the empty canvas; by the time they wire
anything, the engine is warm.

#5: deleted three unimported dead files that pre-existing tsc -b
strict errors referenced.  Nothing in the live codebase imports
`wireOffsetCalculator`, `wirePathGenerator`, or `wireSegments` —
they were left behind by an earlier wire-routing refactor.
Removing them clears 10+ tsc errors plus the `WireControlPoint`
phantom type they relied on.

Also cleaned up an unused import in
`capacitor-charge-transient.test.ts` (leftover from F2).

1461 tests pass, vite build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:24:31 +02:00
davidmonterocrespo24 54936ef660 feat(sim): Phase 1d #2 + #9 — convergence helpers in Worker + enable LM358 subckt
#2: NgSpiceWorkerAdapter.init() now sets the same convergence
options the Node adapter has — `option gmin=1e-10 gminsteps=20
sourcesteps=10 method=gear maxord=2`.  Production and tests run
with identical solver tolerances; circuits that converged in tests
no longer hit "No vectors" in the browser.  Also added `remcirc`
before loadNetlist so leftover state doesn't bleed across canvases.

#9: opamp-lm358 in componentToSpice now emits the real LM358 macro-
model subckt (`X_id IN+ IN- vcc_rail 0 OUT LM358`) instead of the
behavioural B-source clamp.  The subckt was vendored as an asset in
Phase 2.2 and has been waiting for #2 to land — now active.

Smoke-test side effect: 67/68 → 68/68 examples converge.  The opamp
follower (`an-opamp-follower`) was the last one that didn't.

exampleToBuildNetlistInput now delegates to `buildInputFromStore` —
same analysis-picking logic production uses.  A signal-generator
circuit gets `.tran`, an MCU-driven RC step gets `.tran` with the
right τ window, plain DC gets `.op`.  No more inline analysis guess.

examples-analog.test.ts regex extended to allow X-prefix cards so
the LM358 subckt instance line counts as "one of the SPICE cards
for this component".

1461 tests pass across 105 files (28 pre-existing skips, none
introduced by this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:17:06 +02:00
davidmonterocrespo24 f9e5c19f95 feat(sim): Phase 1c G+F3 — retire legacy CircuitScheduler / eecircuit-engine
The mixed-mode migration's endgame.  After this commit there is ONE
SPICE solver path in the codebase — the vendored ngspice WASM via
SolverPort, behind both NgSpiceWorkerAdapter (production browser) and
NgSpiceNodeAdapter (Vitest Node).  Zero hybrids; zero legacy left to
maintain.

Deleted production files:
  • simulation/spice/CircuitScheduler.ts        (200ms-poll legacy)
  • simulation/spice/SpiceEngine.ts             (eecircuit-engine wrap)
  • simulation/spice/SpiceEngine.lazy.ts        (lazy code-split)
  • simulation/spice/subscribeToStore.ts        (legacy solve loop)
  • simulation/spice/connectLegacySolverToMixedMode.ts  (bridge)
  • simulation/spice/connectMixedModeSchedulerToStore.ts (feature flag)

Deleted tests (no longer cover any live code):
  • connect-legacy-solver-to-mixed-mode.test.ts
  • connect-mixed-mode-scheduler-to-store.test.ts
  • spice-rectifier-live-bootstrap.test.ts

Migrated 6 tests off the deleted `circuitScheduler.solveNow` API to
the new `__tests__/helpers/solveInput.ts` (same shape, backed by
NgSpiceNodeAdapter).

`useElectricalStore` rewritten as a pure state container:
  • setSolveResult(snapshot)  — atomic publish from the service
  • paused / setPaused        — UI control unchanged
  • reset                     — project unload
  • REMOVED: triggerSolve, solveNow, setDebounceMs, scheduler hook
  • REMOVED: dependency on SpiceEngine.lazy preload

EditorPage now mounts a single `startSimulation()` from
`simulation/spice/start.ts`, which constructs
CircuitSimulationService + ADC bridge + MCU edge bridge.  Four
useEffect calls collapsed to one.

`circuitVerifier.ts` (production) and `runNetlist.ts` use an
environment-aware factory: Web Worker in browser, in-proc WASM in
Node tests.  `/* @vite-ignore */` keeps the Node adapter chain
(node:fs, node:url) out of the browser bundle while still letting
Node resolve it dynamically.

Removed `eecircuit-engine` from package.json dependencies.

`collectPinStates` extracted to its own module so the service doesn't
depend on the (now deleted) subscribeToStore.ts.

Verification:
  • 1392/1392 tests pass across 103 files (28 pre-existing skips).
  • `tsc --noEmit` clean.
  • `vite build` succeeds (27 s, only the existing chunk-size
    warning that pre-dates this work).

Phase 1c — COMPLETE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:46:34 +02:00
davidmonterocrespo24 848786dd16 feat(sim): Phase 1c G prep — production wiring file (start.ts)
Single-call mount for the new mixed-mode loop:
  • CircuitSimulationService (orchestrator)
  • connectAnalogInputsToMcu (ADC bridge)
  • connectMcuEdgesToService (pin event subscriptions)

References useElectricalStore.setSolveResult (to be added in the
same step that retires triggerSolve / CircuitScheduler).  Not
activated in EditorPage yet — six existing tests still consume the
legacy `solveNow` / `triggerSolve` API and need to migrate to
CircuitSimulationService.tick() first.

Holding G activation until the test migration lands so we don't
strand the legacy `solveNow` callers in mid-air.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:25:57 +02:00
davidmonterocrespo24 194ff94045 feat(sim): Phase 1c F2 — migrate 22 SPICE test files to NgSpiceNodeAdapter
The test suite now runs against the SAME ngspice WASM that
production uses — closing the "no hybrid" gap.  Every test file
that used to import `runNetlist` from `SpiceEngine.ts`
(eecircuit-engine) now imports from a compatibility shim
`__tests__/helpers/testSolver.ts` that uses the new
NgSpiceNodeAdapter under the hood.

Migrated (all 22 files): spice-{smoke,active,passive,transient,ac,
digital,avr-mixed,mosfet-pwm,mosfet-diag,npn-switch-diag,
npn-switch-integration,relay-integration,relaxation-oscillator,
signal-generator-tran,rectifier-live-repro}.test.ts plus
component-to-spice, examples-analog-live, examples-digital,
instruments, netlist-builder, phase-4-wire-resistance,
mixed-mode-bjt-switch-integration.

Helper translates between ngspice's raw vector names ('n0',
'<src>#branch', 'frequency', 'time') and the legacy SpiceResult
convention ('v(n0)', 'i(<src>)', special axes).  Re-exports the
`NL` source-card helpers (pulse, sin, pwl, dc, ac) so existing
tests don't touch their builder code.

Adapter additions for the migration:
- listCurrentVectors() — case-preserved enumeration via
  ngSpice_AllVecs (getVecInfo lookup is case-sensitive).
- readAllCurrentVectors() — single-solve read of every vector;
  re-running the analysis would create a new plot and invalidate
  pointers.
- Complex-vector handling: interleaved [re,im,re,im,...] doubles
  in compDataPtr, separate from real-only vectors.
- Convergence helpers: `option gmin=1e-10 gminsteps=20 method=gear
  maxord=2` set on init so op-amp + diode circuits bias correctly
  without each user netlist needing its own `.option`.
- loadCircuit strips inline `.op` / `.tran` / `.ac` directives
  before source, so the SolverPort owns analysis timing (running
  it twice via source + explicit command leaves the second pass
  with an empty plot).
- loadCircuit issues `remcirc` before source so leftover state
  doesn't bleed between tests sharing the singleton adapter.

`circuitVerifier.ts` (production) migrated to the new
`simulation/spice/runNetlist.ts` (Worker-adapter-backed) so the
last consumer of SpiceEngine.ts can be retired in F3.

One test skipped with documentation: `an-opamp-follower` (.op)
fails to converge on the new engine — known issue for B-source
clamps; the LM358 subckt path also has this problem.  Slot in
Phase 1c E1 (convergence helpers / .options tuning) to fix.

233/233 migrated tests pass against real ngspice via the Node
adapter.

Next: F3 — delete SpiceEngine.ts + SpiceEngine.lazy.ts + the
eecircuit-engine dependency from package.json.  Requires G first
(retire CircuitScheduler) because CircuitScheduler still imports
from SpiceEngine.lazy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:23:53 +02:00
davidmonterocrespo24 8db973675a feat(sim): Phase 1c F1 — NgSpiceNodeAdapter runs real WASM in Node tests
Loads the vendored ngspice-interactive WASM directly in the Vitest
Node process, no Web Worker required.  Implements the same SolverPort
contract as NgSpiceWorkerAdapter, so production code and tests share
ONE solver — closing the "no hybrid" gap.

loadNgSpiceForNode (Node-only loader):
- Reads ngspice-lib.js as text, wraps with a hoisted
  `var Module = config` so the emscripten singleton picks up our
  locateFile + callbacks.
- Re-wires Module.onRuntimeInitialized to copy closure-local FS /
  HEAP* into Module._velxio_* (the vendored build doesn't export
  them via EXPORTED_RUNTIME_METHODS so direct Module.FS triggers an
  abort accessor).

NgSpiceNodeAdapter:
- bindApi (cwrap), registerCallbacks (no-op via addFunction),
  stageFilesystem (recursive mkdir + writeFile of model .cm + spinit),
  initialiseNgspice (null callback pointers; the build still solves
  fine without print/data hooks).
- loadCircuit writes the netlist to /circuit.spc on the FS and
  issues `source /circuit.spc` — sidesteps `_malloc` (not exported
  by this build) that the obvious ngSpice_Circ path would need.
- solve() dispatches op/tran/ac, reads requested vectors via
  ngGet_Vec_Info using the actual struct offsets verified against
  the live build dump: flags=8, realdata=12, imagdata=16, length=20.
- alterSource issues `alter` for incremental re-solves.

5/5 SolverPort contract tests pass against real ngspice:
- init idempotent
- DC op solves a 100Ω/100Ω divider → V(mid) = 2.5 V exactly
- omits requested vectors that don't exist
- alterSource changes V1 → V(mid) tracks the new voltage
- transient RC charge (τ=1ms) reaches >4.5V after 5τ

Next: F2 — migrate the ~22 test files that use eecircuit-engine via
`runNetlist` to this adapter. After F2, F3 deletes eecircuit-engine
and `SpiceEngine.ts` for good.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:49:39 +02:00
davidmonterocrespo24 5d64654668 feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish
CircuitSimulationService.handleMcuEdge(boardId, pinName, state, vcc)
runs the WASM alter + .op + extract path instead of rebuilding the
netlist. Cached `loadedContext` lets `publishFromLastResult` shape an
ElectricalSnapshot without re-running buildInputFromStore.

Coalesces with the canvas-change tick:
- If a full solve is in flight: edge is queued and replayed after
  (so the netlist matches when alter runs).
- Last-edge-wins per pin: edges overwrite the same field, so a
  10kHz toggle collapses to whatever was last seen at flush time.

connectMcuEdgesToService.ts wires PinManager.onPinChange events to
the service:
- Subscribes to every Arduino-pin slot (0..63) per board.  Per-pin
  listeners are no-cost when the pin never fires.
- Coalesces edges per pin in a 16 ms window before calling
  handleMcuEdge (60 fps cap, well below per-solve cost of 5-15 ms).
- Re-subscribes when boards change (PinManager instances are
  recreated by loadHex / setActiveBoard).

MixedModeSchedulerPort gains onMcuPinChange in the port interface
(was already on the singleton but missing from the contract).

3 new service tests cover:
- initial full solve + alter + republish on edge
- coalescing edges with in-flight full solves
- handleMcuEdge kicks a full tick when no circuit is loaded

11 service tests + 90-test regression suite pass. tsc clean.

Next: E — convergence helpers (.options gmin, op-amp retry) so the
LM358 subckt can finally be enabled in componentToSpice.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:26:09 +02:00
davidmonterocrespo24 5ce99fab5d feat(sim): Phase 1c C1+C2 — extract ADC/waveform bridge to solver-agnostic module
connectAnalogInputsToMcu.ts is now the single owner of:
  • DC scalar ADC injection (setAdcVoltage)
  • AC waveform-time per-read sampling (patched onADCRead)
  • ESP32 QEMU waveform push (setAdcWaveform)

The module subscribes to `useElectricalStore` regardless of who
populated it (legacy CircuitScheduler today, CircuitSimulationService
tomorrow).  Replacing the solver path no longer touches ADC logic.

subscribeToStore.ts cut from 591 to 161 lines.  Its remaining
responsibility: the legacy solve loop (subscribe to canvas changes,
200 ms running-timer, push to `useElectricalStore.triggerSolve`).
That whole file disappears in step G1 once the service is the
default; today it stays so the legacy path keeps working alongside
the new architecture.

EditorPage mounts the four subscribers explicitly:
  1. wireElectricalSolver — legacy solve loop
  2. connectLegacySolverToMixedMode — bridge to scheduler cache
  3. connectAnalogInputsToMcu — ADC + waveform replay (NEW)
  4. connectMixedModeSchedulerToStore — flagged WASM path

Pre-existing flaky test in spice-rectifier-live-repro.test.ts
(asserted "wireElectricalSolver queues NO RAF") removed.  It tested
implementation details of an installation path that no longer
exists; end-to-end ADC behaviour is covered by
circuit-simulation-service.test.ts and the BJT-switch integration
test.  Per the migration rule "tests only for real velxio code", a
pre-existing flake testing legacy installation paths is not real
coverage.

Next: D1+D2 — MCU pin event subscriptions so MCU edges drive
scheduler.alterSource + re-resolve, with throttling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:20:49 +02:00
davidmonterocrespo24 a8cd5dd8ce feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator)
The service is the single owner of the simulation loop.  Replaces
the trio of wireElectricalSolver + connectLegacySolverToMixedMode +
connectMixedModeSchedulerToStore once G* lands.

Architecture:
- Depends on PORTS only — SimulatorStorePort, ElectricalStorePort,
  MixedModeSchedulerPort.  Zero coupling to useSimulatorStore /
  useElectricalStore / WASM.  Easy to test with fakes (and that's
  what circuit-simulation-service.test.ts does).
- Single tick(): build netlist → load → solve → extract → publish.
  Coalesces concurrent triggers so rapid store changes collapse to
  one trailing solve.
- Domain ElectricalSnapshot type covers nodeVoltages + branchCurrents
  + pinNetMap + timeWaveforms + analysisMode + warnings.  Shape
  matches what the 12 existing useElectricalStore consumers read.

NetlistBuilder extension: BuildNetlistResult now reports `nets`
(every non-ground SPICE net) and `voltageSources` (every V card the
builder emitted).  The service uses these to construct the full
vectorsOfInterest list — every node voltage + every branch current
— so the solver returns the data the legacy consumers want.

Scheduler addition: `setExtraVectorsOfInterest(vectors)` lets the
orchestrator add to the per-pin set.  Branch currents (i(v_*))
flow through this hook.

8 service tests cover initial solve, branch current extraction,
re-solve on store change, no-spurious-solve, coalescing, .tran
waveforms, warnings forwarding, error-tolerance.

Next: C1+C2 — extract ADC injection / waveform replay into a
solver-agnostic module that just subscribes to useElectricalStore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:28:26 +02:00
davidmonterocrespo24 d048a7d031 feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort
MixedModeScheduler now accepts any SolverPort implementation via
solverFactory injection.  The ad-hoc `NgSpiceClient` interface is
gone; the scheduler talks domain port types only.

New capabilities that fell out of the refactor:
- `resolveTran(step, stop)` — runs .tran via the solver and publishes
  the steady-state (last-sample) voltage per pin. Full waveform
  reachable via `getLastResult()` for downstream consumers
  (CircuitSimulationService in B1+ will use this to populate
  useElectricalStore.timeWaveforms).
- `getLastResult()` exposes the SolveResult so the upcoming service
  layer can extract branchCurrents + waveforms without re-reading.
- `vectorsOfInterest` is computed from pinNetMap on every solve, so
  the adapter only issues N parallel readVecs (where N = distinct
  non-ground nets) instead of guessing.

`__setSchedulerEngineFactoryForTests` renamed to
`__setSchedulerSolverFactoryForTests`.

Tests fully migrated to FakeSolverAdapter — no more inline mock
NgSpiceClient.  Test layering now mirrors production: scheduler tests
exercise port consumption, port-contract tests exercise the port
itself.

60 tests pass across mixed-mode-scheduler, solver-port-contract,
mixed-mode-bjt-switch-integration (real ngspice), pin-resolver,
pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store,
connect-legacy-solver-to-mixed-mode.  tsc clean.

Next: B1 — CircuitSimulationService, the layer above the scheduler
that builds netlists, picks .op vs .tran, and publishes results to
both useElectricalStore and the scheduler cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:24:12 +02:00
davidmonterocrespo24 834f8f7e0a feat(sim): Phase 1c A2+A3 — NgSpiceWorkerAdapter + FakeSolverAdapter
Two SolverPort adapters land in this commit:

- NgSpiceWorkerAdapter — production. Wraps the vendored
  NgSpiceInteractive client. Translates SolverPort calls into worker
  messages. Parallel readVec for every vectorOfInterest after each
  solve. .tran also reads the `time` vector for the axis.
- FakeSolverAdapter — in-memory test double. Records every call,
  returns canned vectors via static map or dynamic supplier. Optional
  solveDelayMs for race-condition tests.

Port surface refined: solve(analysis, options) now takes
SolveOptions.vectorsOfInterest so the adapter can parallelise reads
instead of guessing what the caller cares about.

This bundles A3 (resolveTran) into A2 because the same Solve API
handles every analysis kind — the adapter dispatches on
analysis.kind to build the right ngspice command (`op`, `tran <step>
<stop>`, `ac <sweep> <points> <fstart> <fstop>`).

11 SolverPort contract tests pass. When NgSpiceNodeAdapter lands in
F1, it will run the same contract suite verbatim to confirm it
honours the port identically.

Next: A4 — refactor MixedModeScheduler to depend on SolverPort
instead of the ad-hoc NgSpiceClient interface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:21:46 +02:00
davidmonterocrespo24 e8537eec6f feat(sim): Phase 1c A1 — define SolverPort (hexagonal port)
First commit of the full migration to a single WASM-driven solver.
Defines the abstract contract that domain code (MixedModeScheduler,
CircuitSimulationService) will depend on. Adapters in ./adapters/
implement the port against concrete engines.

Surface kept narrow:
- init / loadCircuit / solve / alterSource / dispose
- SolveAnalysis: op | tran | ac
- SolveResult: vectors map + timeAxis + solveMs + warnings

Domain types live in the port file (SolveVector, SolveResult) so the
port has no upward dependency on ../types.ts. Adapters bridge between
domain types and engine-specific shapes.

Next: A2 — implement NgSpiceWorkerAdapter on top of NgSpiceInteractive.
Then A3 (resolveTran), A4 (scheduler refactor), A5 (fake + tests).
See velxio-prod/project/sim-mixedmode/phase-1c-migration-plan.md for
the full sub-step roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:19:05 +02:00
davidmonterocrespo24 173037b593 feat(sim): Phase 1c step 1 — feature-flagged WASM-driven connector
Adds `connectMixedModeSchedulerToStore` — when enabled, it subscribes
to the simulator store and drives the MixedModeScheduler's WASM path
(`loadCircuit` + `resolveDc`) directly, parallel to the legacy
`wireElectricalSolver` + `connectLegacySolverToMixedMode` bridge.

Opt-in mechanisms (two ways, either works):
- URL query: `?mixedmode=on`
- Persistent: `localStorage.velxio.mixedmode = 'on'`

When the flag is off (default), behaviour is identical to before.
When on, both connectors publish voltages into the scheduler cache;
last write wins.  This is deliberate during the A/B test — the two
paths can be compared by toggling the flag and watching the same
canvas behave identically (or surfacing divergence as a real bug).

The connector coalesces solves: if one is in flight, the next store
change marks a pending re-solve that fires once the first finishes,
collapsing N rapid changes into 1 trailing solve.  Errors are logged
but don't propagate — the legacy solver is still running, so a WASM
convergence failure shouldn't kill the editor.

`collectPinStates` is now exported from `subscribeToStore.ts` so the
new connector reuses the same per-board pin-number mapping.

10 unit tests cover initial solve, re-solve on changes, coalescing
under load, error tolerance, unsubscribe cleanup, and the feature-
flag predicate (URL + localStorage paths).  jsdom env scoped to this
file via `// @vitest-environment jsdom`.

Phase 1c step 1 of N: this is the plumbing that lets us validate the
WASM path in production without flipping the default.  Step 2 would
add MCU pin-event subscriptions so MCU edges trigger re-solves
(currently only canvas changes do).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:53:36 +02:00
davidmonterocrespo24 340323c1d3 feat(sim): Phase 4 — opt-in wire resistance (length_cm)
Wires can now carry a `length_cm` property. When set, the NetlistBuilder
treats them as a real resistor (0.01 ohm/cm ≈ AWG 22 copper) instead of
the legacy perfect-conductor union. Wires without `length_cm` are
unchanged — 100% backwards compatible until the UI starts attaching
length values based on canvas geometry.

Implementation:
- `WireForSpice.length_cm?: number` added to types
- Union-Find pass skips `union(a, b)` when length_cm > 0, so endpoints
  end up in separate nets
- After component-card emission, scan `resistiveWires` and emit
  `R_wire_<id> <netA> <netB> <ohms>` for each
- Pull-down detection runs after so the wire R counts as a DC path

Verified end-to-end with real ngspice:
- 100/100 divider at 5V → vmid = 2.5V (legacy, no wire R)
- Same with 1 cm supply wire → vmid = 2.4999 V (0.25 mV drop)
- Same with 500 cm supply wire → vmid ≈ 2.439 V (~6% drop)

5 new Phase 4 tests + 208 regression tests pass.

This is the plumbing-first deliverable from the original sim-mixedmode
plan — UI work (compute length from canvas waypoints) is a separate
front-end task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:31:28 +02:00
davidmonterocrespo24 46aa16bfc2 feat(sim): Phase 2.2 — vendor LM358 macro-model subckt (asset only)
The full LM358 SPICE3 subcircuit from National Semiconductor (via
stmbl) is now exported as LM358_SUBCKT from
simulation/spice/models/lm358Subckt.ts.  Internal models renamed
DX→DX_LM358 and QX→QX_LM358 so the subckt coexists cleanly with any
other vendored library.

Integration into opamp-lm358 was attempted and reverted — the
subckt's internal capacitors/inductors/poly sources cause ngspice
`.op` to time out (>60 s) on a simple unity-gain follower.  The
behavioural B-source clamp remains the active model.  When Phase 1c
moves the default analysis to `.tran` (or we add `.options gmin=1e-10`
selectively for op-amp-containing netlists), the subckt is sitting
right next door waiting to be wired in.

Phase 2.2 lockdown test guards the asset:
- declares `.SUBCKT LM358 1 2 99 50 28` interface (IN+ IN- V+ V- OUT)
- ensures internal model names are LM358-scoped (not the bare DX/QX
  that collide with other SPICE libraries)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:28:46 +02:00
davidmonterocrespo24 8f49665cdf feat(sim): include component pins in NetlistBuilder.pinNetMap + e2e BJT-switch test
Fixes the gap that Phase 1b step 4 surfaced: the legacy pinNetMap was
built from board endpoints only, so the bridge from legacy solver to
MixedModeScheduler had nothing to publish for component pins like
"q1:C" — every SpiceResolvedPinResolver was stuck on FLOATING.

Now pinNetMap contains an entry for every wire endpoint, board or
component. Backwards compatible: legacy ADC injection only ever looked
up `boardId:pinName` keys, which are unchanged.

The new e2e integration test wires up real ngspice (eecircuit-engine,
no mock):
  Arduino pin 9 → 1k → 2N2222 base; collector via 220 to 5V
  - pin 9 HIGH → BJT saturated → Vc ≈ 0.05V → resolver emits LOW
  - pin 9 LOW  → BJT cut off    → Vc ≈ 5V    → resolver emits HIGH

Validated against the AVR_HC logic family (Phase 3). With 216 tests
green across 25 files, the Phase 1b pipeline is now demonstrably
correct end-to-end against a real SPICE solver, not just mocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:22:36 +02:00
davidmonterocrespo24 da07345bc0 feat(sim): Phase 1b continued, step 4 — bridge legacy solver into MixedModeScheduler
Connects the existing electrical solver's output (nodeVoltages +
pinNetMap from useElectricalStore) to the mixed-mode scheduler's
voltage cache.  SpiceResolvedPinResolver subscribers now actually see
live voltages — they were stuck on FLOATING until this commit.

Design:
- `connectLegacySolverToMixedMode()` subscribes to useElectricalStore.
  On every nodeVoltages / pinNetMap change it walks pinNetMap and
  calls scheduler.publishVoltage(componentId, pinName, v) for each
  pin.  Ground pins (canonical net '0') resolve to 0 V directly.
  NaN / Infinity voltages are skipped.
- `connectLegacySolverToMixedModeFor(store, scheduler)` is the
  lower-level form used by tests so neither Zustand nor the WASM
  scheduler need to boot.
- EditorPage mounts both `wireElectricalSolver` (legacy ADC path) and
  `connectLegacySolverToMixedMode` (new SPICE-resolved path) in the
  same useEffect — they coexist; the connector only routes events,
  so no behaviour regresses for components that don't opt into
  SpiceResolvedPinResolver.

7 new unit tests cover initial publish, re-publish on store change,
ground-pin shortcut, NaN filtering, and unsubscribe cleanup.

This is the wiring that completes Phase 1b's end-to-end pipe.  The
WASM-driven onMcuPinChange path (loadCircuit + alter + tran in the
scheduler itself) stays available for future migration off the legacy
solver entirely — see Phase 1b doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:15:24 +02:00
davidmonterocrespo24 61b04e46f8 feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange
Wires the second half of the mixed-mode event loop on top of the
voltage cache that step 1 added.

Step 2 — loadCircuit + resolveDc:
- `loadCircuit(netlist, pinNetMap)` accepts the artifacts that
  NetlistBuilder already produces, boots the engine lazily, calls
  `loadNetlist`, and clears the voltage cache so stale values from a
  previous circuit cannot leak through.
- `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for
  each non-ground net and publishVoltage for each pin. Ground pins
  short-circuit to 0 V without an extra round-trip. Missing nets are
  skipped quietly so a disconnected probe pin can't break the resolve.

Step 3 — onMcuPinChange:
- Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller
  decides the volts: `state ? vcc : 0` for plain digital, but boards
  with open-drain / output-impedance semantics can pass any number.
- Silent no-op when no engine has been started, so legacy paths that
  fire pinChange unconditionally can't crash the simulator.

NgSpiceClient interface added and exported so unit tests can inject a
fake engine that records alter() calls and returns canned readVec
values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the
load → resolve → alter → republish loop end-to-end without booting
the real WASM worker.

The orchestration layer (Zustand subscriber / DynamicComponent hook)
that calls `loadCircuit` whenever the canvas changes is the next step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:11:10 +02:00
davidmonterocrespo24 1ab294cf10 feat(sim): Phase 1b continued, step 1 — scheduler voltage cache + subscriber routing
Adds the runtime plumbing that Phase 1b's SPICE event loop will drive:
- `publishVoltage(componentId, pin, voltage)` updates a (componentId,
  pin) → volts cache and notifies every matching subscriber.
- `getCurrentVoltage(...)` reads the cache (was previously stubbed
  null).
- subscribe/publish routing exercised by 7 new unit tests.

The scheduler still does not yet drive ngspice — `start()`,
`onMcuPinChange()` are unchanged. But once Phase 1b's solve loop is in
place, calling `publishVoltage` after each `readVec` is all the wiring
needed for components to start reacting to SPICE-resolved analog
states. This is the smallest non-trivial step that keeps the
architecture honest (no test-only emitters; the same code path will be
used in production).

Tests skip booting the WASM worker — they call publishVoltage
directly, so they pass in plain Vitest with no JSDOM Worker shim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:06:20 +02:00
davidmonterocrespo24 d9945d13b8 feat(sim): Phase 2.1 — migrate MOSFETs to VDMOS macro-models
Switches 3 of the 4 simulated MOSFETs from Level=1 Shichman-Hodges
to LTSpice VDMOS macro-models. VDMOS captures real-device behaviour
(Ron, gate charge Qg, gate-drain Miller capacitance Cgdmax/Cgdmin,
body diode) that Level=1 fundamentally can't model.

Instance line changes from
  M_id D G S S MODEL L=2u W=200u            (4-terminal NMOS + W/L)
to
  M_id D G S MODEL                          (3-terminal VDMOS)

Parts migrated:
  mosfet-2n7000  → 2N7002 VDMOS (Vto=1.6, Ron=2 ohm — matches old Vto)
  mosfet-irf540  → IRF530 VDMOS (Vto=4, Ron=160m — IRF540 missing
                                  from LTSpice library, IRF530 is the
                                  closest same-series part)
  mosfet-irf9540 → IRF9640 VDMOS (pchan, Vto=-3.5 — IRF9540 missing,
                                  IRF9640 is the 200V P-channel sub)

mosfet-fqp27p06 kept on Level=1 (no upstream VDMOS equivalent yet).

spice-mosfet-pwm regression test still passes: Id=8.6 mA at Vgs=5V,
0 at Vgs=0V, monotonic across the ramp. All 155 SPICE + analog
examples + lockdown tests pass.

Phase 2.1 lockdown test added — verifies VDMOS-shape instance line
(5 tokens, no L=/W=) and that the .model card carries `VDMOS(`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:03:49 +02:00
davidmonterocrespo24 c476084e00 feat(sim): Phase 2 — upgrade BJT/diode models to LTSpice Gummel-Poon (SPICE3F5)
Replaces the truncated 4-5 param NPN/PNP/D models in componentToSpice.ts
with full Gummel-Poon / SPICE3F5 parameter sets sourced from the
LTSpice-Libraries (Linear Tech standard.bjt and standard.dio). Junction
capacitances, transit times, and reverse-recovery now match real-device
behaviour — circuits using these parts will now exhibit correct AC and
switching response on top of DC saturation.

Parts upgraded:
  BJT NPN: 2N2222, BC547, 2N3055
  BJT PNP: 2N3906, BC557
  Diode:   1N4148 (silicon switching), 1N5817, 1N5819 (Schottky)

MOSFET (Level=1) and 1N4007/zener kept as-is - they need separate
VDMOS migration validated against the MOSFET PWM regression test.

Phase 2.0 of the mixed-mode simulator project. See
velxio-prod/project/sim-mixedmode/phase-02-device-models.md.

All 115 SPICE tests pass; relay-integration test confirms the netlist
dedupe set still collapses two D1N4148 references (canonical diode +
relay flyback) into a single .model line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:57:36 +02:00
davidmonterocrespo24 27c59664cd feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection
Adds the architecture pieces for mixed-mode coupling without yet
driving the SPICE engine.  Components on a path that crosses an active
device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route
through a new SPICE-resolved PinResolver variant; everything else
keeps the digital fast-path from Phase 0.

What ships:

  - simulation/PinResolver.ts
    * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES`
      list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay).
    * `DetailedPinTrace` / `DetailedPinTracer` types — the trace
      function now reports whether it crossed an active device, on
      top of the Arduino pin number.
    * `createSpiceResolvedPinResolver()` — new factory; reads voltages
      from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW
      with hysteresis (thresholdHigh != thresholdLow → Schmitt-like).

  - simulation/spice/MixedModeScheduler.ts (new)
    * Singleton orchestrator that holds the NgSpiceInteractive engine
      and the SpiceVoltageSource subscription registry.
    * `start()` / `stop()` / `dispose()` lifecycle.
    * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource.
    * `onMcuPinChange()` placeholder for the alter+tran event loop.
    * Skeleton: subscribers register but never receive events yet.
      Phase 1b continued will wire NgSpiceInteractive into the loop.

  - components/DynamicComponent.tsx
    * Trace function extended with `traceDetailed()` that tracks
      whether the BFS crossed an active component.
    * PinResolver factory branches: active-path → SPICE-resolved (uses
      the scheduler), digital-only → existing default impl.  Default
      threshold = vcc/2 with no hysteresis; Phase 3 will replace with
      per-logic-family Vil/Vih.

Phase 0 LED behavior intact (digital path).  Phase 1b SPICE-resolved
path falls back to FLOATING until Phase 1b continued wires the engine.

Tests:
  - pin-resolver-phase1b.test.ts (new) — 8/8 passing.
    Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator
    metadata id; SPICE-resolved resolver state reporting, threshold
    conversion, hysteresis dead-band, unsubscribe.
  - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression).
  - tsc --noEmit on the new files: clean.

No deploy in this commit — staged for end-of-session rebuild + push
per user preference.
2026-05-15 16:38:30 +02:00
davidmonterocrespo24 d41be4f48b feat(sim/spice): vendor ngspice+XSpice WASM and add NgSpiceInteractive client (Phase 1a)
Phase 1a of the mixed-mode simulator project.  Vendors prebuilt
ngspice+XSpice WASM artifacts from ejkreboot/ngspice-xspice-wasm (MIT,
2026) and adds a TypeScript client that exposes the ngspice shared
callable API for interactive (event-driven) use.

What's vendored at frontend/public/wasm/ngspice-interactive/ (~27 MB):

  - ngspice-lib.wasm  (24 MB) — ngspice 33 + XSpice, MAIN_MODULE
  - ngspice-lib.js   (2.7 MB) — Emscripten glue
  - {analog,digital,xtradev,xtraevt,table,tlines,spice2poly}.cm
                              — XSpice code models, loaded dynamically
  - spinit                    — ngspice startup script
  - PROVENANCE.md             — sources + license info

Note on the cost: 27 MB is a one-way commit to git history, but the
existing eecircuit-engine dependency already ships 39 MB in node_modules
(not tracked, re-downloaded per build). Vendoring our copy:
  - removes a third-party npm dependency
  - pins the exact build we tested with
  - means the WASM is served as a static asset (no Vite chunking)
The alternative (publish as @velxio/ngspice-interactive-wasm) was
deferred to keep the iteration cycle fast during Phase 1+.

New TypeScript client at frontend/src/simulation/spice/wasm/:

  - NgSpiceInteractive.ts        — Promise-based client class with
                                    init / loadNetlist / command /
                                    alter / readVec / reset / dispose
  - ngspice-interactive-worker.js — vendored from ejkreboot's worker
                                    and extended with 'loadNetlist',
                                    'command', 'readVec' message types
                                    plus per-command stdout/stderr
                                    capture

POC test at __tests__/ngspice-interactive.test.ts (skipped in node env
because Worker isn't available; runs in a browser-mode test env):
  - voltage divider .op → reads v(mid) ≈ 2.5V
  - RC step → reads v(cap) time series, final ≈ 5V
  - alter Vsrc → second .tran → final ≈ 1V (proves alter+rerun works)

Known limitation deferred to Phase 1b: the vendored WASM is built
without pthreads (no -sUSE_PTHREADS=1), so ngspice's bg_run is
synchronous-blocking. True mixed-mode event injection requires a
pthread-enabled rebuild (with SharedArrayBuffer + cross-origin
isolation). For Phase 1a we use the workaround: chained short-tran
invocations with `alter` between them. The new architecture is built
to swap in a real bg_halt/bg_resume implementation later without
changing component handlers — see NgSpiceInteractive.ts docstring.

Tests passing:
  - pin-resolver (Phase 0): 8/8
  - ngspice-interactive: 3 skipped (need browser env)
  - tsc --noEmit on the new files: clean
2026-05-15 16:14:50 +02:00
David Montero Crespo a097601a73 Add HD44780Decoder and various I2C sketches
- Implement HD44780Decoder for decoding I2C commands to HD44780-compatible LCDs.
- Add bmp280_bridge_reader.ino to read BMP280 chip_id and status registers via I2C.
- Create i2c_scanner_multi.ino to scan I2C addresses and report responding devices.
- Introduce lcd_i2c_hello.ino to demonstrate basic LCD functionality with I2C.
- Implement pcf8574_bidirectional.ino to test bidirectional communication with PCF8574.
- Add pico_i2c_master_reader.ino for reading BMP280 from a Raspberry Pi Pico.
- Create rtc_lcd_clock.ino to display time from a DS1307 RTC on an I2C LCD.
2026-05-12 14:26:33 -03:00
David Montero Crespo 692af00258 style: update ESLint rules and clean up code formatting across multiple files 2026-04-21 17:14:27 -03:00
David Montero Crespo 212ecd1bcb refactor: rename components and update prefixes to 'velxio-' for consistency
- Modified the index file to reflect the new naming convention for Velxio components.
- Changed JSX declarations to use 'velxio-' prefix for various components.
- Updated component overrides to replace 'wokwi-' with 'velxio-' for logic gates and other components.
- Adjusted SVG generation script to use 'velxio-' prefix for BMP280 and Raspberry Pi components.
- Marked submodules as dirty in QEMU and RP2040 libraries.
- Added .prettierignore and .prettierrc.json for consistent code formatting.
- Introduced InstrumentComponent with support for Voltmeter and Ammeter, including pin information handling.
2026-04-21 16:45:45 -03:00
David Montero Crespo 993a25390c feat: add passive component presets and custom elements
- Implemented a script to inject passive-component preset variants into `scripts/component-overrides.json`, including resistors, capacitors, and inductors with custom names and thumbnails.
- Added a new custom element `<wokwi-capacitor-electrolytic>` representing a polarized aluminum-can capacitor with appropriate SVG representation.
- Updated metadata generation to accommodate new component names and thumbnails for better user experience in the component picker.
- Marked submodules `qemu-lcgamboa` and `rp2040js` as dirty to reflect local changes.
2026-04-21 15:21:03 -03:00
David Montero Crespo b152cb1919 Refactor property synchronization in simulation parts; introduce emitPropertyChange event
- Replaced syncStoreProperty function with emitPropertyChange to decouple parts from Zustand store.
- Updated relay component mapping to ensure proper handling of coil and contact states.
- Added new test cases for half-wave rectifier and relay-controlled LED to ensure correct functionality.
- Introduced InlineComponentSVGs for schematic-style icons of various components.
- Updated submodule references for qemu-lcgamboa, rp2040js, and wokwi-elements to indicate dirty state.
2026-04-21 10:56:20 -03:00
David Montero Crespo 9cc9cfebd6 Add end-to-end tests for ammeter, voltmeter, and capacitor charging behavior
- Implement `ammeter-waveform.test.ts` to validate AC readings from a sine wave source.
- Create `capacitor-charge-transient.test.ts` to test the charging response of an RC circuit driven by a microcontroller pin.
- Introduce `esp32-rectifier-integration.test.ts` for testing rectifier behavior using QEMU and ESP32.
- Add helper functions in `esp32RectifierE2E.ts` for the rectifier test harness.
- Develop `voltmeter-waveform.test.ts` to ensure correct AC and DC readings from a sine wave source.
- Implement unit tests for waveform statistics in `waveform-stats.test.ts` to validate RMS, mean, peak, and interpolation functions.
- Create `waveformStats.ts` to provide statistical functions for time-domain waveform analysis.
2026-04-21 02:17:30 -03:00
David Montero Crespo 137f9ab0a0 Add tests for serial batching and spice rectifier functionality
- Implement `serial-batching.test.ts` to verify the behavior of `createSerialBatcher`, ensuring it coalesces multiple appends into a single flush, preserves byte order, and groups by board.
- Create `spice-rectifier-integration.test.ts` to test the end-to-end functionality of the Half-Wave Rectifier example, covering the entire simulation pipeline from input building to circuit solving.
- Add `spice-rectifier-live-repro.test.ts` to reproduce a live-app failure scenario, tracing through each layer of the simulation to identify potential failure points.
- Introduce `spice-signal-generator-tran.test.ts` to validate the behavior of the signal generator and ensure correct analysis type switching based on circuit components.
- Establish `serialBatcher.ts` to implement a batching mechanism for USART output, reducing the frequency of store updates and preventing React's maximum update depth error.
2026-04-20 23:56:42 -03:00
David Montero Crespo dcb8a92b79 feat: enhance electrical simulation and testing framework
- Decoupled electrical simulation from the simulator store, ensuring SPICE is always active for accurate circuit analysis.
- Removed feature flag for electrical simulation, simplifying the state management.
- Preloaded SPICE engine at app start to eliminate latency during the first solve.
- Added comprehensive tests for MOSFET PWM LED behavior and NPN transistor switch functionality, ensuring correct current flow and response to pin states.
- Implemented diagnostics for floating input nodes in RC low-pass filter circuits, addressing singular matrix issues in SPICE simulations.
- Introduced active semiconductor metadata registry for better component management and simulation fidelity.
- Updated Vite configuration to force re-bundling of local wokwi-elements after component additions.
2026-04-20 16:38:31 -03:00
David Montero Crespo 61c1ddfc22 feat: add capacitor and inductor components, update netlist builder to return pinNetMap 2026-04-17 19:27:18 -03:00
David Montero Crespo 04d14a74b2 feat: always-on SPICE mode, full board ADC integration, LED brightness from current
Electrical simulation is now active by default (mode='spice' instead of
'off') — users no longer need to toggle the mode on manually. The engine
lazy-loads on first solve, so there is no startup cost penalty.

Changes:
- useElectricalStore: default mode = 'spice' when ELECTRICAL_SIM_ENABLED
- subscribeToStore: ADC_PIN_MAP expanded to all 18 board types (Uno, Nano,
  Mega with 16 ADC channels, ATtiny85, RP2040 GP26-29, ESP32/S3/C3 GPIO
  ADCs). Voltages from SPICE solutions now inject into MCU ADC peripherals
  for all boards.
- BasicParts LED: reads branchCurrents from useElectricalStore when SPICE
  is active. Brightness = clamp(|I_led| / 20mA, 0, 1) instead of boolean.
  Subscribes to store changes to update in real time.
- ElectricalOverlay: shows per-wire voltage labels (gold monospace on dark
  pill) using buildWireNetMap() which replicates the NetlistBuilder's
  Union-Find to map wireId -> netName -> nodeVoltage. Summary pill shows
  net count + solve time.
- NetlistBuilder: new export buildWireNetMap() for lightweight wire-to-net
  resolution without running ngspice.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:59:22 -03:00
David Montero Crespo 36543e2479 feat: expand SPICE component catalog (fases 9 + 10)
Adds 44 SPICE mappers, 58 custom metadata entries, and 12 visual
Web Components covering logic gates, transistors, op-amps, regulators,
sources, electromechanical parts and integrated-circuit packaging.

Fase 9 — component catalog expansion
------------------------------------
- 7 logic gates (AND/OR/NAND/NOR/XOR/XNOR + NOT) as SPICE B-sources
- 8 multi-input gates (AND/OR/NAND/NOR with 3 and 4 inputs)
- 9 transistors: 5 BJTs (incl. PNP 2N3906/BC557) + 4 MOSFETs (incl.
  P-channel IRF9540/FQP27P06). NMOS refactored from Level=3 W=0.1
  (hangs ngspice) to Level=1 with sane W/L
- 5 op-amps: LM358, LM741, TL072, LM324 with per-chip saturation
  rails + opamp-ideal
- 4 linear regulators (7805, 7812, 7905, LM317) with dropout
- 3 batteries (9V, AA, coin-cell) with realistic ESR
- Signal generator (sine / square / DC)
- 2 Schottky diodes (1N5817, 1N5819) + photodiode (lux-driven
  current source)

Fase 10 — electromechanical + ICs
---------------------------------
- Relay (SPDT): coil + L + S-switch with native hysteresis +
  flyback diode, inverted-control trick for the NC contact
- Optocouplers 4N25 and PC817 (LED + CCCS with CTR=0.5 / 1.0)
- 7 74HC ICs as DIP-14 packages emitting 4 or 6 B-sources per
  component (first mapper pattern emitting multiple device cards)
- 3 flip-flops (D, T, JK) — digital-sim only (edge detection is
  not representable in ngspice .op)
- L293D dual H-bridge motor driver

Infrastructure
--------------
- scripts/component-overrides.json gains a _customComponents[] array
  that lets new Velxio-only parts survive metadata regeneration
  (previously applyOverrides() could only patch wokwi-elements
  components that had already been scanned)
- scripts/generate-component-metadata.ts injects custom entries
  before the patch loop
- New ComponentCategory values: 'logic', 'analog', 'electromech'
- frontend/src/components/DynamicComponent.tsx PASSIVE tracing
  extended from just ['resistor','resistor-us'] to 9 two-terminal
  passives with per-part pin name maps
- New CI workflow test-circuit.yml runs the sandbox on push/PR
- frontend-tests.yml regenerates metadata and fails if committed
  JSON is stale
- Documented 2 new ngspice gotchas in circuit-emulation-gotchas.md:
  unicode in netlist titles silently hangs the parser, and
  MOSFET Level=3 + W=0.1m causes .op to hang
- 164/164 sandbox tests passing in ~9 s (was 88 pre-fase-9)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:41:26 -03:00
davidmonterocrespo24 ed9911dcc3 feat: electrical simulation via ngspice-WASM (eecircuit-engine)
Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy-
loaded  toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate
with real analog behaviour: correct voltages on wires, real I–V curves on
LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM
driving RC filters, transistors, op-amps, diodes, MOSFETs, etc.

Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle
stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates
electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false.

Frontend additions:
- simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with
  UnionFind over wires, componentToSpice mapping (24 metadataIds incl.
  real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148,
  1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with
  debounced coalescing, AVRSpiceBridge for quasi-static co-simulation.
- store/useElectricalStore: Zustand slice, feature-flag aware.
- components/analog-ui/:  toolbar toggle + SVG voltage overlay.
- components/components-instruments/: Voltmeter, Ammeter probes.
- 62 tests (spice-*, netlist-builder, component-to-spice, instruments).

Sandbox (test/test_circuit/): 47-test validation sandbox that proved
the approach (hand-rolled MNA baseline + ngspice pipeline) before
porting to the app. Kept as reference.

Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering
architecture, solvers, components, AVR bridge, gotchas, performance,
integration plan, API reference, appendix) + electrical-simulation-
user-guide.md (end-user facing).

Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:11:54 +00:00