feat: enhance RP2040 and AVR simulators with serial baud rate handling; update editor toolbar and library manager modal for improved state management and UI

pull/10/head
David Montero Crespo 2026-03-05 21:07:10 -03:00
parent 4ba2ccb877
commit 9b8747349f
10 changed files with 181 additions and 56 deletions

View File

@ -224,6 +224,13 @@ class ArduinoCLIService:
# arduino-cli requires sketch name to match directory name
sketch_file = sketch_dir / "sketch.ino"
# For RP2040 boards, redirect Serial (USB CDC) to Serial1 (UART0)
# The emulator captures UART0 output, but the arduino-pico core
# defaults Serial to USB CDC which isn't emulated.
if "rp2040" in board_fqbn:
code = "#define Serial Serial1\n" + code
sketch_file.write_text(code)
print(f"Created sketch file: {sketch_file}")

View File

@ -3,12 +3,18 @@ import { useEditorStore } from '../../store/useEditorStore';
import { useSimulatorStore, BOARD_FQBN, BOARD_LABELS } from '../../store/useSimulatorStore';
import { compileCode } from '../../services/compilation';
import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
import { CompilationConsole } from './CompilationConsole';
import { parseCompileResult } from '../../utils/compilationLogger';
import type { CompilationLog } from '../../utils/compilationLogger';
import './EditorToolbar.css';
export const EditorToolbar = () => {
interface EditorToolbarProps {
consoleOpen: boolean;
setConsoleOpen: (open: boolean | ((v: boolean) => boolean)) => void;
compileLogs: CompilationLog[];
setCompileLogs: (logs: CompilationLog[] | ((prev: CompilationLog[]) => CompilationLog[])) => void;
}
export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compileLogs, setCompileLogs }: EditorToolbarProps) => {
const { code } = useEditorStore();
const {
boardType,
@ -23,12 +29,10 @@ export const EditorToolbar = () => {
const [compiling, setCompiling] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [libManagerOpen, setLibManagerOpen] = useState(false);
const [consoleOpen, setConsoleOpen] = useState(false);
const [compileLogs, setCompileLogs] = useState<CompilationLog[]>([]);
const addLog = useCallback((log: CompilationLog) => {
setCompileLogs((prev) => [...prev, log]);
}, []);
setCompileLogs((prev: CompilationLog[]) => [...prev, log]);
}, [setCompileLogs]);
const handleCompile = async () => {
setCompiling(true);
@ -45,7 +49,7 @@ export const EditorToolbar = () => {
// Parse the full result into log entries
const resultLogs = parseCompileResult(result, boardLabel);
setCompileLogs((prev) => [...prev, ...resultLogs]);
setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]);
if (result.success) {
if (result.hex_content) {
@ -203,18 +207,6 @@ export const EditorToolbar = () => {
<div className="toolbar-error-detail">{message.text}</div>
)}
{/* Compilation Console */}
{consoleOpen && (
<div style={{ height: 200, flexShrink: 0 }}>
<CompilationConsole
isOpen={consoleOpen}
onClose={() => setConsoleOpen(false)}
logs={compileLogs}
onClear={() => setCompileLogs([])}
/>
</div>
)}
<LibraryManagerModal isOpen={libManagerOpen} onClose={() => setLibManagerOpen(false)} />
</>
);

View File

@ -191,6 +191,12 @@
animation: spin 0.8s linear infinite;
}
.lib-spinner-center {
width: 32px;
height: 32px;
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);

View File

@ -33,26 +33,29 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
}
}, []);
// Fetch installed list when modal opens (to cross-reference in search tab)
// Reset state when modal closes
useEffect(() => {
if (isOpen) {
fetchInstalled();
if (!isOpen) {
setSearchQuery('');
setSearchResults([]);
setStatusMsg(null);
}
}, [isOpen, fetchInstalled]);
}, [isOpen]);
// Also refresh when switching to the installed tab
// Fetch installed list when modal opens or switching to installed tab
useEffect(() => {
if (isOpen && activeTab === 'installed') {
fetchInstalled();
}
if (isOpen && activeTab === 'installed') fetchInstalled();
}, [isOpen, activeTab, fetchInstalled]);
useEffect(() => {
if (!searchQuery.trim()) {
setSearchResults([]);
return;
}
if (isOpen) fetchInstalled();
}, [isOpen, fetchInstalled]);
// Search: immediate on open (empty query), debounced when typing
useEffect(() => {
if (!isOpen) return;
if (debounceRef.current) clearTimeout(debounceRef.current);
const delay = searchQuery ? 400 : 0;
debounceRef.current = setTimeout(async () => {
setLoadingSearch(true);
setStatusMsg(null);
@ -65,10 +68,10 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
} finally {
setLoadingSearch(false);
}
}, 500);
}, delay);
return () => { if (debounceRef.current) clearTimeout(debounceRef.current); };
}, [searchQuery]);
}, [searchQuery, isOpen]);
const handleInstall = async (libName: string) => {
setInstallingLib(libName);
@ -89,9 +92,6 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
};
const handleClose = () => {
setStatusMsg(null);
setSearchQuery('');
setSearchResults([]);
onClose();
};
@ -193,18 +193,20 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
</div>
<div className="lib-list">
{!searchQuery.trim() && (
{loadingSearch && (
<div className="lib-empty">
<p>Type a library name to search</p>
<p className="lib-empty-sub">e.g. "ArduinoJson", "Servo", "LiquidCrystal"</p>
<svg className="lib-spinner lib-spinner-center" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
<p className="lib-empty-sub">{searchQuery ? `Searching "${searchQuery}"…` : 'Loading libraries…'}</p>
</div>
)}
{searchQuery.trim() && searchResults.length === 0 && !loadingSearch && (
{!loadingSearch && searchResults.length === 0 && (
<div className="lib-empty">
<p>No libraries found for "{searchQuery}"</p>
<p>{searchQuery ? `No libraries found for "${searchQuery}"` : 'No libraries available'}</p>
</div>
)}
{searchResults.map((lib, i) => (
{!loadingSearch && searchResults.map((lib, i) => (
<div key={i} className="lib-item">
<div className="lib-item-info">
<div className="lib-item-header">

View File

@ -8,6 +8,7 @@ import { useSimulatorStore } from '../../store/useSimulatorStore';
export const SerialMonitor: React.FC = () => {
const serialOutput = useSimulatorStore((s) => s.serialOutput);
const serialBaudRate = useSimulatorStore((s) => s.serialBaudRate);
const running = useSimulatorStore((s) => s.running);
const serialWrite = useSimulatorStore((s) => s.serialWrite);
const clearSerialOutput = useSimulatorStore((s) => s.clearSerialOutput);
@ -49,6 +50,9 @@ export const SerialMonitor: React.FC = () => {
<div style={styles.header}>
<span style={styles.title}>Serial Monitor</span>
<div style={styles.headerControls}>
{serialBaudRate > 0 && (
<span style={styles.baudRate}>{serialBaudRate.toLocaleString()} baud</span>
)}
<label style={styles.autoscrollLabel}>
<input
type="checkbox"
@ -127,6 +131,15 @@ const styles: Record<string, React.CSSProperties> = {
alignItems: 'center',
gap: 8,
},
baudRate: {
color: '#569cd6',
fontSize: 11,
fontFamily: 'monospace',
background: '#1e1e1e',
border: '1px solid #3a3a3a',
borderRadius: 3,
padding: '1px 6px',
},
autoscrollLabel: {
color: '#999',
fontSize: 11,

View File

@ -6,18 +6,35 @@ import React, { useRef, useState, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { CodeEditor } from '../components/editor/CodeEditor';
import { EditorToolbar } from '../components/editor/EditorToolbar';
import { CompilationConsole } from '../components/editor/CompilationConsole';
import { SimulatorCanvas } from '../components/simulator/SimulatorCanvas';
import { SerialMonitor } from '../components/simulator/SerialMonitor';
import { useSimulatorStore } from '../store/useSimulatorStore';
import type { CompilationLog } from '../utils/compilationLogger';
import '../App.css';
const BOTTOM_PANEL_MIN = 80;
const BOTTOM_PANEL_MAX = 600;
const BOTTOM_PANEL_DEFAULT = 200;
const resizeHandleStyle: React.CSSProperties = {
height: 5,
flexShrink: 0,
cursor: 'row-resize',
background: '#2a2d2e',
borderTop: '1px solid #3c3c3c',
borderBottom: '1px solid #3c3c3c',
};
export const EditorPage: React.FC = () => {
const [editorWidthPct, setEditorWidthPct] = useState(45);
const containerRef = useRef<HTMLDivElement>(null);
const resizingRef = useRef(false);
const serialMonitorOpen = useSimulatorStore((s) => s.serialMonitorOpen);
const toggleSerialMonitor = useSimulatorStore((s) => s.toggleSerialMonitor);
const [serialHeightPct, setSerialHeightPct] = useState(30);
const [consoleOpen, setConsoleOpen] = useState(false);
const [compileLogs, setCompileLogs] = useState<CompilationLog[]>([]);
const [bottomPanelHeight, setBottomPanelHeight] = useState(BOTTOM_PANEL_DEFAULT);
const handleResizeMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
@ -44,6 +61,27 @@ export const EditorPage: React.FC = () => {
document.addEventListener('mouseup', handleMouseUp);
}, []);
const handleBottomPanelResizeMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const startY = e.clientY;
const startHeight = bottomPanelHeight;
const onMove = (ev: MouseEvent) => {
const delta = startY - ev.clientY;
setBottomPanelHeight(Math.max(BOTTOM_PANEL_MIN, Math.min(BOTTOM_PANEL_MAX, startHeight + delta)));
};
const onUp = () => {
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.body.style.cursor = 'row-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}, [bottomPanelHeight]);
return (
<div className="app">
<header className="app-header">
@ -90,11 +128,33 @@ export const EditorPage: React.FC = () => {
</header>
<div className="app-container" ref={containerRef}>
<div className="editor-panel" style={{ width: `${editorWidthPct}%` }}>
<EditorToolbar />
<div className="editor-wrapper">
<div className="editor-panel" style={{ width: `${editorWidthPct}%`, display: 'flex', flexDirection: 'column' }}>
<EditorToolbar
consoleOpen={consoleOpen}
setConsoleOpen={setConsoleOpen}
compileLogs={compileLogs}
setCompileLogs={setCompileLogs}
/>
<div className="editor-wrapper" style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
<CodeEditor />
</div>
{consoleOpen && (
<>
<div
onMouseDown={handleBottomPanelResizeMouseDown}
style={resizeHandleStyle}
title="Drag to resize"
/>
<div style={{ height: bottomPanelHeight, flexShrink: 0 }}>
<CompilationConsole
isOpen={consoleOpen}
onClose={() => setConsoleOpen(false)}
logs={compileLogs}
onClear={() => setCompileLogs([])}
/>
</div>
</>
)}
</div>
{/* Resize handle */}
@ -103,13 +163,20 @@ export const EditorPage: React.FC = () => {
</div>
<div className="simulator-panel" style={{ width: `${100 - editorWidthPct}%`, display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: serialMonitorOpen ? `0 0 ${100 - serialHeightPct}%` : '1 1 auto', overflow: 'hidden', position: 'relative' }}>
<div style={{ flex: 1, overflow: 'hidden', position: 'relative', minHeight: 0 }}>
<SimulatorCanvas />
</div>
{serialMonitorOpen && (
<div style={{ flex: `0 0 ${serialHeightPct}%`, minHeight: 100, display: 'flex', flexDirection: 'column' }}>
<SerialMonitor />
</div>
<>
<div
onMouseDown={handleBottomPanelResizeMouseDown}
style={resizeHandleStyle}
title="Drag to resize"
/>
<div style={{ height: bottomPanelHeight, flexShrink: 0 }}>
<SerialMonitor />
</div>
</>
)}
</div>
</div>

View File

@ -47,6 +47,8 @@ export class AVRSimulator {
/** Serial output buffer — subscribers receive each byte or line */
public onSerialData: ((char: string) => void) | null = null;
/** Fires whenever the sketch changes Serial baud rate (Serial.begin) */
public onBaudRateChange: ((baudRate: number) => void) | null = null;
private lastPortBValue = 0;
private lastPortCValue = 0;
private lastPortDValue = 0;
@ -94,6 +96,11 @@ export class AVRSimulator {
this.onSerialData(String.fromCharCode(value));
}
};
this.usart.onConfigurationChange = () => {
if (this.onBaudRateChange && this.usart) {
this.onBaudRateChange(this.usart.baudRate);
}
};
// TWI (I2C)
this.twi = new AVRTWI(this.cpu, twiConfig, 16000000);
@ -262,6 +269,11 @@ export class AVRSimulator {
this.usart.onByteTransmit = (value: number) => {
if (this.onSerialData) this.onSerialData(String.fromCharCode(value));
};
this.usart.onConfigurationChange = () => {
if (this.onBaudRateChange && this.usart) {
this.onBaudRateChange(this.usart.baudRate);
}
};
this.twi = new AVRTWI(this.cpu, twiConfig, 16000000);
this.i2cBus = new I2CBusManager(this.twi);

View File

@ -115,9 +115,16 @@ export class RP2040Simulator {
this.rp2040.core.PC = 0x10000000;
// ── Wire UART0 (default Serial port for Arduino-Pico) ────────────
let serialBuffer = '';
this.rp2040.uart[0].onByte = (value: number) => {
const ch = String.fromCharCode(value);
serialBuffer += ch;
if (ch === '\n') {
console.log('[RP2040 UART0]', serialBuffer.trimEnd());
serialBuffer = '';
}
if (this.onSerialData) {
this.onSerialData(String.fromCharCode(value));
this.onSerialData(ch);
}
};

View File

@ -22,9 +22,11 @@ function setAdcVoltage(simulator: AnySimulator, pin: number, voltage: number): b
const channel = pin - 26;
// RP2040 ADC: 12-bit, 3.3V reference
const adcValue = Math.round((voltage / 3.3) * 4095);
console.log(`[setAdcVoltage] RP2040 ch${channel} = ${adcValue} (${voltage.toFixed(3)}V)`);
simulator.setADCValue(channel, adcValue);
return true;
}
console.warn(`[setAdcVoltage] RP2040 pin ${pin} is not an ADC pin (26-29)`);
return false;
}
// AVR: pins 14-19 → ADC channels 0-5
@ -94,15 +96,21 @@ PartSimulationRegistry.register('rgb-led', {
PartSimulationRegistry.register('potentiometer', {
attachEvents: (element, simulator, getArduinoPinHelper) => {
const pin = getArduinoPinHelper('SIG');
if (pin === null) return () => { };
console.log(`[Potentiometer] attachEvents called, SIG pin resolved to: ${pin}`);
if (pin === null) {
console.warn('[Potentiometer] No SIG pin found — skipping ADC attachment');
return () => { };
}
// Determine reference voltage based on board type
const isRP2040 = simulator instanceof RP2040Simulator;
const refVoltage = isRP2040 ? 3.3 : 5.0;
console.log(`[Potentiometer] Board type: ${isRP2040 ? 'RP2040' : 'AVR'}, refV: ${refVoltage}`);
const onInput = () => {
const raw = parseInt((element as any).value || '0', 10);
const volts = (raw / 1023.0) * refVoltage;
console.log(`[Potentiometer] pin=${pin}, raw=${raw}, volts=${volts.toFixed(3)}`);
if (!setAdcVoltage(simulator, pin, volts)) {
console.warn(`[Potentiometer] ADC not available for pin ${pin}`);
}

View File

@ -51,6 +51,7 @@ interface SimulatorState {
// Serial monitor state
serialOutput: string;
serialBaudRate: number;
serialMonitorOpen: boolean;
// Actions
@ -164,6 +165,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
selectedWireId: null,
wireInProgress: null,
serialOutput: '',
serialBaudRate: 0,
serialMonitorOpen: false,
setBoardType: (type: BoardType) => {
@ -178,7 +180,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
set({ boardType: type, simulator, compiledHex: null, serialOutput: '' });
if (simulator instanceof AVRSimulator) {
simulator.onBaudRateChange = (baudRate: number) => set({ serialBaudRate: baudRate });
}
set({ boardType: type, simulator, compiledHex: null, serialOutput: '', serialBaudRate: 0 });
console.log(`Board switched to: ${type}`);
},
@ -191,7 +196,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
set({ simulator, serialOutput: '' });
if (simulator instanceof AVRSimulator) {
simulator.onBaudRateChange = (baudRate: number) => set({ serialBaudRate: baudRate });
}
set({ simulator, serialOutput: '', serialBaudRate: 0 });
console.log(`Simulator initialized: ${boardType}`);
},
@ -239,7 +247,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
simulator.addI2CDevice(new I2CMemoryDevice(0x50) as RP2040I2CDevice);
}
simulator.start();
set({ running: true });
set({ running: true, serialMonitorOpen: true });
}
},
@ -259,7 +267,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
set({ running: false, serialOutput: '' });
if (simulator instanceof AVRSimulator) {
simulator.onBaudRateChange = (baudRate: number) => set({ serialBaudRate: baudRate });
}
set({ running: false, serialOutput: '', serialBaudRate: 0 });
}
},