feat: Add Arduino Nano support to simulator and update related components

pull/10/head
David Montero Crespo 2026-03-07 23:14:33 -03:00
parent c7f9d6d029
commit 41dfd20583
6 changed files with 73 additions and 16 deletions

View File

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

View File

@ -2,6 +2,7 @@ import { useSimulatorStore, BOARD_LABELS } from '../../store/useSimulatorStore';
import type { BoardType } from '../../store/useSimulatorStore'; import type { BoardType } from '../../store/useSimulatorStore';
import React, { useEffect, useState, useRef, useCallback } from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import { ArduinoUno } from '../components-wokwi/ArduinoUno'; import { ArduinoUno } from '../components-wokwi/ArduinoUno';
import { ArduinoNano } from '../components-wokwi/ArduinoNano';
import { NanoRP2040 } from '../components-wokwi/NanoRP2040'; import { NanoRP2040 } from '../components-wokwi/NanoRP2040';
import { ComponentPickerModal } from '../ComponentPickerModal'; import { ComponentPickerModal } from '../ComponentPickerModal';
import { ComponentPropertyDialog } from './ComponentPropertyDialog'; import { ComponentPropertyDialog } from './ComponentPropertyDialog';
@ -587,6 +588,12 @@ export const SimulatorCanvas = () => {
y={boardPosition.y} y={boardPosition.y}
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)} led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
/> />
) : boardType === 'arduino-nano' ? (
<ArduinoNano
x={boardPosition.x}
y={boardPosition.y}
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
/>
) : ( ) : (
<NanoRP2040 <NanoRP2040
x={boardPosition.x} x={boardPosition.x}
@ -602,8 +609,8 @@ export const SimulatorCanvas = () => {
position: 'absolute', position: 'absolute',
left: boardPosition.x, left: boardPosition.x,
top: boardPosition.y, top: boardPosition.y,
width: boardType === 'arduino-uno' ? 360 : 280, width: boardType === 'arduino-uno' ? 360 : boardType === 'arduino-nano' ? 175 : 280,
height: boardType === 'arduino-uno' ? 250 : 180, height: boardType === 'arduino-uno' ? 250 : boardType === 'arduino-nano' ? 70 : 180,
cursor: 'move', cursor: 'move',
zIndex: 1, zIndex: 1,
}} }}
@ -621,7 +628,7 @@ export const SimulatorCanvas = () => {
{/* Board pin overlay */} {/* Board pin overlay */}
<PinOverlay <PinOverlay
componentId={boardType === 'arduino-uno' ? 'arduino-uno' : 'nano-rp2040'} componentId={boardType === 'arduino-uno' ? 'arduino-uno' : boardType === 'arduino-nano' ? 'arduino-nano' : 'nano-rp2040'}
componentX={boardPosition.x} componentX={boardPosition.x}
componentY={boardPosition.y} componentY={boardPosition.y}
onPinClick={handlePinClick} onPinClick={handlePinClick}

View File

@ -11,7 +11,7 @@ export interface ExampleProject {
category: 'basics' | 'sensors' | 'displays' | 'communication' | 'games' | 'robotics'; category: 'basics' | 'sensors' | 'displays' | 'communication' | 'games' | 'robotics';
difficulty: 'beginner' | 'intermediate' | 'advanced'; difficulty: 'beginner' | 'intermediate' | 'advanced';
/** Target board — defaults to 'arduino-uno' if omitted */ /** Target board — defaults to 'arduino-uno' if omitted */
boardType?: 'arduino-uno' | 'raspberry-pi-pico'; boardType?: 'arduino-uno' | 'arduino-nano' | 'raspberry-pi-pico';
code: string; code: string;
components: Array<{ components: Array<{
type: string; type: string;

View File

@ -7,15 +7,17 @@ import type { RP2040I2CDevice } from '../simulation/RP2040Simulator';
import type { Wire, WireInProgress, WireEndpoint } from '../types/wire'; import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
import { calculatePinPosition } from '../utils/pinPositionCalculator'; import { calculatePinPosition } from '../utils/pinPositionCalculator';
export type BoardType = 'arduino-uno' | 'raspberry-pi-pico'; export type BoardType = 'arduino-uno' | 'arduino-nano' | 'raspberry-pi-pico';
export const BOARD_FQBN: Record<BoardType, string> = { export const BOARD_FQBN: Record<BoardType, string> = {
'arduino-uno': 'arduino:avr:uno', 'arduino-uno': 'arduino:avr:uno',
'arduino-nano': 'arduino:avr:nano:cpu=atmega328',
'raspberry-pi-pico': 'rp2040:rp2040:rpipico', 'raspberry-pi-pico': 'rp2040:rp2040:rpipico',
}; };
export const BOARD_LABELS: Record<BoardType, string> = { export const BOARD_LABELS: Record<BoardType, string> = {
'arduino-uno': 'Arduino Uno', 'arduino-uno': 'Arduino Uno',
'arduino-nano': 'Arduino Nano',
'raspberry-pi-pico': 'Raspberry Pi Pico', 'raspberry-pi-pico': 'Raspberry Pi Pico',
}; };
@ -184,7 +186,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
if (running) { if (running) {
get().stopSimulation(); get().stopSimulation();
} }
const simulator = type === 'arduino-uno' const simulator = (type === 'arduino-uno' || type === 'arduino-nano')
? new AVRSimulator(pinManager) ? new AVRSimulator(pinManager)
: new RP2040Simulator(pinManager); : new RP2040Simulator(pinManager);
// Wire serial output callback for both simulator types // Wire serial output callback for both simulator types
@ -200,7 +202,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
initSimulator: () => { initSimulator: () => {
const { boardType } = get(); const { boardType } = get();
const simulator = boardType === 'arduino-uno' const simulator = (boardType === 'arduino-uno' || boardType === 'arduino-nano')
? new AVRSimulator(pinManager) ? new AVRSimulator(pinManager)
: new RP2040Simulator(pinManager); : new RP2040Simulator(pinManager);
// Wire serial output callback for both simulator types // Wire serial output callback for both simulator types

View File

@ -51,7 +51,7 @@ const ARDUINO_UNO_ANALOG_MAP: Record<string, number> = {
}; };
/** All known board component IDs in the simulator */ /** All known board component IDs in the simulator */
export const BOARD_COMPONENT_IDS = ['arduino-uno', 'nano-rp2040']; export const BOARD_COMPONENT_IDS = ['arduino-uno', 'arduino-nano', 'nano-rp2040'];
/** /**
* Check whether a componentId represents a board (not an external component). * Check whether a componentId represents a board (not an external component).
@ -69,7 +69,7 @@ export function isBoardComponent(componentId: string): boolean {
* @returns Numeric pin/GPIO number, or null if unmapped * @returns Numeric pin/GPIO number, or null if unmapped
*/ */
export function boardPinToNumber(boardId: string, pinName: string): number | null { export function boardPinToNumber(boardId: string, pinName: string): number | null {
if (boardId === 'arduino-uno') { if (boardId === 'arduino-uno' || boardId === 'arduino-nano') {
// Try numeric (covers '0' through '13', also legacy examples using just numbers) // Try numeric (covers '0' through '13', also legacy examples using just numbers)
const num = parseInt(pinName, 10); const num = parseInt(pinName, 10);
if (!isNaN(num) && num >= 0 && num <= 21) return num; if (!isNaN(num) && num >= 0 && num <= 21) return num;

View File

@ -43,7 +43,7 @@ export interface VelxioComponent {
} }
export interface ImportResult { export interface ImportResult {
boardType: 'arduino-uno' | 'raspberry-pi-pico'; boardType: 'arduino-uno' | 'arduino-nano' | 'raspberry-pi-pico';
boardPosition: { x: number; y: number }; boardPosition: { x: number; y: number };
components: VelxioComponent[]; components: VelxioComponent[];
wires: Wire[]; wires: Wire[];
@ -53,9 +53,9 @@ export interface ImportResult {
// ── Board mappings ──────────────────────────────────────────────────────────── // ── Board mappings ────────────────────────────────────────────────────────────
// Wokwi board type → Velxio boardType // Wokwi board type → Velxio boardType
const WOKWI_TYPE_TO_BOARD: Record<string, 'arduino-uno' | 'raspberry-pi-pico'> = { const WOKWI_TYPE_TO_BOARD: Record<string, 'arduino-uno' | 'arduino-nano' | 'raspberry-pi-pico'> = {
'wokwi-arduino-uno': 'arduino-uno', 'wokwi-arduino-uno': 'arduino-uno',
'wokwi-arduino-nano': 'arduino-uno', 'wokwi-arduino-nano': 'arduino-nano',
'wokwi-arduino-mega': 'arduino-uno', 'wokwi-arduino-mega': 'arduino-uno',
'wokwi-raspberry-pi-pico': 'raspberry-pi-pico', 'wokwi-raspberry-pi-pico': 'raspberry-pi-pico',
}; };
@ -63,12 +63,14 @@ const WOKWI_TYPE_TO_BOARD: Record<string, 'arduino-uno' | 'raspberry-pi-pico'> =
// Velxio boardType → Wokwi type // Velxio boardType → Wokwi type
const BOARD_TO_WOKWI_TYPE: Record<string, string> = { const BOARD_TO_WOKWI_TYPE: Record<string, string> = {
'arduino-uno': 'wokwi-arduino-uno', 'arduino-uno': 'wokwi-arduino-uno',
'arduino-nano': 'wokwi-arduino-nano',
'raspberry-pi-pico': 'wokwi-raspberry-pi-pico', 'raspberry-pi-pico': 'wokwi-raspberry-pi-pico',
}; };
// Velxio boardType → default Wokwi part id // Velxio boardType → default Wokwi part id
const BOARD_TO_WOKWI_ID: Record<string, string> = { const BOARD_TO_WOKWI_ID: Record<string, string> = {
'arduino-uno': 'uno', 'arduino-uno': 'uno',
'arduino-nano': 'nano',
'raspberry-pi-pico': 'pico', 'raspberry-pi-pico': 'pico',
}; };
@ -140,8 +142,10 @@ export async function exportToWokwiZip(
// Build connections // Build connections
const connections: [string, string, string, string[]][] = wires.map((w) => { const connections: [string, string, string, string[]][] = wires.map((w) => {
const startId = w.start.componentId === 'arduino-uno' ? boardId : w.start.componentId; const isBoardStart = w.start.componentId === 'arduino-uno' || w.start.componentId === 'arduino-nano' || w.start.componentId === 'nano-rp2040';
const endId = w.end.componentId === 'arduino-uno' ? boardId : w.end.componentId; const isBoardEnd = w.end.componentId === 'arduino-uno' || w.end.componentId === 'arduino-nano' || w.end.componentId === 'nano-rp2040';
const startId = isBoardStart ? boardId : w.start.componentId;
const endId = isBoardEnd ? boardId : w.end.componentId;
return [ return [
`${startId}:${w.start.pinName}`, `${startId}:${w.start.pinName}`,
`${endId}:${w.end.pinName}`, `${endId}:${w.end.pinName}`,
@ -193,6 +197,14 @@ export async function importFromWokwiZip(file: File): Promise<ImportResult> {
const boardType = boardPart ? WOKWI_TYPE_TO_BOARD[boardPart.type] : 'arduino-uno'; const boardType = boardPart ? WOKWI_TYPE_TO_BOARD[boardPart.type] : 'arduino-uno';
const boardId = boardPart?.id ?? 'uno'; const boardId = boardPart?.id ?? 'uno';
// Velxio internal component ID for the board element (must match DOM element id)
const VELXIO_BOARD_ID: Record<string, string> = {
'arduino-uno': 'arduino-uno',
'arduino-nano': 'arduino-nano',
'raspberry-pi-pico': 'nano-rp2040',
};
const velxioBoardId = VELXIO_BOARD_ID[boardType] ?? 'arduino-uno';
// Board position from diagram (use directly as Velxio board position) // Board position from diagram (use directly as Velxio board position)
const boardPosition = { const boardPosition = {
x: boardPart?.left ?? 50, x: boardPart?.left ?? 50,
@ -221,8 +233,8 @@ export async function importFromWokwiZip(file: File): Promise<ImportResult> {
const endPin = colonB >= 0 ? endStr.slice(colonB + 1) : ''; const endPin = colonB >= 0 ? endStr.slice(colonB + 1) : '';
// Remap board part id → Velxio internal board id // Remap board part id → Velxio internal board id
const startId = startCompRaw === boardId ? 'arduino-uno' : startCompRaw; const startId = startCompRaw === boardId ? velxioBoardId : startCompRaw;
const endId = endCompRaw === boardId ? 'arduino-uno' : endCompRaw; const endId = endCompRaw === boardId ? velxioBoardId : endCompRaw;
return { return {
id: `wire-${i}-${Date.now()}`, id: `wire-${i}-${Date.now()}`,