feat: add support for RP2040 board, including simulator and compilation enhancements

pull/10/head
David Montero Crespo 2026-03-04 19:28:33 -03:00
parent f139187382
commit 7944ce2de3
20 changed files with 630 additions and 62 deletions

39
.dockerignore Normal file
View File

@ -0,0 +1,39 @@
# Dependencies
node_modules/
**/node_modules/
# Python
__pycache__/
*.py[cod]
venv/
env/
*.egg-info/
# Build outputs (will be built inside Docker)
frontend/dist/
frontend/.vite/
wokwi-libs/avr8js/dist/
wokwi-libs/wokwi-elements/dist/
# IDE and OS
.vscode/
.idea/
.DS_Store
Thumbs.db
*.swp
# Git
.git/
.gitignore
# Docker files (avoid recursion)
docker-compose.yml
Dockerfile*
# Documentation (not needed in image)
*.md
doc/
LICENSE
# Logs
*.log

4
.github/FUNDING.yml vendored
View File

@ -1 +1,3 @@
github: [davidmonterocrespo24]
github: davidmonterocrespo24
custom:
- https://paypal.me/dmonterocrepoclub

1
.gitignore vendored
View File

@ -76,3 +76,4 @@ wokwi-libs/*/.cache/
!wokwi-libs/wokwi-features/
.claude/settings.local.json
.history/*
.daveagent/*

View File

@ -7,8 +7,10 @@ A fully local, open-source Arduino emulator inspired by [Wokwi](https://wokwi.co
If you find this project helpful, please consider giving it a star! Your support helps the project grow and motivates continued development.
[![GitHub stars](https://img.shields.io/github/stars/davidmonterocrespo24/openwokwi?style=social)](https://github.com/davidmonterocrespo24/openwokwi/stargazers)
[![Sponsor](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-pink?logo=githubsponsors)](https://github.com/sponsors/davidmonterocrespo24)
[![PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal)](https://paypal.me/dmonterocrepoclub)
Every star counts and helps make this project better!
Every star counts and helps make this project better! You can also support the project financially through GitHub Sponsors or PayPal -- any contribution helps keep the development going.
## Screenshots
@ -131,13 +133,37 @@ arduino-cli core install arduino:avr
## Installation
### 1. Clone the repository
### Option A: Docker (Recommended)
The fastest way to get started. Requires only [Docker](https://docs.docker.com/get-docker/) and Docker Compose.
```bash
git clone https://github.com/davidmonterocrespo24/openwokwi.git
cd openwokwi
docker compose up --build
```
Once running:
- **App**: http://localhost:3000
- **API**: http://localhost:8001
- **API Docs**: http://localhost:8001/docs
The Docker setup automatically installs `arduino-cli`, the AVR core, builds the wokwi-libs, and serves everything -- no other prerequisites needed.
To stop:
```bash
docker compose down
```
### Option B: Manual Setup
#### 1. Clone the repository
```bash
git clone https://github.com/davidmonterocrespo24/openwokwi.git
cd openwokwi
```
### 2. Setup Backend
#### 2. Setup Backend
```bash
cd backend
@ -152,7 +178,7 @@ venv\Scripts\activate
pip install -r requirements.txt
```
### 3. Setup Frontend
#### 3. Setup Frontend
```bash
cd frontend

30
backend/Dockerfile Normal file
View File

@ -0,0 +1,30 @@
FROM python:3.12-slim
# Install system dependencies and arduino-cli
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh \
&& mv bin/arduino-cli /usr/local/bin/ \
&& rm -rf bin \
&& apt-get purge -y curl \
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Initialize arduino-cli and install AVR core
RUN arduino-cli core update-index \
&& arduino-cli core install arduino:avr
WORKDIR /app
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
EXPOSE 8001
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]

View File

@ -14,6 +14,8 @@ class CompileRequest(BaseModel):
class CompileResponse(BaseModel):
success: bool
hex_content: str | None = None
binary_content: str | None = None # base64-encoded .bin for RP2040
binary_type: str | None = None # 'bin' or 'uf2'
stdout: str
stderr: str
error: str | None = None
@ -29,6 +31,8 @@ async def compile_sketch(request: CompileRequest):
return CompileResponse(
success=result["success"],
hex_content=result.get("hex_content"),
binary_content=result.get("binary_content"),
binary_type=result.get("binary_type"),
stdout=result.get("stdout", ""),
stderr=result.get("stderr", ""),
error=result.get("error")

View File

@ -1,6 +1,7 @@
import subprocess
import tempfile
import asyncio
import base64
from pathlib import Path
@ -11,10 +12,10 @@ class ArduinoCLIService:
def _ensure_core_installed(self):
"""
Ensure Arduino AVR core is installed
Ensure Arduino AVR core is installed.
RP2040 core is optional (installed separately by the user).
"""
try:
# Check if core is installed
result = subprocess.run(
[self.cli_path, "core", "list"],
capture_output=True,
@ -23,7 +24,6 @@ class ArduinoCLIService:
if "arduino:avr" not in result.stdout:
print("Arduino AVR core not installed. Installing...")
# Install AVR core
subprocess.run(
[self.cli_path, "core", "install", "arduino:avr"],
check=True
@ -33,6 +33,10 @@ class ArduinoCLIService:
print(f"Warning: Could not verify arduino:avr core: {e}")
print("Please ensure arduino-cli is installed and in PATH")
def _is_rp2040_board(self, fqbn: str) -> bool:
"""Return True if the FQBN targets an RP2040/RP2350 board."""
return any(p in fqbn for p in ("rp2040", "rp2350", "mbed_rp2040", "mbed_rp2350"))
async def compile(self, code: str, board_fqbn: str = "arduino:avr:uno") -> dict:
"""
Compile Arduino sketch using arduino-cli
@ -85,31 +89,64 @@ class ArduinoCLIService:
print(f"Stderr: {result.stderr}")
if result.returncode == 0:
# Read compiled hex file
hex_file = build_dir / "sketch.ino.hex"
print(f"Looking for hex file at: {hex_file}")
print(f"Hex file exists: {hex_file.exists()}")
print(f"Files in build dir: {list(build_dir.iterdir())}")
if hex_file.exists():
hex_content = hex_file.read_text()
print(f"Hex file size: {len(hex_content)} bytes")
print("=== Compilation successful ===\n")
return {
"success": True,
"hex_content": hex_content,
"stdout": result.stdout,
"stderr": result.stderr
}
if self._is_rp2040_board(board_fqbn):
# RP2040 outputs a .bin file (and optionally .uf2)
# Try .bin first (raw binary, simplest to load into emulator)
bin_file = build_dir / "sketch.ino.bin"
uf2_file = build_dir / "sketch.ino.uf2"
target_file = bin_file if bin_file.exists() else (uf2_file if uf2_file.exists() else None)
if target_file:
raw_bytes = target_file.read_bytes()
binary_b64 = base64.b64encode(raw_bytes).decode('ascii')
print(f"[RP2040] Binary file: {target_file.name}, size: {len(raw_bytes)} bytes")
print("=== RP2040 Compilation successful ===\n")
return {
"success": True,
"hex_content": None,
"binary_content": binary_b64,
"binary_type": "bin" if target_file == bin_file else "uf2",
"stdout": result.stdout,
"stderr": result.stderr
}
else:
print(f"[RP2040] Binary file not found. Files: {list(build_dir.iterdir())}")
print("=== RP2040 Compilation failed: binary not found ===\n")
return {
"success": False,
"error": "RP2040 binary (.bin/.uf2) not found after compilation",
"stdout": result.stdout,
"stderr": result.stderr
}
else:
# List files in build directory
print(f"Files in build dir: {list(build_dir.iterdir())}")
print("=== Compilation failed: hex file not found ===\n")
return {
"success": False,
"error": "Hex file not found after compilation",
"stdout": result.stdout,
"stderr": result.stderr
}
# AVR outputs a .hex file (Intel HEX format)
hex_file = build_dir / "sketch.ino.hex"
print(f"Looking for hex file at: {hex_file}")
print(f"Hex file exists: {hex_file.exists()}")
if hex_file.exists():
hex_content = hex_file.read_text()
print(f"Hex file size: {len(hex_content)} bytes")
print("=== AVR Compilation successful ===\n")
return {
"success": True,
"hex_content": hex_content,
"binary_content": None,
"stdout": result.stdout,
"stderr": result.stderr
}
else:
print(f"Files in build dir: {list(build_dir.iterdir())}")
print("=== Compilation failed: hex file not found ===\n")
return {
"success": False,
"error": "Hex file not found after compilation",
"stdout": result.stdout,
"stderr": result.stderr
}
else:
print("=== Compilation failed ===\n")
return {

25
docker-compose.yml Normal file
View File

@ -0,0 +1,25 @@
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8001:8001"
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8001/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
ports:
- "3000:80"
depends_on:
backend:
condition: service_healthy
restart: unless-stopped

1
frontend/.env.production Normal file
View File

@ -0,0 +1 @@
VITE_API_BASE=/api

43
frontend/Dockerfile Normal file
View File

@ -0,0 +1,43 @@
# ---- Stage 1: Build wokwi-libs and frontend ----
FROM node:20-slim AS builder
WORKDIR /app
# Copy root package.json (needed for metadata generation via tsx)
COPY package.json .
RUN npm install
# Copy metadata generation script
COPY scripts/ scripts/
# Copy and build avr8js
COPY wokwi-libs/avr8js/ wokwi-libs/avr8js/
WORKDIR /app/wokwi-libs/avr8js
RUN npm install && npm run build
# Copy and build wokwi-elements
COPY wokwi-libs/wokwi-elements/ wokwi-libs/wokwi-elements/
WORKDIR /app/wokwi-libs/wokwi-elements
RUN npm install && npm run build
# Copy frontend source and install dependencies
WORKDIR /app
COPY frontend/ frontend/
WORKDIR /app/frontend
RUN npm install
# Generate component metadata and build frontend for production
RUN npm run build
# ---- Stage 2: Serve with nginx ----
FROM nginx:alpine
# Copy custom nginx config
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
# Copy built frontend assets
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

26
frontend/nginx.conf Normal file
View File

@ -0,0 +1,26 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Frontend SPA serve index.html for all non-file routes
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API requests to backend
location /api/ {
proxy_pass http://backend:8001/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View File

@ -0,0 +1,36 @@
import '@wokwi/elements';
import { useRef, useEffect } from 'react';
interface NanoRP2040Props {
id?: string;
x?: number;
y?: number;
ledBuiltIn?: boolean;
}
export const NanoRP2040 = ({
id = 'nano-rp2040',
x = 0,
y = 0,
ledBuiltIn = false,
}: NanoRP2040Props) => {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
if (ref.current) {
(ref.current as any).ledBuiltIn = ledBuiltIn;
}
}, [ledBuiltIn]);
return (
<wokwi-nano-rp2040-connect
id={id}
ref={ref}
style={{
position: 'absolute',
left: `${x}px`,
top: `${y}px`,
}}
/>
);
};

View File

@ -1,12 +1,21 @@
import { useState } from 'react';
import { useEditorStore } from '../../store/useEditorStore';
import { useSimulatorStore } from '../../store/useSimulatorStore';
import { useSimulatorStore, BOARD_FQBN } from '../../store/useSimulatorStore';
import { compileCode } from '../../services/compilation';
import './EditorToolbar.css';
export const EditorToolbar = () => {
const { code } = useEditorStore();
const { setCompiledHex, startSimulation, stopSimulation, resetSimulation, running, compiledHex } = useSimulatorStore();
const {
boardType,
setCompiledHex,
setCompiledBinary,
startSimulation,
stopSimulation,
resetSimulation,
running,
compiledHex,
} = useSimulatorStore();
const [compiling, setCompiling] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
@ -15,21 +24,27 @@ export const EditorToolbar = () => {
setMessage(null);
try {
console.log('Starting compilation...');
const result = await compileCode(code);
const fqbn = BOARD_FQBN[boardType];
console.log('Starting compilation for board:', fqbn);
const result = await compileCode(code, fqbn);
console.log('Compilation result:', result);
if (result.success && result.hex_content) {
setCompiledHex(result.hex_content);
setMessage({ type: 'success', text: 'Compilation successful! Ready to run.' });
if (result.success) {
if (result.hex_content) {
// AVR path
setCompiledHex(result.hex_content);
setMessage({ type: 'success', text: 'Compilation successful! Ready to run.' });
} else if (result.binary_content) {
// RP2040 path
setCompiledBinary(result.binary_content);
setMessage({ type: 'success', text: 'Compilation successful! Ready to run.' });
} else {
setMessage({ type: 'error', text: 'Compilation produced no output' });
}
} else {
const errorMsg = result.error || result.stderr || 'Compilation failed';
console.error('Compilation error:', errorMsg);
console.error('Full result:', JSON.stringify(result, null, 2));
setMessage({
type: 'error',
text: errorMsg,
});
setMessage({ type: 'error', text: errorMsg });
}
} catch (err) {
console.error('Compilation exception:', err);

View File

@ -1,6 +1,8 @@
import { useSimulatorStore, ARDUINO_POSITION } from '../../store/useSimulatorStore';
import { useSimulatorStore, ARDUINO_POSITION, BOARD_LABELS } from '../../store/useSimulatorStore';
import type { BoardType } from '../../store/useSimulatorStore';
import React, { useEffect, useState, useRef } from 'react';
import { ArduinoUno } from '../components-wokwi/ArduinoUno';
import { NanoRP2040 } from '../components-wokwi/NanoRP2040';
import { ComponentPickerModal } from '../ComponentPickerModal';
import { ComponentPropertyDialog } from './ComponentPropertyDialog';
import { DynamicComponent, createComponentFromMetadata } from '../DynamicComponent';
@ -14,6 +16,8 @@ import './SimulatorCanvas.css';
export const SimulatorCanvas = () => {
const {
boardType,
setBoardType,
components,
running,
pinManager,
@ -371,8 +375,21 @@ export const SimulatorCanvas = () => {
{/* Main Canvas */}
<div className="simulator-canvas">
<div className="canvas-header">
<h3>Arduino Simulator</h3>
<h3>Simulator</h3>
<div className="canvas-header-info">
{/* Board selector */}
<select
className="board-selector"
value={boardType}
onChange={(e) => setBoardType(e.target.value as BoardType)}
disabled={running}
title="Select board"
>
{(Object.entries(BOARD_LABELS) as [BoardType, string][]).map(([type, label]) => (
<option key={type} value={type}>{label}</option>
))}
</select>
<button
className="add-component-btn"
onClick={() => setShowComponentPicker(true)}
@ -397,16 +414,24 @@ export const SimulatorCanvas = () => {
{/* Wire Layer - Renders below all components */}
<WireLayer />
{/* Arduino Uno Board using wokwi-elements */}
<ArduinoUno
x={ARDUINO_POSITION.x}
y={ARDUINO_POSITION.y}
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
/>
{/* Board visual — switches based on selected board type */}
{boardType === 'arduino-uno' ? (
<ArduinoUno
x={ARDUINO_POSITION.x}
y={ARDUINO_POSITION.y}
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
/>
) : (
<NanoRP2040
x={ARDUINO_POSITION.x}
y={ARDUINO_POSITION.y}
ledBuiltIn={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
/>
)}
{/* Arduino pin overlay */}
{/* Board pin overlay */}
<PinOverlay
componentId="arduino-uno"
componentId={boardType === 'arduino-uno' ? 'arduino-uno' : 'nano-rp2040'}
componentX={ARDUINO_POSITION.x}
componentY={ARDUINO_POSITION.y}
onPinClick={handlePinClick}

View File

@ -1,10 +1,12 @@
import axios from 'axios';
const API_BASE = 'http://localhost:8001/api';
const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8001/api';
export interface CompileResult {
success: boolean;
hex_content?: string;
binary_content?: string; // base64-encoded .bin for RP2040
binary_type?: 'bin' | 'uf2';
stdout: string;
stderr: string;
error?: string;

View File

@ -72,6 +72,20 @@ export class PinManager {
return this.pinStates.get(arduinoPin) || false;
}
/**
* Directly fire pin change callbacks for a specific pin.
* Used by RP2040Simulator which has individual GPIO listeners instead of PORT registers.
*/
triggerPinChange(pin: number, state: boolean): void {
const current = this.pinStates.get(pin);
if (current === state) return; // no change
this.pinStates.set(pin, state);
const callbacks = this.listeners.get(pin);
if (callbacks) {
callbacks.forEach(cb => cb(pin, state));
}
}
// ── PWM duty cycle API ───────────────────────────────────────────────────
/**

View File

@ -0,0 +1,184 @@
import { RP2040, GPIOPinState } from 'rp2040js';
import { PinManager } from './PinManager';
/**
* RP2040Simulator Emulates Raspberry Pi Pico (RP2040) using rp2040js
*
* Features:
* - ARM Cortex-M0+ CPU emulation at 125 MHz
* - 30 GPIO pins (GPIO0-GPIO29)
* - ADC on GPIO26-GPIO29 (A0-A3)
* - LED_BUILTIN on GPIO25
*
* Arduino-pico pin mapping (Earle Philhower's core):
* D0 = GPIO0 D29 = GPIO29
* A0 = GPIO26 A3 = GPIO29
* LED_BUILTIN = GPIO25
*/
const F_CPU = 125_000_000; // 125 MHz
const CYCLE_NANOS = 1e9 / F_CPU; // nanoseconds per cycle (~8 ns)
const FPS = 60;
const CYCLES_PER_FRAME = Math.floor(F_CPU / FPS); // ~2 083 333
export class RP2040Simulator {
private rp2040: RP2040 | null = null;
private running = false;
private animationFrame: number | null = null;
public pinManager: PinManager;
private speed = 1.0;
private gpioUnsubscribers: Array<() => void> = [];
constructor(pinManager: PinManager) {
this.pinManager = pinManager;
}
/**
* Load a compiled binary into the RP2040 flash memory.
* Accepts a base64-encoded string of the raw .bin file output by arduino-cli.
*/
loadBinary(base64: string): void {
console.log('[RP2040] Loading binary...');
const binaryStr = atob(base64);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
console.log(`[RP2040] Binary size: ${bytes.length} bytes`);
this.rp2040 = new RP2040();
// Load binary into flash starting at offset 0 (maps to 0x10000000)
this.rp2040.flash.set(bytes, 0);
// Set up GPIO listeners
this.setupGpioListeners();
console.log('[RP2040] CPU initialized, GPIO listeners attached');
}
/** Same interface as AVRSimulator for store compatibility */
loadHex(_hexContent: string): void {
console.warn('[RP2040] loadHex() called on RP2040Simulator — use loadBinary() instead');
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getADC(): any {
return this.rp2040?.adc ?? null;
}
private setupGpioListeners(): void {
this.gpioUnsubscribers.forEach(fn => fn());
this.gpioUnsubscribers = [];
if (!this.rp2040) return;
for (let gpioIdx = 0; gpioIdx < 30; gpioIdx++) {
const pin = gpioIdx;
const gpio = this.rp2040.gpio[gpioIdx];
if (!gpio) continue;
const unsub = gpio.addListener((state: GPIOPinState, _oldState: GPIOPinState) => {
const isHigh = state === GPIOPinState.High;
this.pinManager.triggerPinChange(pin, isHigh);
});
this.gpioUnsubscribers.push(unsub);
}
}
start(): void {
if (this.running || !this.rp2040) {
console.warn('[RP2040] Already running or not initialized');
return;
}
this.running = true;
console.log('[RP2040] Starting simulation at 125 MHz...');
let frameCount = 0;
const execute = (_timestamp: number) => {
if (!this.running || !this.rp2040) return;
const cyclesTarget = Math.floor(CYCLES_PER_FRAME * this.speed);
const { core } = this.rp2040;
// Access the internal clock — rp2040js attaches it to the RP2040 instance
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const clock = (this.rp2040 as any).clock;
try {
let cyclesDone = 0;
while (cyclesDone < cyclesTarget) {
if (core.waiting) {
if (clock) {
const jump: number = clock.nanosToNextAlarm;
clock.tick(jump);
cyclesDone += Math.ceil(jump / CYCLE_NANOS);
} else {
break;
}
} else {
const cycles: number = core.executeInstruction();
if (clock) clock.tick(cycles * CYCLE_NANOS);
cyclesDone += cycles;
}
}
frameCount++;
if (frameCount % 60 === 0) {
console.log(`[RP2040] Frame ${frameCount}, PC: 0x${core.PC.toString(16)}`);
}
} catch (error) {
console.error('[RP2040] Simulation error:', error);
this.stop();
return;
}
this.animationFrame = requestAnimationFrame(execute);
};
this.animationFrame = requestAnimationFrame(execute);
}
stop(): void {
if (!this.running) return;
this.running = false;
if (this.animationFrame !== null) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
console.log('[RP2040] Simulation stopped');
}
reset(): void {
this.stop();
if (this.rp2040) {
const flashCopy = new Uint8Array(this.rp2040.flash);
this.rp2040 = new RP2040();
this.rp2040.flash.set(flashCopy, 0);
this.setupGpioListeners();
console.log('[RP2040] CPU reset');
}
}
isRunning(): boolean {
return this.running;
}
setSpeed(speed: number): void {
this.speed = Math.max(0.1, Math.min(10.0, speed));
}
/**
* Drive a GPIO pin externally (e.g. from a button or slider).
* GPIO n = Arduino D(n) for Raspberry Pi Pico.
*/
setPinState(arduinoPin: number, state: boolean): void {
if (!this.rp2040) return;
const gpio = this.rp2040.gpio[arduinoPin];
if (gpio) {
gpio.setInputValue(state);
}
}
}

View File

@ -1,4 +1,7 @@
import { AVRSimulator } from '../AVRSimulator';
import { RP2040Simulator } from '../RP2040Simulator';
export type AnySimulator = AVRSimulator | RP2040Simulator;
/**
* Interface for simulation logic mapped to a specific wokwi-element
@ -25,7 +28,7 @@ export interface PartSimulationLogic {
*/
attachEvents?: (
element: HTMLElement,
avrSimulator: AVRSimulator,
simulator: AnySimulator,
getArduinoPinHelper: (componentPinName: string) => number | null
) => () => void;
}

View File

@ -1,9 +1,22 @@
import { create } from 'zustand';
import { AVRSimulator } from '../simulation/AVRSimulator';
import { RP2040Simulator } from '../simulation/RP2040Simulator';
import { PinManager } from '../simulation/PinManager';
import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
import { calculatePinPosition } from '../utils/pinPositionCalculator';
export type BoardType = 'arduino-uno' | 'raspberry-pi-pico';
export const BOARD_FQBN: Record<BoardType, string> = {
'arduino-uno': 'arduino:avr:uno',
'raspberry-pi-pico': 'rp2040:rp2040:rpipico',
};
export const BOARD_LABELS: Record<BoardType, string> = {
'arduino-uno': 'Arduino Uno',
'raspberry-pi-pico': 'Raspberry Pi Pico',
};
// Fixed position for the Arduino board (not in components array)
export const ARDUINO_POSITION = { x: 50, y: 50 };
@ -16,8 +29,12 @@ interface Component {
}
interface SimulatorState {
// Board selection
boardType: BoardType;
setBoardType: (type: BoardType) => void;
// Simulation state
simulator: AVRSimulator | null;
simulator: AVRSimulator | RP2040Simulator | null;
pinManager: PinManager;
running: boolean;
compiledHex: string | null;
@ -33,10 +50,12 @@ interface SimulatorState {
// Actions
initSimulator: () => void;
loadHex: (hex: string) => void;
loadBinary: (base64: string) => void;
startSimulation: () => void;
stopSimulation: () => void;
resetSimulation: () => void;
setCompiledHex: (hex: string) => void;
setCompiledBinary: (base64: string) => void;
setRunning: (running: boolean) => void;
// Component management
@ -70,6 +89,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
const pinManager = new PinManager();
return {
boardType: 'arduino-uno' as BoardType,
simulator: null,
pinManager,
running: false,
@ -133,15 +153,30 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
selectedWireId: null,
wireInProgress: null,
setBoardType: (type: BoardType) => {
const { running } = get();
if (running) {
get().stopSimulation();
}
const simulator = type === 'arduino-uno'
? new AVRSimulator(pinManager)
: new RP2040Simulator(pinManager);
set({ boardType: type, simulator, compiledHex: null });
console.log(`Board switched to: ${type}`);
},
initSimulator: () => {
const simulator = new AVRSimulator(pinManager);
const { boardType } = get();
const simulator = boardType === 'arduino-uno'
? new AVRSimulator(pinManager)
: new RP2040Simulator(pinManager);
set({ simulator });
console.log('Simulator initialized');
console.log(`Simulator initialized: ${boardType}`);
},
loadHex: (hex: string) => {
const { simulator } = get();
if (simulator) {
if (simulator && simulator instanceof AVRSimulator) {
try {
simulator.loadHex(hex);
set({ compiledHex: hex });
@ -150,7 +185,22 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
console.error('Failed to load HEX:', error);
}
} else {
console.warn('Simulator not initialized');
console.warn('loadHex: simulator not initialized or wrong board type');
}
},
loadBinary: (base64: string) => {
const { simulator } = get();
if (simulator && simulator instanceof RP2040Simulator) {
try {
simulator.loadBinary(base64);
set({ compiledHex: base64 }); // reuse compiledHex as "program loaded" flag
console.log('Binary loaded into RP2040 successfully');
} catch (error) {
console.error('Failed to load binary:', error);
}
} else {
console.warn('loadBinary: simulator not initialized or wrong board type');
}
},
@ -180,10 +230,14 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
setCompiledHex: (hex: string) => {
set({ compiledHex: hex });
// Auto-load hex when set
get().loadHex(hex);
},
setCompiledBinary: (base64: string) => {
set({ compiledHex: base64 }); // use compiledHex as "program ready" flag
get().loadBinary(base64);
},
setRunning: (running: boolean) => set({ running }),
addComponent: (component) => {

View File

@ -8,10 +8,11 @@ export default defineConfig({
resolve: {
alias: {
'avr8js': path.resolve(__dirname, '../wokwi-libs/avr8js/dist/esm'),
'rp2040js': path.resolve(__dirname, '../wokwi-libs/rp2040js/dist/esm'),
'@wokwi/elements': path.resolve(__dirname, '../wokwi-libs/wokwi-elements/dist/esm'),
},
},
optimizeDeps: {
include: ['avr8js', '@wokwi/elements'],
include: ['avr8js', 'rp2040js', '@wokwi/elements'],
},
})