Commit Graph

3 Commits

Author SHA1 Message Date
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
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