diff --git a/frontend/src/components/simulator/SerialMonitor.tsx b/frontend/src/components/simulator/SerialMonitor.tsx new file mode 100644 index 0000000..b76e1d5 --- /dev/null +++ b/frontend/src/components/simulator/SerialMonitor.tsx @@ -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(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 ( +
+ {/* Header */} +
+ Serial Monitor +
+ + +
+
+ + {/* Output area */} +
+        {serialOutput || (running ? 'Waiting for serial data...\n' : 'Start simulation to see serial output.\n')}
+      
+ + {/* Input row */} +
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Type message to send..." + style={styles.input} + disabled={!running} + /> + + +
+
+ ); +}; + +const styles: Record = { + 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, + }, +}; diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 109fdab..e83323d 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -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 + +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 + +#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 + +#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 + +#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 +#include + +#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 diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index 5699e7e..dbaa9e2 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -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(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 = () => { Examples + @@ -74,8 +102,15 @@ export const EditorPage: React.FC = () => {
-
- +
+
+ +
+ {serialMonitorOpen && ( +
+ +
+ )}
diff --git a/frontend/src/simulation/AVRSimulator.ts b/frontend/src/simulation/AVRSimulator.ts index dd144d2..6dbe205 100644 --- a/frontend/src/simulation/AVRSimulator.ts +++ b/frontend/src/simulation/AVRSimulator.ts @@ -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); + } + } } diff --git a/frontend/src/simulation/I2CBusManager.ts b/frontend/src/simulation/I2CBusManager.ts new file mode 100644 index 0000000..1db3df3 --- /dev/null +++ b/frontend/src/simulation/I2CBusManager.ts @@ -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 = 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; + } +} diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index d8f0bea..d178068 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -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((set, get) => { @@ -152,6 +162,8 @@ export const useSimulatorStore = create((set, get) => { ], selectedWireId: null, wireInProgress: null, + serialOutput: '', + serialMonitorOpen: false, setBoardType: (type: BoardType) => { const { running } = get(); @@ -161,7 +173,12 @@ export const useSimulatorStore = create((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((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((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((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((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: '' }); + }, }; });