feat: add Serial Monitor component and integrate with AVR simulator

- Implemented SerialMonitor component to display serial output and allow user input.
- Enhanced AVRSimulator to handle USART communication and transmit serial data.
- Updated useSimulatorStore to manage serial output state and toggle visibility of the Serial Monitor.
- Added example Arduino sketches for serial communication, including Serial Echo and Serial LED Control.
- Introduced I2CBusManager to manage virtual I2C devices and integrated with AVRSimulator.
pull/10/head
David Montero Crespo 2026-03-05 06:56:14 -03:00
parent 13cf7be465
commit 5d175abdcf
6 changed files with 1152 additions and 8 deletions

View File

@ -0,0 +1,201 @@
/**
* Serial Monitor shows Arduino Serial output and allows sending data back.
* Connects to the AVRSimulator USART via the Zustand store.
*/
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { useSimulatorStore } from '../../store/useSimulatorStore';
export const SerialMonitor: React.FC = () => {
const serialOutput = useSimulatorStore((s) => s.serialOutput);
const running = useSimulatorStore((s) => s.running);
const serialWrite = useSimulatorStore((s) => s.serialWrite);
const clearSerialOutput = useSimulatorStore((s) => s.clearSerialOutput);
const [inputValue, setInputValue] = useState('');
const [lineEnding, setLineEnding] = useState<'none' | 'nl' | 'cr' | 'both'>('nl');
const [autoscroll, setAutoscroll] = useState(true);
const outputRef = useRef<HTMLPreElement>(null);
// Auto-scroll to bottom when new output arrives
useEffect(() => {
if (autoscroll && outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight;
}
}, [serialOutput, autoscroll]);
const handleSend = useCallback(() => {
if (!inputValue && lineEnding === 'none') return;
let text = inputValue;
switch (lineEnding) {
case 'nl': text += '\n'; break;
case 'cr': text += '\r'; break;
case 'both': text += '\r\n'; break;
}
serialWrite(text);
setInputValue('');
}, [inputValue, lineEnding, serialWrite]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSend();
}
}, [handleSend]);
return (
<div style={styles.container}>
{/* Header */}
<div style={styles.header}>
<span style={styles.title}>Serial Monitor</span>
<div style={styles.headerControls}>
<label style={styles.autoscrollLabel}>
<input
type="checkbox"
checked={autoscroll}
onChange={(e) => setAutoscroll(e.target.checked)}
style={styles.checkbox}
/>
Autoscroll
</label>
<button onClick={clearSerialOutput} style={styles.clearBtn} title="Clear output">
Clear
</button>
</div>
</div>
{/* Output area */}
<pre ref={outputRef} style={styles.output}>
{serialOutput || (running ? 'Waiting for serial data...\n' : 'Start simulation to see serial output.\n')}
</pre>
{/* Input row */}
<div style={styles.inputRow}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type message to send..."
style={styles.input}
disabled={!running}
/>
<select
value={lineEnding}
onChange={(e) => setLineEnding(e.target.value as typeof lineEnding)}
style={styles.select}
>
<option value="none">No line ending</option>
<option value="nl">Newline</option>
<option value="cr">Carriage return</option>
<option value="both">Both NL &amp; CR</option>
</select>
<button onClick={handleSend} disabled={!running} style={styles.sendBtn}>
Send
</button>
</div>
</div>
);
};
const styles: Record<string, React.CSSProperties> = {
container: {
display: 'flex',
flexDirection: 'column',
height: '100%',
background: '#1e1e1e',
borderTop: '1px solid #333',
fontFamily: 'monospace',
fontSize: 13,
},
header: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '4px 8px',
background: '#252526',
borderBottom: '1px solid #333',
minHeight: 28,
},
title: {
color: '#cccccc',
fontWeight: 600,
fontSize: 12,
},
headerControls: {
display: 'flex',
alignItems: 'center',
gap: 8,
},
autoscrollLabel: {
color: '#999',
fontSize: 11,
display: 'flex',
alignItems: 'center',
gap: 4,
cursor: 'pointer',
},
checkbox: {
margin: 0,
cursor: 'pointer',
},
clearBtn: {
background: 'transparent',
border: '1px solid #555',
color: '#ccc',
padding: '2px 8px',
borderRadius: 3,
cursor: 'pointer',
fontSize: 11,
},
output: {
flex: 1,
margin: 0,
padding: 8,
color: '#00ff41',
background: '#0a0a0a',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
minHeight: 0,
fontSize: 13,
lineHeight: '1.4',
},
inputRow: {
display: 'flex',
gap: 4,
padding: 4,
background: '#252526',
borderTop: '1px solid #333',
},
input: {
flex: 1,
background: '#1e1e1e',
border: '1px solid #444',
color: '#ccc',
padding: '4px 8px',
borderRadius: 3,
fontFamily: 'monospace',
fontSize: 12,
outline: 'none',
},
select: {
background: '#1e1e1e',
border: '1px solid #444',
color: '#ccc',
padding: '4px',
borderRadius: 3,
fontSize: 11,
outline: 'none',
},
sendBtn: {
background: '#0e639c',
border: 'none',
color: '#fff',
padding: '4px 12px',
borderRadius: 3,
cursor: 'pointer',
fontSize: 12,
fontWeight: 600,
},
};

View File

@ -761,6 +761,595 @@ void loop() {
// Power / Contrast logic is usually handled internally or ignored in basic simulation
],
},
// ─── Protocol Test Examples ──────────────────────────────────────────────
{
id: 'serial-echo',
title: 'Serial Echo (USART)',
description: 'Tests Serial communication: echoes typed characters back and prints status. Open the Serial Monitor to interact.',
category: 'communication',
difficulty: 'beginner',
code: `// Serial Echo — USART Protocol Test
// Open the Serial Monitor to send and receive data.
// Everything you type is echoed back with extra info.
void setup() {
Serial.begin(9600);
Serial.println("=============================");
Serial.println(" Serial Echo Test (USART)");
Serial.println("=============================");
Serial.println("Type something and press Send.");
Serial.println();
// Print system info
Serial.print("CPU Clock: ");
Serial.print(F_CPU / 1000000);
Serial.println(" MHz");
Serial.print("Baud rate: 9600");
Serial.println();
Serial.println();
}
unsigned long charCount = 0;
void loop() {
if (Serial.available() > 0) {
char c = Serial.read();
charCount++;
Serial.print("[");
Serial.print(charCount);
Serial.print("] Received: '");
Serial.print(c);
Serial.print("' (ASCII ");
Serial.print((int)c);
Serial.println(")");
}
// Periodic heartbeat
static unsigned long lastBeat = 0;
if (millis() - lastBeat >= 5000) {
lastBeat = millis();
Serial.print("Uptime: ");
Serial.print(millis() / 1000);
Serial.print("s | Chars received: ");
Serial.println(charCount);
}
}
`,
components: [
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
],
wires: [],
},
{
id: 'serial-led-control',
title: 'Serial LED Control',
description: 'Control an LED via Serial commands: send "1" or "0". Tests USART RX + GPIO output together.',
category: 'communication',
difficulty: 'beginner',
code: `// Serial LED Control
// Send "1" to turn LED ON, "0" to turn LED OFF.
// Demonstrates Serial input controlling hardware.
const int LED_PIN = 13;
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
Serial.println("=========================");
Serial.println(" Serial LED Controller");
Serial.println("=========================");
Serial.println("Send '1' = LED ON");
Serial.println("Send '0' = LED OFF");
Serial.println("Send '?' = Status");
Serial.println();
}
bool ledState = false;
void loop() {
if (Serial.available() > 0) {
char cmd = Serial.read();
switch (cmd) {
case '1':
digitalWrite(LED_PIN, HIGH);
ledState = true;
Serial.println("[OK] LED is ON");
break;
case '0':
digitalWrite(LED_PIN, LOW);
ledState = false;
Serial.println("[OK] LED is OFF");
break;
case '?':
Serial.print("[STATUS] LED is ");
Serial.println(ledState ? "ON" : "OFF");
Serial.print("[STATUS] Uptime: ");
Serial.print(millis() / 1000);
Serial.println("s");
break;
default:
if (cmd >= 32) { // ignore control chars
Serial.print("[ERR] Unknown command: '");
Serial.print(cmd);
Serial.println("' (use 1, 0, or ?)");
}
break;
}
}
}
`,
components: [
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
{ type: 'wokwi-led', id: 'led-1', x: 400, y: 120, properties: { color: 'green' } },
],
wires: [
{ id: 'w-led', start: { componentId: 'arduino-uno', pinName: '13' }, end: { componentId: 'led-1', pinName: 'A' }, color: '#00cc00' },
],
},
{
id: 'i2c-scanner',
title: 'I2C Scanner (TWI)',
description: 'Scans the I2C bus and reports all devices found. Tests TWI protocol. Virtual devices at 0x48, 0x50, 0x68 should be detected.',
category: 'communication',
difficulty: 'intermediate',
code: `// I2C Bus Scanner — TWI Protocol Test
// Scans all 127 I2C addresses and reports which ones respond with ACK.
// The emulator has virtual devices at:
// 0x48 = Temperature sensor
// 0x50 = EEPROM
// 0x68 = DS1307 RTC
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
Serial.println("===========================");
Serial.println(" I2C Bus Scanner (TWI)");
Serial.println("===========================");
Serial.println("Scanning...");
Serial.println();
int devicesFound = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
byte error = Wire.endTransmission();
if (error == 0) {
Serial.print(" Device found at 0x");
if (addr < 16) Serial.print("0");
Serial.print(addr, HEX);
// Identify known addresses
switch (addr) {
case 0x27: Serial.print(" (PCF8574 LCD backpack)"); break;
case 0x3C: Serial.print(" (SSD1306 OLED)"); break;
case 0x48: Serial.print(" (Temperature sensor)"); break;
case 0x50: Serial.print(" (EEPROM)"); break;
case 0x68: Serial.print(" (DS1307 RTC)"); break;
case 0x76: Serial.print(" (BME280 sensor)"); break;
case 0x77: Serial.print(" (BMP180/BMP280)"); break;
}
Serial.println();
devicesFound++;
}
}
Serial.println();
Serial.print("Scan complete. ");
Serial.print(devicesFound);
Serial.println(" device(s) found.");
if (devicesFound == 0) {
Serial.println("No I2C devices found. Check connections.");
}
}
void loop() {
// Rescan every 10 seconds
delay(10000);
Serial.println("\\nRescanning...");
setup();
}
`,
components: [
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
],
wires: [],
},
{
id: 'i2c-rtc-read',
title: 'I2C RTC Clock (DS1307)',
description: 'Reads time from a virtual DS1307 RTC via I2C and prints it to Serial. Tests TWI read transactions.',
category: 'communication',
difficulty: 'intermediate',
code: `// I2C RTC Reader — DS1307 at address 0x68
// Reads hours:minutes:seconds from the virtual RTC
// and prints to Serial Monitor every second.
#include <Wire.h>
#define DS1307_ADDR 0x68
byte bcdToDec(byte val) {
return ((val >> 4) * 10) + (val & 0x0F);
}
void setup() {
Wire.begin();
Serial.begin(9600);
Serial.println("===========================");
Serial.println(" DS1307 RTC Reader (I2C)");
Serial.println("===========================");
Serial.println();
}
void loop() {
// Set register pointer to 0 (seconds)
Wire.beginTransmission(DS1307_ADDR);
Wire.write(0x00);
Wire.endTransmission();
// Request 7 bytes: sec, min, hr, dow, date, month, year
Wire.requestFrom(DS1307_ADDR, 7);
if (Wire.available() >= 7) {
byte sec = bcdToDec(Wire.read() & 0x7F);
byte min = bcdToDec(Wire.read());
byte hr = bcdToDec(Wire.read() & 0x3F);
byte dow = bcdToDec(Wire.read());
byte date = bcdToDec(Wire.read());
byte month = bcdToDec(Wire.read());
byte year = bcdToDec(Wire.read());
// Print formatted time
Serial.print("Time: ");
if (hr < 10) Serial.print("0");
Serial.print(hr);
Serial.print(":");
if (min < 10) Serial.print("0");
Serial.print(min);
Serial.print(":");
if (sec < 10) Serial.print("0");
Serial.print(sec);
Serial.print(" Date: ");
if (date < 10) Serial.print("0");
Serial.print(date);
Serial.print("/");
if (month < 10) Serial.print("0");
Serial.print(month);
Serial.print("/20");
if (year < 10) Serial.print("0");
Serial.println(year);
} else {
Serial.println("Error: Could not read RTC");
}
delay(1000);
}
`,
components: [
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
],
wires: [],
},
{
id: 'i2c-eeprom-rw',
title: 'I2C EEPROM Read/Write',
description: 'Writes data to a virtual I2C EEPROM (0x50) and reads it back. Tests TWI write+read transactions.',
category: 'communication',
difficulty: 'intermediate',
code: `// I2C EEPROM Read/Write Test
// Virtual EEPROM at address 0x50
// Writes values to registers, then reads them back.
#include <Wire.h>
#define EEPROM_ADDR 0x50
void writeEEPROM(byte reg, byte value) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg); // register address
Wire.write(value); // data
Wire.endTransmission();
delay(5); // EEPROM write cycle time
}
byte readEEPROM(byte reg) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom(EEPROM_ADDR, 1);
if (Wire.available()) {
return Wire.read();
}
return 0xFF;
}
void setup() {
Wire.begin();
Serial.begin(9600);
Serial.println("============================");
Serial.println(" I2C EEPROM R/W Test (0x50)");
Serial.println("============================");
Serial.println();
// Write test pattern
Serial.println("Writing test data...");
for (byte i = 0; i < 8; i++) {
byte value = (i + 1) * 10; // 10, 20, 30, ...
writeEEPROM(i, value);
Serial.print(" Write reg[");
Serial.print(i);
Serial.print("] = ");
Serial.println(value);
}
Serial.println();
Serial.println("Reading back...");
// Read back and verify
byte errors = 0;
for (byte i = 0; i < 8; i++) {
byte expected = (i + 1) * 10;
byte actual = readEEPROM(i);
Serial.print(" Read reg[");
Serial.print(i);
Serial.print("] = ");
Serial.print(actual);
if (actual == expected) {
Serial.println(" [OK]");
} else {
Serial.print(" [FAIL] expected ");
Serial.println(expected);
errors++;
}
}
Serial.println();
if (errors == 0) {
Serial.println("All tests PASSED!");
} else {
Serial.print(errors);
Serial.println(" test(s) FAILED.");
}
}
void loop() {
// Nothing to do
delay(1000);
}
`,
components: [
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
],
wires: [],
},
{
id: 'spi-loopback',
title: 'SPI Loopback Test',
description: 'Tests SPI by sending bytes and reading responses. Demonstrates MOSI/MISO/SCK/SS protocol.',
category: 'communication',
difficulty: 'intermediate',
code: `// SPI Loopback Test
// Sends bytes via SPI and logs the exchange.
// Without a physical slave, the emulator returns the sent byte.
#include <SPI.h>
#define SS_PIN 10
void setup() {
Serial.begin(9600);
Serial.println("========================");
Serial.println(" SPI Protocol Test");
Serial.println("========================");
Serial.println();
pinMode(SS_PIN, OUTPUT);
digitalWrite(SS_PIN, HIGH);
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV16);
Serial.println("SPI initialized.");
Serial.print("Clock divider: 16 (");
Serial.print(F_CPU / 16);
Serial.println(" Hz)");
Serial.println();
// Send test pattern
Serial.println("Sending test pattern via SPI:");
byte testData[] = {0xAA, 0x55, 0xFF, 0x00, 0x42, 0xDE, 0xAD, 0xBE};
digitalWrite(SS_PIN, LOW); // Select slave
for (int i = 0; i < sizeof(testData); i++) {
byte sent = testData[i];
byte received = SPI.transfer(sent);
Serial.print(" TX: 0x");
if (sent < 16) Serial.print("0");
Serial.print(sent, HEX);
Serial.print(" RX: 0x");
if (received < 16) Serial.print("0");
Serial.print(received, HEX);
if (sent == received) {
Serial.println(" (loopback OK)");
} else {
Serial.println();
}
}
digitalWrite(SS_PIN, HIGH); // Deselect slave
Serial.println();
Serial.println("SPI test complete.");
}
void loop() {
delay(1000);
}
`,
components: [
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
],
wires: [],
},
{
id: 'multi-protocol',
title: 'Multi-Protocol Demo',
description: 'Uses Serial + I2C + SPI together. Reads RTC via I2C, sends SPI data, and logs everything to Serial.',
category: 'communication',
difficulty: 'advanced',
code: `// Multi-Protocol Demo: Serial + I2C + SPI
// Demonstrates all three major communication protocols
// working together in a single sketch.
#include <Wire.h>
#include <SPI.h>
#define DS1307_ADDR 0x68
#define EEPROM_ADDR 0x50
#define SS_PIN 10
byte bcdToDec(byte val) {
return ((val >> 4) * 10) + (val & 0x0F);
}
void readRTC(byte &hr, byte &min, byte &sec) {
Wire.beginTransmission(DS1307_ADDR);
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDR, 3);
sec = bcdToDec(Wire.read() & 0x7F);
min = bcdToDec(Wire.read());
hr = bcdToDec(Wire.read() & 0x3F);
}
void writeEEPROM(byte reg, byte value) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.write(value);
Wire.endTransmission();
delay(5);
}
byte readEEPROM(byte reg) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom(EEPROM_ADDR, 1);
return Wire.available() ? Wire.read() : 0xFF;
}
byte spiTransfer(byte data) {
digitalWrite(SS_PIN, LOW);
byte result = SPI.transfer(data);
digitalWrite(SS_PIN, HIGH);
return result;
}
void setup() {
Serial.begin(9600);
Wire.begin();
pinMode(SS_PIN, OUTPUT);
digitalWrite(SS_PIN, HIGH);
SPI.begin();
Serial.println("===================================");
Serial.println(" Multi-Protocol Demo");
Serial.println(" Serial (USART) + I2C (TWI) + SPI");
Serial.println("===================================");
Serial.println();
// ── I2C: Scan bus ──
Serial.println("[I2C] Scanning bus...");
int found = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.print(" Found device at 0x");
if (addr < 16) Serial.print("0");
Serial.println(addr, HEX);
found++;
}
}
Serial.print(" ");
Serial.print(found);
Serial.println(" device(s) on I2C bus.");
Serial.println();
// ── I2C: Write/read EEPROM ──
Serial.println("[I2C] EEPROM write/read test:");
writeEEPROM(0, 42);
writeEEPROM(1, 99);
byte v0 = readEEPROM(0);
byte v1 = readEEPROM(1);
Serial.print(" Wrote 42, read ");
Serial.print(v0);
Serial.println(v0 == 42 ? " [OK]" : " [FAIL]");
Serial.print(" Wrote 99, read ");
Serial.print(v1);
Serial.println(v1 == 99 ? " [OK]" : " [FAIL]");
Serial.println();
// ── SPI: Transfer test ──
Serial.println("[SPI] Transfer test:");
byte spiData[] = {0xAA, 0x55, 0x42};
for (int i = 0; i < 3; i++) {
byte rx = spiTransfer(spiData[i]);
Serial.print(" TX=0x");
if (spiData[i] < 16) Serial.print("0");
Serial.print(spiData[i], HEX);
Serial.print(" RX=0x");
if (rx < 16) Serial.print("0");
Serial.println(rx, HEX);
}
Serial.println();
Serial.println("Setup complete. Reading RTC...");
Serial.println();
}
void loop() {
// ── Serial: Print RTC time every 2 seconds ──
byte hr, min, sec;
readRTC(hr, min, sec);
Serial.print("[RTC] ");
if (hr < 10) Serial.print("0");
Serial.print(hr);
Serial.print(":");
if (min < 10) Serial.print("0");
Serial.print(min);
Serial.print(":");
if (sec < 10) Serial.print("0");
Serial.print(sec);
Serial.print(" | Uptime: ");
Serial.print(millis() / 1000);
Serial.println("s");
delay(2000);
}
`,
components: [
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
],
wires: [],
},
];
// Get examples by category

View File

@ -7,12 +7,17 @@ import { Link } from 'react-router-dom';
import { CodeEditor } from '../components/editor/CodeEditor';
import { EditorToolbar } from '../components/editor/EditorToolbar';
import { SimulatorCanvas } from '../components/simulator/SimulatorCanvas';
import { SerialMonitor } from '../components/simulator/SerialMonitor';
import { useSimulatorStore } from '../store/useSimulatorStore';
import '../App.css';
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 handleResizeMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
@ -58,6 +63,29 @@ export const EditorPage: React.FC = () => {
</svg>
Examples
</Link>
<button
onClick={toggleSerialMonitor}
className="serial-monitor-toggle"
title="Toggle Serial Monitor"
style={{
background: serialMonitorOpen ? '#0e639c' : 'transparent',
border: '1px solid #555',
color: '#ccc',
padding: '4px 10px',
borderRadius: 4,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
fontSize: 13,
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg>
Serial
</button>
</div>
</header>
@ -74,8 +102,15 @@ export const EditorPage: React.FC = () => {
<div className="resize-handle-grip" />
</div>
<div className="simulator-panel" style={{ width: `${100 - editorWidthPct}%` }}>
<SimulatorCanvas />
<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' }}>
<SimulatorCanvas />
</div>
{serialMonitorOpen && (
<div style={{ flex: `0 0 ${serialHeightPct}%`, minHeight: 100, display: 'flex', flexDirection: 'column' }}>
<SerialMonitor />
</div>
)}
</div>
</div>
</div>

View File

@ -1,6 +1,8 @@
import { CPU, AVRTimer, timer0Config, timer1Config, timer2Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, AVRADC, adcConfig, AVRSPI, spiConfig } from 'avr8js';
import { CPU, AVRTimer, timer0Config, timer1Config, timer2Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, AVRADC, adcConfig, AVRSPI, spiConfig, AVRTWI, twiConfig } from 'avr8js';
import { PinManager } from './PinManager';
import { hexToUint8Array } from '../utils/hexParser';
import { I2CBusManager } from './I2CBusManager';
import type { I2CDevice } from './I2CBusManager';
/**
* AVRSimulator - Emulates Arduino Uno (ATmega328p) using avr8js
@ -34,11 +36,17 @@ export class AVRSimulator {
private portD: AVRIOPort | null = null;
private adc: AVRADC | null = null;
public spi: AVRSPI | null = null;
public usart: AVRUSART | null = null;
public twi: AVRTWI | null = null;
public i2cBus: I2CBusManager | null = null;
private program: Uint16Array | null = null;
private running = false;
private animationFrame: number | null = null;
public pinManager: PinManager;
private speed = 1.0; // Simulation speed multiplier
/** Serial output buffer — subscribers receive each byte or line */
public onSerialData: ((char: string) => void) | null = null;
private lastPortBValue = 0;
private lastPortCValue = 0;
private lastPortDValue = 0;
@ -79,12 +87,25 @@ export class AVRSimulator {
this.spi!.completeTransfer(value);
};
// USART (Serial) — hook onByteTransmit to forward output
this.usart = new AVRUSART(this.cpu, usart0Config, 16000000);
this.usart.onByteTransmit = (value: number) => {
if (this.onSerialData) {
this.onSerialData(String.fromCharCode(value));
}
};
// TWI (I2C)
this.twi = new AVRTWI(this.cpu, twiConfig, 16000000);
this.i2cBus = new I2CBusManager(this.twi);
this.peripherals = [
new AVRTimer(this.cpu, timer0Config),
new AVRTimer(this.cpu, timer1Config),
new AVRTimer(this.cpu, timer2Config),
new AVRUSART(this.cpu, usart0Config, 16000000),
this.usart,
this.spi,
this.twi,
];
// Initialize ADC (analogRead support)
@ -233,11 +254,25 @@ export class AVRSimulator {
console.log('Resetting AVR CPU...');
this.cpu = new CPU(this.program);
this.spi = new AVRSPI(this.cpu, spiConfig, 16000000);
this.spi.onByte = (value) => { this.spi!.completeTransfer(value); };
this.usart = new AVRUSART(this.cpu, usart0Config, 16000000);
this.usart.onByteTransmit = (value: number) => {
if (this.onSerialData) this.onSerialData(String.fromCharCode(value));
};
this.twi = new AVRTWI(this.cpu, twiConfig, 16000000);
this.i2cBus = new I2CBusManager(this.twi);
this.peripherals = [
new AVRTimer(this.cpu, timer0Config),
new AVRTimer(this.cpu, timer1Config),
new AVRTimer(this.cpu, timer2Config),
new AVRUSART(this.cpu, usart0Config, 16000000),
this.usart,
this.spi,
this.twi,
];
this.adc = new AVRADC(this.cpu, adcConfig);
@ -287,4 +322,23 @@ export class AVRSimulator {
this.portC.setPin(arduinoPin - 14, state);
}
}
/**
* Send a byte to the Arduino serial port (RX) as if typed in the Serial Monitor.
*/
serialWrite(text: string): void {
if (!this.usart) return;
for (let i = 0; i < text.length; i++) {
this.usart.writeByte(text.charCodeAt(i));
}
}
/**
* Register a virtual I2C device on the bus (e.g. RTC, sensor).
*/
addI2CDevice(device: I2CDevice): void {
if (this.i2cBus) {
this.i2cBus.addDevice(device);
}
}
}

View File

@ -0,0 +1,215 @@
/**
* I2C Bus Manager virtual I2C devices that attach to avr8js AVRTWI
*
* Each device registers at a 7-bit I2C address. When the Arduino sketch
* does Wire.beginTransmission(addr) / Wire.requestFrom(addr, ...), the
* TWI event handler routes events to the matching virtual device.
*/
import type { AVRTWI, TWIEventHandler } from 'avr8js';
// ── Virtual I2C device interface ────────────────────────────────────────────
export interface I2CDevice {
/** 7-bit I2C address (e.g. 0x27 for PCF8574 LCD backpack, 0x3C for SSD1306) */
address: number;
/** Called when master sends a byte after addressing this device for write */
writeByte(value: number): boolean; // return true for ACK
/** Called when master requests a byte from this device (read mode) */
readByte(): number;
/** Optional: called on STOP condition */
stop?(): void;
}
// ── I2C Bus Manager (TWIEventHandler for avr8js) ───────────────────────────
export class I2CBusManager implements TWIEventHandler {
private devices: Map<number, I2CDevice> = new Map();
private activeDevice: I2CDevice | null = null;
private writeMode = true;
constructor(private twi: AVRTWI) {
twi.eventHandler = this;
}
/** Register a virtual I2C device on the bus */
addDevice(device: I2CDevice): void {
this.devices.set(device.address, device);
}
/** Remove a device by address */
removeDevice(address: number): void {
this.devices.delete(address);
}
// ── TWIEventHandler implementation ──────────────────────────────────────
start(_repeated: boolean): void {
this.twi.completeStart();
}
stop(): void {
if (this.activeDevice?.stop) this.activeDevice.stop();
this.activeDevice = null;
this.twi.completeStop();
}
connectToSlave(addr: number, write: boolean): void {
const device = this.devices.get(addr);
if (device) {
this.activeDevice = device;
this.writeMode = write;
this.twi.completeConnect(true); // ACK
} else {
this.activeDevice = null;
this.twi.completeConnect(false); // NACK — no such address
}
}
writeByte(value: number): void {
if (this.activeDevice) {
const ack = this.activeDevice.writeByte(value);
this.twi.completeWrite(ack);
} else {
this.twi.completeWrite(false);
}
}
readByte(_ack: boolean): void {
if (this.activeDevice) {
const value = this.activeDevice.readByte();
this.twi.completeRead(value);
} else {
this.twi.completeRead(0xff);
}
}
}
// ── Built-in virtual I2C devices ───────────────────────────────────────────
/**
* Generic I2C memory / register device.
* Emulates a device with 256 byte registers.
* First write byte = register address, subsequent bytes = data.
* Reads return register contents sequentially.
*
* Used to test I2C communication without a specific device implementation.
*/
export class I2CMemoryDevice implements I2CDevice {
public registers = new Uint8Array(256);
private regPointer = 0;
private firstByte = true;
/** Callback fired whenever a register is written */
public onRegisterWrite: ((reg: number, value: number) => void) | null = null;
constructor(public address: number) {}
writeByte(value: number): boolean {
if (this.firstByte) {
this.regPointer = value;
this.firstByte = false;
} else {
this.registers[this.regPointer] = value;
if (this.onRegisterWrite) {
this.onRegisterWrite(this.regPointer, value);
}
this.regPointer = (this.regPointer + 1) & 0xFF;
}
return true; // ACK
}
readByte(): number {
const value = this.registers[this.regPointer];
this.regPointer = (this.regPointer + 1) & 0xFF;
return value;
}
stop(): void {
this.firstByte = true;
}
}
/**
* Virtual DS1307 RTC returns system time via I2C (address 0x68).
* Supports Wire.requestFrom(0x68, 7) to read seconds..year in BCD.
*/
export class VirtualDS1307 implements I2CDevice {
public address = 0x68;
private regPointer = 0;
private firstByte = true;
private toBCD(n: number): number {
return ((Math.floor(n / 10) & 0xF) << 4) | (n % 10 & 0xF);
}
writeByte(value: number): boolean {
if (this.firstByte) {
this.regPointer = value;
this.firstByte = false;
}
return true;
}
readByte(): number {
const now = new Date();
let val = 0;
switch (this.regPointer) {
case 0: val = this.toBCD(now.getSeconds()); break; // seconds
case 1: val = this.toBCD(now.getMinutes()); break; // minutes
case 2: val = this.toBCD(now.getHours()); break; // hours (24h)
case 3: val = this.toBCD(now.getDay() + 1); break; // day of week (1=Sun)
case 4: val = this.toBCD(now.getDate()); break; // date
case 5: val = this.toBCD(now.getMonth() + 1); break; // month
case 6: val = this.toBCD(now.getFullYear() % 100); break; // year
default: val = 0;
}
this.regPointer = (this.regPointer + 1) & 0x3F;
return val;
}
stop(): void {
this.firstByte = true;
}
}
/**
* Virtual temperature / humidity sensor (address 0x48).
* Returns fixed temperature (configurable) and humidity.
*/
export class VirtualTempSensor implements I2CDevice {
public address = 0x48;
private regPointer = 0;
private firstByte = true;
/** Temperature in degrees C * 100 (e.g. 2350 = 23.50 C) */
public temperature = 2350;
/** Humidity in % * 100 */
public humidity = 5500;
writeByte(value: number): boolean {
if (this.firstByte) {
this.regPointer = value;
this.firstByte = false;
}
return true;
}
readByte(): number {
let val = 0;
// Register 0: temp high byte, 1: temp low byte, 2: humidity high, 3: humidity low
switch (this.regPointer) {
case 0: val = (this.temperature >> 8) & 0xFF; break;
case 1: val = this.temperature & 0xFF; break;
case 2: val = (this.humidity >> 8) & 0xFF; break;
case 3: val = this.humidity & 0xFF; break;
default: val = 0xFF;
}
this.regPointer = (this.regPointer + 1) & 0xFF;
return val;
}
stop(): void {
this.firstByte = true;
}
}

View File

@ -2,6 +2,7 @@ import { create } from 'zustand';
import { AVRSimulator } from '../simulation/AVRSimulator';
import { RP2040Simulator } from '../simulation/RP2040Simulator';
import { PinManager } from '../simulation/PinManager';
import { VirtualDS1307, VirtualTempSensor, I2CMemoryDevice } from '../simulation/I2CBusManager';
import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
import { calculatePinPosition } from '../utils/pinPositionCalculator';
@ -47,6 +48,10 @@ interface SimulatorState {
selectedWireId: string | null;
wireInProgress: WireInProgress | null;
// Serial monitor state
serialOutput: string;
serialMonitorOpen: boolean;
// Actions
initSimulator: () => void;
loadHex: (hex: string) => void;
@ -82,6 +87,11 @@ interface SimulatorState {
// Wire position updates (auto-update when components move)
updateWirePositions: (componentId: string) => void;
recalculateAllWirePositions: () => void;
// Serial monitor
toggleSerialMonitor: () => void;
serialWrite: (text: string) => void;
clearSerialOutput: () => void;
}
export const useSimulatorStore = create<SimulatorState>((set, get) => {
@ -152,6 +162,8 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
],
selectedWireId: null,
wireInProgress: null,
serialOutput: '',
serialMonitorOpen: false,
setBoardType: (type: BoardType) => {
const { running } = get();
@ -161,7 +173,12 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
const simulator = type === 'arduino-uno'
? new AVRSimulator(pinManager)
: new RP2040Simulator(pinManager);
set({ boardType: type, simulator, compiledHex: null });
if (simulator instanceof AVRSimulator) {
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
}
set({ boardType: type, simulator, compiledHex: null, serialOutput: '' });
console.log(`Board switched to: ${type}`);
},
@ -170,7 +187,12 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
const simulator = boardType === 'arduino-uno'
? new AVRSimulator(pinManager)
: new RP2040Simulator(pinManager);
set({ simulator });
if (simulator instanceof AVRSimulator) {
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
}
set({ simulator, serialOutput: '' });
console.log(`Simulator initialized: ${boardType}`);
},
@ -207,6 +229,12 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
startSimulation: () => {
const { simulator } = get();
if (simulator) {
// Register virtual I2C devices before starting
if (simulator instanceof AVRSimulator && simulator.i2cBus) {
simulator.addI2CDevice(new VirtualDS1307());
simulator.addI2CDevice(new VirtualTempSensor());
simulator.addI2CDevice(new I2CMemoryDevice(0x50)); // generic EEPROM at 0x50
}
simulator.start();
set({ running: true });
}
@ -224,7 +252,13 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
const { simulator } = get();
if (simulator) {
simulator.reset();
set({ running: false });
// Re-wire serial callback after reset
if (simulator instanceof AVRSimulator) {
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
}
set({ running: false, serialOutput: '' });
}
},
@ -458,5 +492,21 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
set({ wires: updatedWires });
},
// Serial monitor actions
toggleSerialMonitor: () => {
set((s) => ({ serialMonitorOpen: !s.serialMonitorOpen }));
},
serialWrite: (text: string) => {
const { simulator } = get();
if (simulator && simulator instanceof AVRSimulator) {
simulator.serialWrite(text);
}
},
clearSerialOutput: () => {
set({ serialOutput: '' });
},
};
});