velxio/frontend
davidmonterocrespo24 0f05544ca8 feat(esp32): SignalRouter — model the GPIO Matrix as first-class
Replaces the per-peripheral ad-hoc `_ledc_gpio_map` cache with a
proper signal-routing abstraction that mirrors the ESP32 SoC's
IO_MUX + GPIO Matrix exactly. Same idea as real silicon: signal
sources (LEDC channels, RMT, MCPWM, ...) → 40-entry routing table
→ GPIO pins.

Motivation (from user bug report in
velxio.dev/project/5218f9e3-136d-43b3-bba1-6cebde21e1a4): two
ESP32 servos on a solar-tracker visibly oscillated between two
positions instead of moving smoothly when the user changed LDR
sliders. Commit 77bf897 patched it (per-channel gpio memo +
broadcast guard) but the user requested a proper hardware-fidel
architecture, not patches.

Backend:
* `app/services/signal_router.py` — SignalRouter class. Forward
  index (gpio → signal_id) + reverse index (signal_id → set of
  gpios). `replace_snapshot()` returns the diff for the polling-
  fallback path; future C plugin hook becomes a push without
  touching this code.
* `app/services/esp32_signals.py` — Signal id constants from
  ESP32 TRM (LEDC HS 72-79, LS 80-87) + `ledc_signal_for_channel()`
  helper.
* `app/services/esp32_worker.py` — `_ledc_gpio_map` is gone;
  `_refresh_ledc_gpio_map` replaced by `_refresh_signal_routing`
  which emits `gpio_routing {gpio, signal_id}` events on diff.
  The 0x5000 LEDC callback and the LEDC poll thread now emit
  `ledc_duty {channel, duty_pct}` (canonical, no gpio) alongside
  the legacy `ledc_update {channel, duty, gpio}` for back-compat
  during rollout.

Frontend:
* `simulation/SignalRouter.ts` — 1-to-1 TS mirror of the Python
  class. Same forward + reverse index; same `pinsForSignal` /
  `updateRouting` / `clearRouting` API.
* `simulation/esp32-signals.ts` — Signal id constants, mirror
  of the Python module.
* `simulation/Esp32Bridge.ts` — new `onLedcDuty`, `onGpioRouting`,
  `onGpioRoutingClear` callbacks; handlers for the new event types.
* `store/useSimulatorStore.ts` — `makeLedcDutyHandler` looks up
  pins via `router.pinsForSignal(ledcSignalForChannel(channel))`
  and dispatches per pin. `makeGpioRoutingHandler` /
  `makeGpioRoutingClearHandler` keep the mirror in sync. Per-board
  `signalRouterMap` parallels `pinManagerMap` in lifecycle.
  `makeLedcUpdateHandler` (and its memo workaround from 77bf897)
  stays wired for back-compat during rollout; removed in a
  follow-up commit once prod is verified stable on the new path.

Tests:
* `test/backend/unit/test_signal_router.py` (20 tests) covers
  update/clear semantics, idempotency, multi-pin routing,
  snapshot diff, channel↔signal-id helpers, and the multi-servo
  regression scenario.
* `frontend/src/__tests__/SignalRouter.test.ts` (17 tests) is the
  mirror — same scenarios on the TS side.
* `frontend/src/__tests__/esp32-multi-servo-gpio-matrix.test.ts`
  (6 tests) drives the end-to-end SignalRouter handler pipeline,
  asserts that two servos on GPIO 13/12 via LEDC channels 0/1
  move independently (no mirroring), that re-routing carries
  cleanly, and — critically — that `PinManager.broadcastPwm` is
  never called.

Totals: +700 LOC, 1876 frontend tests pass (was 1853), 278 backend
unit tests pass (was 259).

Docs: ESP32_EMULATION.md §9.2 rewritten with the new architecture
diagram + a runbook for adding future peripherals through the
SignalRouter.

The C plugin hook in qemu-lcgamboa that would push gpio_out_sel
writes synchronously (eliminating the polling race window entirely)
is the next step — kept as a follow-up because the polling-fallback
path here already resolves the routing before each duty event
fires, so the bug is fixed end-to-end. The plugin work removes the
race condition fundamentally.
2026-05-17 05:00:53 +02:00
..
public chore(sitemap): bump lastmod dates to 2026-05-16 2026-05-16 17:34:25 -03:00
scripts fix(monaco): cross-platform postinstall via Node script 2026-05-11 12:42:33 -03:00
src feat(esp32): SignalRouter — model the GPIO Matrix as first-class 2026-05-17 05:00:53 +02:00
.env.production
.gitignore fix(monaco): ignore public/monaco/ in git per Copilot suggestion (PR #163) 2026-05-11 12:24:45 -03:00
.prettierignore
.prettierrc.json
Dockerfile
README.md docs(frontend): update README to reflect npm packages instead of third-party clones 2026-05-11 13:03:52 -03:00
eslint.config.js
esp32 plan.md
index.html
nginx.conf
package.json ci: Phase 1d-tests I + K + L — workflow hardening + nightly library-compile 2026-05-15 23:19:11 +02:00
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vite.config.ts test(sim): Phase 1d-tests J + C — vitest.config.ts + components-metadata integrity 2026-05-15 22:58:12 +02:00
vitest.config.ts test(sim): Phase 1d-tests J + C — vitest.config.ts + components-metadata integrity 2026-05-15 22:58:12 +02:00

README.md

Arduino Emulator - Frontend

React + TypeScript + Vite frontend for the Arduino emulator with visual simulator and code editor.

Features

  • Monaco Code Editor - Full VSCode-like Arduino code editing experience
  • Dynamic Component System - 48+ wokwi-elements components with search and categories
  • Visual Simulator Canvas - Interactive drag-and-drop circuit builder
  • Component Property Dialog - Single-click component interaction (rotate, delete, view pins)
  • Segment-Based Wire Editing - Drag wire segments perpendicular to orientation (like Wokwi)
  • Real AVR8 Emulation - Actual ATmega328p emulation using avr8js
  • Pin Management - Automatic pin mapping and state synchronization
  • Grid Snapping - 20px grid alignment for clean circuit layouts

Tech Stack

  • React 18 - UI framework
  • TypeScript - Static typing
  • Vite 5 - Build tool and dev server
  • Monaco Editor - Code editor (VSCode engine)
  • Zustand - State management
  • Axios - HTTP client for backend API
  • avr8js - AVR8 CPU emulator (npm package)
  • @wokwi/elements - Electronic web components (npm package)

Development

Prerequisites

Install Dependencies

npm install

Run Development Server

npm run dev

The app will be available at http://localhost:5173

Build for Production

npm run build

Output will be in the dist/ directory.

Lint

npm run lint

Project Structure

frontend/
├── src/
│   ├── components/
│   │   ├── velxio-components/    # React wrappers for wokwi-elements + Velxio-original parts
│   │   ├── editor/               # Monaco Editor components
│   │   │   ├── CodeEditor.tsx
│   │   │   └── EditorToolbar.tsx
│   │   └── simulator/            # Simulation canvas components
│   │       ├── SimulatorCanvas.tsx
│   │       ├── WireLayer.tsx
│   │       ├── WireRenderer.tsx
│   │       ├── PinOverlay.tsx
│   │       ├── ComponentPropertyDialog.tsx
│   │       ├── ComponentPickerModal.tsx
│   │       └── ComponentPalette.tsx
│   ├── simulation/
│   │   ├── AVRSimulator.ts       # AVR8 CPU wrapper
│   │   └── PinManager.ts         # Pin mapping and callbacks
│   ├── store/
│   │   ├── useEditorStore.ts     # Code editor state
│   │   └── useSimulatorStore.ts  # Simulation state
│   ├── services/
│   │   ├── api.ts                # Backend API client
│   │   └── ComponentRegistry.ts  # Component metadata
│   ├── types/                    # TypeScript definitions
│   ├── utils/
│   │   ├── hexParser.ts          # Intel HEX parser
│   │   ├── wirePathGenerator.ts  # Wire SVG path generation
│   │   └── wireSegments.ts       # Segment-based wire editing
│   ├── App.tsx                   # Main app component
│   └── main.tsx                  # Entry point
├── public/                       # Static assets
├── vite.config.ts               # Vite configuration
└── package.json

Key Architecture Patterns

State Management (Zustand)

Two main stores:

  • useEditorStore - Code content, theme, compilation state
  • useSimulatorStore - Simulation running state, components, wires, compiled hex

Wokwi Libraries

@wokwi/elements, avr8js and rp2040js are regular npm dependencies resolved from node_modules like any other package — no local clones required.

AVR Simulation Loop

  • Runs at ~60 FPS using requestAnimationFrame
  • Executes ~267,000 CPU cycles per frame (16MHz / 60fps)
  • Port listeners fire when GPIO registers change
  • PinManager routes pin states to component callbacks

Component System

Components are Web Components from wokwi-elements:

  1. React wrappers in velxio-components/
  2. Dynamic loading via ComponentRegistry
  3. Pin info extracted from component metadata
  4. State updates via refs and callbacks

Wire Editing System

Segment-based editing (like Wokwi):

  • Wires consist of orthogonal segments (horizontal/vertical)
  • Drag segments perpendicular to orientation:
    • Horizontal segments: move up/down (ns-resize)
    • Vertical segments: move left/right (ew-resize)
  • Local preview state during drag (requestAnimationFrame)
  • Store update only on mouse up with grid snapping (20px)

Performance Optimizations

  • requestAnimationFrame for smooth wire dragging
  • Local state for real-time previews
  • Memoized path generation and segment computation
  • Store updates batched at interaction completion

API Integration

Backend endpoints (http://localhost:8001):

  • POST /api/compile - Compile Arduino code to .hex
  • GET /api/compile/status/{task_id} - Check compilation status
  • GET /api/compile/download/{filename} - Download compiled .hex

See backend documentation for API details.

Component Development

Adding a New Component Type

  1. Check if wokwi-elements has the component:

    ls ../third-party/wokwi-elements/src/
    
  2. Create React wrapper in src/components/velxio-components/:

    import React, { useRef, useEffect } from 'react';
    
    export const WokwiMyComponent: React.FC<Props> = ({ ... }) => {
      const elementRef = useRef<any>(null);
    
      useEffect(() => {
        if (elementRef.current) {
          elementRef.current.setAttribute('prop', value);
        }
      }, [value]);
    
      return <wokwi-my-component ref={elementRef} />;
    };
    
  3. Add to ComponentRegistry metadata

  4. Use in SimulatorCanvas or make available in ComponentPalette

Troubleshooting

Monaco Editor Not Loading

  • Check if monaco-editor is installed
  • Verify Vite worker configuration in vite.config.ts

Components Not Rendering

  • Ensure wokwi-elements is built: cd ../third-party/wokwi-elements && npm run build
  • Check browser console for Web Component registration errors
  • Verify Vite alias paths in vite.config.ts

Wire Editing Performance Issues

  • Ensure requestAnimationFrame is being used
  • Check that store updates only happen on mouse up, not during drag
  • Verify no unnecessary re-renders with React DevTools

Pin Alignment Issues

  • Pin coordinates from wokwi-elements are in CSS pixels
  • Do NOT multiply by MM_TO_PX conversion factor
  • Verify component position + pin offset calculation

Compilation Fails

  • Check backend is running at http://localhost:8001
  • Verify arduino-cli is installed and arduino:avr core is available
  • Check CORS configuration in backend

References