From 7944ce2de3d9d8fe3d0e2fad1a904a4f7bb68c3a Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Wed, 4 Mar 2026 19:28:33 -0300 Subject: [PATCH] feat: add support for RP2040 board, including simulator and compilation enhancements --- .dockerignore | 39 ++++ .github/FUNDING.yml | 4 +- .gitignore | 1 + README.md | 34 +++- backend/Dockerfile | 30 +++ backend/app/api/routes/compile.py | 4 + backend/app/services/arduino_cli.py | 89 ++++++--- docker-compose.yml | 25 +++ frontend/.env.production | 1 + frontend/Dockerfile | 43 ++++ frontend/nginx.conf | 26 +++ .../components-wokwi/NanoRP2040.tsx | 36 ++++ .../src/components/editor/EditorToolbar.tsx | 39 ++-- .../components/simulator/SimulatorCanvas.tsx | 45 ++++- frontend/src/services/compilation.ts | 4 +- frontend/src/simulation/PinManager.ts | 14 ++ frontend/src/simulation/RP2040Simulator.ts | 184 ++++++++++++++++++ .../parts/PartSimulationRegistry.ts | 5 +- frontend/src/store/useSimulatorStore.ts | 66 ++++++- frontend/vite.config.ts | 3 +- 20 files changed, 630 insertions(+), 62 deletions(-) create mode 100644 .dockerignore create mode 100644 backend/Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend/.env.production create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf create mode 100644 frontend/src/components/components-wokwi/NanoRP2040.tsx create mode 100644 frontend/src/simulation/RP2040Simulator.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8b51619 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 59edb51..db5ee64 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,3 @@ -github: [davidmonterocrespo24] +github: davidmonterocrespo24 +custom: + - https://paypal.me/dmonterocrepoclub diff --git a/.gitignore b/.gitignore index a090c80..0f54e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ wokwi-libs/*/.cache/ !wokwi-libs/wokwi-features/ .claude/settings.local.json .history/* +.daveagent/* \ No newline at end of file diff --git a/README.md b/README.md index 6af5e50..5836a37 100644 --- a/README.md +++ b/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. [![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 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..5a27878 --- /dev/null +++ b/backend/Dockerfile @@ -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"] diff --git a/backend/app/api/routes/compile.py b/backend/app/api/routes/compile.py index ae9bbd4..eb8a4ce 100644 --- a/backend/app/api/routes/compile.py +++ b/backend/app/api/routes/compile.py @@ -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") diff --git a/backend/app/services/arduino_cli.py b/backend/app/services/arduino_cli.py index d37dd4c..83ba217 100644 --- a/backend/app/services/arduino_cli.py +++ b/backend/app/services/arduino_cli.py @@ -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 { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ce624d1 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..0a0c303 --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1 @@ +VITE_API_BASE=/api diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..8fafcf9 --- /dev/null +++ b/frontend/Dockerfile @@ -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;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..b522fd3 --- /dev/null +++ b/frontend/nginx.conf @@ -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"; + } +} diff --git a/frontend/src/components/components-wokwi/NanoRP2040.tsx b/frontend/src/components/components-wokwi/NanoRP2040.tsx new file mode 100644 index 0000000..8322493 --- /dev/null +++ b/frontend/src/components/components-wokwi/NanoRP2040.tsx @@ -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(null); + + useEffect(() => { + if (ref.current) { + (ref.current as any).ledBuiltIn = ledBuiltIn; + } + }, [ledBuiltIn]); + + return ( + + ); +}; diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 58357eb..53451b8 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -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); diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 56d2a92..6fd570a 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -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 */}
-

Arduino Simulator

+

Simulator

+ {/* Board selector */} + +