feat: add support for RP2040 board, including simulator and compilation enhancements
parent
f139187382
commit
7944ce2de3
|
|
@ -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
|
||||
|
|
@ -1 +1,3 @@
|
|||
github: [davidmonterocrespo24]
|
||||
github: davidmonterocrespo24
|
||||
custom:
|
||||
- https://paypal.me/dmonterocrepoclub
|
||||
|
|
|
|||
|
|
@ -76,3 +76,4 @@ wokwi-libs/*/.cache/
|
|||
!wokwi-libs/wokwi-features/
|
||||
.claude/settings.local.json
|
||||
.history/*
|
||||
.daveagent/*
|
||||
34
README.md
34
README.md
|
|
@ -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.
|
||||
|
||||
[](https://github.com/davidmonterocrespo24/openwokwi/stargazers)
|
||||
[](https://github.com/sponsors/davidmonterocrespo24)
|
||||
[](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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
VITE_API_BASE=/api
|
||||
|
|
@ -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;"]
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue