diff --git a/README.md b/README.md index ae6919e..c0f43f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Arduino Emulator - Wokwi Clone +# OpenWokwi - Arduino Emulator -Local Arduino emulator with code editor and visual simulator. +A fully local, open-source Arduino emulator inspired by [Wokwi](https://wokwi.com). Write Arduino code, compile it, and simulate it with real AVR8 CPU emulation and 48+ interactive electronic components — all running in your browser. ## Support This Project @@ -10,18 +10,6 @@ If you find this project helpful, please consider giving it a star! Your support Every star counts and helps make this project better! -## Features - -- ✅ Code editor with syntax highlighting (Monaco Editor) -- ✅ Arduino code compilation with arduino-cli -- ✅ **Official Wokwi repositories cloned locally** - - ✅ **wokwi-elements** - Electronic web components - - ✅ **avr8js** - AVR8 emulator - - ✅ **rp2040js** - RP2040 emulator (future) -- ✅ Visual components using wokwi-elements (Arduino Uno, LEDs, etc.) -- ⏳ Full emulation with avr8js (in progress) -- ⏳ SQLite persistence (coming soon) - ## Screenshots ![Arduino Emulator - Editor and Simulator](doc/img1.png) @@ -32,6 +20,82 @@ Arduino emulator with Monaco code editor and visual simulator with wokwi-element Interactive component properties dialog and segment-based wire editing +## Features + +### Code Editing +- ✅ **Monaco Editor** — Full C++ editor with syntax highlighting, autocomplete, minimap, and dark theme +- ✅ **Arduino compilation** via `arduino-cli` backend — compile sketches to `.hex` files +- ✅ **Compile / Run / Stop / Reset** toolbar buttons with status messages + +### AVR8 Simulation (avr8js) +- ✅ **Real ATmega328p emulation** at 16 MHz using avr8js +- ✅ **Intel HEX parser** with checksum verification +- ✅ **Full GPIO support** — PORTB (pins 8-13), PORTC (A0-A5), PORTD (pins 0-7) +- ✅ **Timer0 peripheral** support +- ✅ **USART (Serial)** support +- ✅ **~60 FPS simulation loop** with `requestAnimationFrame` (~267k cycles/frame) +- ✅ **Speed control** — adjustable from 0.1x to 10x +- ✅ **Single-step debugging** API +- ✅ **External pin state injection** for input components (buttons, potentiometers) + +### Component System (48+ Components) +- ✅ **48 electronic components** auto-discovered from wokwi-elements source code +- ✅ **Component picker modal** with search bar, category filtering, and live wokwi-element previews as thumbnails +- ✅ **9 component categories**: Boards (4), Sensors (6), Displays (3), Input (5), Output (5), Motors (2), Passive (4), Other (19) +- ✅ **Dynamic component rendering** from build-time metadata (TypeScript AST parser extracts `@customElement` tags, `@property` decorators, and pin counts) +- ✅ **Drag-and-drop repositioning** on the simulation canvas +- ✅ **Component rotation** in 90° increments +- ✅ **Property dialog** (single-click) — shows pin roles, Arduino pin assignment, rotate & delete actions +- ✅ **Pin selector** (double-click) — assign Arduino pins D0-D13 and A0-A5 to component pins +- ✅ **Pin overlay system** — clickable cyan dots on each component pin with hover animation +- ✅ **Keyboard shortcuts** — Delete/Backspace to remove, Escape to cancel + +### Part Simulation Behaviors +- ✅ **LED** — pin state drives LED on/off +- ✅ **RGB LED** — digital HIGH/LOW mapped to individual R/G/B channels +- ✅ **Pushbutton** — press/release events inject active-LOW pin state into simulation +- ✅ **Potentiometer** — reads element value (0-1023), converts to voltage, injects into ADC channel +- ✅ **LCD 1602 & LCD 2004** — Full HD44780 controller emulation: + - 4-bit mode protocol (high nibble first, then low nibble) + - DDRAM with proper line address mapping + - Commands: Clear Display, Return Home, Entry Mode Set, Display On/Off, Cursor/Display Shift, Function Set + - Initialization sequence handling + - Enable pin falling-edge detection for data latching + +### Wire System +- ✅ **Wire creation** — click a pin to start, click another pin to connect +- ✅ **Real-time preview** — dashed green wire with L-shaped orthogonal routing while creating +- ✅ **Orthogonal wire rendering** — no diagonal paths +- ✅ **Segment-based wire editing** — hover to highlight, drag segments perpendicular to their orientation +- ✅ **Smooth dragging** with `requestAnimationFrame` +- ✅ **8 signal-type wire colors**: Red (VCC), Black (GND), Blue (Analog), Green (Digital), Purple (PWM), Gold (I2C), Orange (SPI), Cyan (USART) +- ✅ **Automatic overlap offset** — parallel wires are offset symmetrically (6px spacing) +- ✅ **Auto-update positions** — wire endpoints recalculate when components move +- ✅ **Grid snapping** (20px grid) + +### Example Projects +- ✅ **8 built-in example projects** with full code, components, and wire definitions: + +| Example | Category | Difficulty | +|---------|----------|------------| +| Blink LED | Basics | Beginner | +| Traffic Light | Basics | Beginner | +| Button Control | Basics | Beginner | +| Fade LED (PWM) | Basics | Beginner | +| Serial Hello World | Communication | Beginner | +| RGB LED Colors | Basics | Intermediate | +| Simon Says Game | Games | Advanced | +| LCD 20x4 Display | Displays | Intermediate | + +- ✅ **Examples gallery** with category and difficulty filters +- ✅ **One-click loading** — loads code, components, and wires into the editor and simulator + +### Wokwi Libraries (Local Clones) +- ✅ **wokwi-elements** — 48+ electronic web components (Lit-based Web Components) +- ✅ **avr8js** — AVR8 CPU emulator +- ✅ **rp2040js** — RP2040 emulator (cloned, for future use) +- ✅ **Build-time metadata generation** — TypeScript AST parser reads wokwi-elements source to generate component metadata automatically + ## Prerequisites ### 1. Node.js @@ -69,7 +133,8 @@ arduino-cli core install arduino:avr ### 1. Clone the repository ```bash -cd e:\Hardware\wokwi_clon +git clone https://github.com/davidmonterocrespo24/openwokwi.git +cd openwokwi ``` ### 2. Setup Backend @@ -123,92 +188,89 @@ The frontend will be available at: ## Usage 1. Open http://localhost:5173 in your browser -2. Write Arduino code in the editor (there's a Blink example by default) -3. Click "Compile" to compile the code -4. If compilation is successful, click "Run" to start the simulation -5. Watch the simulated LED blinking +2. Write Arduino code in the editor (a Blink example is loaded by default) +3. Click **Compile** to compile the code via the backend +4. Click **Run** to start real AVR8 CPU simulation +5. Watch LEDs, LCDs, and other components react in real time +6. Click on components to view properties or assign pin mappings +7. Double-click components to open the pin selector +8. Click pins to create wires connecting components +9. Browse **Examples** to load pre-built projects (Blink, Traffic Light, Simon Says, LCD, etc.) ## Project Structure ``` -wokwi_clon/ -├── frontend/ # React + Vite +openwokwi/ +├── frontend/ # React + Vite + TypeScript │ ├── src/ -│ │ ├── components/ # React components -│ │ │ ├── components-wokwi/ # wokwi-elements wrappers -│ │ │ ├── editor/ # Monaco Editor -│ │ │ └── simulator/ # Simulation canvas -│ │ ├── store/ # Global state (Zustand) -│ │ ├── services/ # API clients -│ │ └── App.tsx # Main component -│ └── package.json +│ │ ├── components/ +│ │ │ ├── ComponentPickerModal.tsx # Component search & picker +│ │ │ ├── DynamicComponent.tsx # Generic wokwi component renderer +│ │ │ ├── components-wokwi/ # Legacy React wrappers +│ │ │ ├── editor/ # Monaco Editor + toolbar +│ │ │ ├── examples/ # Examples gallery +│ │ │ └── simulator/ # Canvas, wires, pins, dialogs +│ │ ├── simulation/ +│ │ │ ├── AVRSimulator.ts # AVR8 CPU emulator wrapper +│ │ │ ├── PinManager.ts # Pin-to-component mapping +│ │ │ └── parts/ # Part behaviors (LED, LCD, etc.) +│ │ ├── store/ # Zustand state management +│ │ ├── services/ # API clients & ComponentRegistry +│ │ ├── types/ # TypeScript types (wires, components) +│ │ ├── utils/ # Hex parser, wire routing, pin calc +│ │ └── pages/ # EditorPage, ExamplesPage +│ └── public/ +│ └── components-metadata.json # Auto-generated component metadata │ -├── backend/ # FastAPI + Python -│ ├── app/ -│ │ ├── api/routes/ # REST endpoints -│ │ ├── services/ # Business logic -│ │ └── main.py # Entry point -│ └── requirements.txt +├── backend/ # FastAPI + Python +│ └── app/ +│ ├── main.py # Entry point, CORS config +│ ├── api/routes/compile.py # POST /api/compile, GET /api/compile/boards +│ └── services/arduino_cli.py # arduino-cli subprocess wrapper │ -├── wokwi-libs/ # Cloned Wokwi repositories -│ ├── wokwi-elements/ # Web Components -│ ├── avr8js/ # AVR8 Emulator -│ ├── rp2040js/ # RP2040 Emulator -│ └── wokwi-features/ # Features and documentation +├── wokwi-libs/ # Cloned Wokwi repositories +│ ├── wokwi-elements/ # 48+ Web Components (Lit) +│ ├── avr8js/ # AVR8 CPU Emulator +│ ├── rp2040js/ # RP2040 Emulator (future) +│ └── wokwi-features/ # Features and documentation │ -├── README.md -├── WOKWI_LIBS.md # Wokwi integration documentation -└── update-wokwi-libs.bat # Update script +├── scripts/ +│ └── generate-component-metadata.ts # AST parser for component discovery +│ +├── ARCHITECTURE.md # Detailed architecture documentation +├── WOKWI_LIBS.md # Wokwi integration documentation +└── update-wokwi-libs.bat # Update local Wokwi libraries ``` ## Technologies Used ### Frontend -- **React** 18 - UI framework -- **Vite** 5 - Build tool -- **TypeScript** - Static typing -- **Monaco Editor** - Code editor (VSCode) -- **Zustand** - State management -- **Axios** - HTTP client +- **React** 19 — UI framework +- **Vite** 7 — Build tool with local library aliases +- **TypeScript** 5.9 — Static typing +- **Monaco Editor** — Code editor (VS Code engine) +- **Zustand** 5 — State management +- **React Router** 7 — Client-side routing +- **Axios** — HTTP client ### Backend -- **FastAPI** - Python web framework -- **uvicorn** - ASGI server -- **arduino-cli** - Arduino compiler -- **SQLAlchemy** - ORM (coming soon) -- **SQLite** - Database (coming soon) +- **FastAPI** — Python web framework +- **uvicorn** — ASGI server +- **arduino-cli** — Arduino compiler (subprocess) -### Simulation -- **avr8js** - AVR8 emulator (coming soon) -- **@wokwi/elements** - Electronic components (coming soon) +### Simulation & Components +- **avr8js** — Real AVR8 ATmega328p emulator (local clone) +- **wokwi-elements** — 48+ electronic web components built with Lit (local clone) +- **rp2040js** — RP2040 emulator (local clone, for future use) -## Upcoming Features +## Planned Features -### Phase 2: Real Emulation with avr8js -- [ ] Integrate avr8js for real ATmega328p emulation -- [ ] .hex file parser -- [ ] PinManager for pin management -- [ ] Real-time execution - -### Phase 3: Visual Components -- [ ] Integrate @wokwi/elements -- [ ] LED component with real state -- [ ] Resistor component -- [ ] Component drag & drop -- [ ] Visual connections (wires) - -### Phase 4: Persistence -- [ ] SQLite database -- [ ] Project CRUD -- [ ] Save/load code and circuit -- [ ] Project history - -### Phase 5: Advanced Features -- [ ] More components (buttons, potentiometers, sensors) -- [ ] Serial monitor -- [ ] Simulation speed control -- [ ] Example projects -- [ ] Export/import projects +- 📋 **Serial Monitor** — UI for reading USART output from the simulation +- 📋 **Project Persistence** — Save/load projects with SQLite +- 📋 **Undo/Redo** — Edit history for code and circuit changes +- 📋 **Multi-board Support** — Runtime board switching (Mega, Nano, ESP32) +- 📋 **Wire Validation** — Electrical validation and error highlighting +- 📋 **Export/Import** — Share projects as files ## Update Wokwi Libraries @@ -248,9 +310,17 @@ See [WOKWI_LIBS.md](WOKWI_LIBS.md) for more details about Wokwi integration. - Make sure Arduino code is valid - Verify you have the `arduino:avr` core installed +### LED doesn't blink +- Check port listeners are firing (browser console logs) +- Verify pin mapping in the component property dialog + +### CPU stuck at PC=0 +- Ensure `avrInstruction()` is being called in the execution loop +- Check hex file was loaded correctly + ## Contributing -This is an educational project. Suggestions and improvements are welcome! +This is an open-source project. Suggestions, bug reports, and pull requests are welcome! ## License @@ -258,9 +328,9 @@ MIT ## References -- [Wokwi](https://wokwi.com) - Project inspiration -- [avr8js](https://github.com/wokwi/avr8js) - AVR8 emulator -- [wokwi-elements](https://github.com/wokwi/wokwi-elements) - Web components -- [arduino-cli](https://github.com/arduino/arduino-cli) - Arduino compiler -- [Monaco Editor](https://microsoft.github.io/monaco-editor/) - Code editor +- [Wokwi](https://wokwi.com) — Project inspiration +- [avr8js](https://github.com/wokwi/avr8js) — AVR8 emulator +- [wokwi-elements](https://github.com/wokwi/wokwi-elements) — Electronic web components +- [arduino-cli](https://github.com/arduino/arduino-cli) — Arduino compiler +- [Monaco Editor](https://microsoft.github.io/monaco-editor/) — Code editor diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index 687bd8c..23f3a1e 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -49,6 +49,7 @@ export const DynamicComponent: React.FC = ({ const handleComponentEvent = useSimulatorStore((s) => s.handleComponentEvent); const running = useSimulatorStore((s) => s.running); + const simulator = useSimulatorStore((s) => s.simulator); // Check if component is interactive (has simulation logic with attachEvents) const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]); @@ -182,43 +183,38 @@ export const DynamicComponent: React.FC = ({ el.addEventListener('button-press', onButtonPress); el.addEventListener('button-release', onButtonRelease); - const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]); // Fallback if id is like led-1 + const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]); let cleanupSimulationEvents: (() => void) | undefined; - if (logic && logic.attachEvents) { - // We need AVRSimulator instance. We can grab it from store. - const simulator = useSimulatorStore.getState().simulator; - if (simulator) { - // Helper to find Arduino pin connected to a component pin - const getArduinoPin = (componentPinName: string): number | null => { - const wires = useSimulatorStore.getState().wires.filter( - w => (w.start.componentId === id && w.start.pinName === componentPinName) || - (w.end.componentId === id && w.end.pinName === componentPinName) - ); + if (logic && logic.attachEvents && simulator) { + // Helper to find Arduino pin connected to a component pin + const getArduinoPin = (componentPinName: string): number | null => { + const wires = useSimulatorStore.getState().wires.filter( + w => (w.start.componentId === id && w.start.pinName === componentPinName) || + (w.end.componentId === id && w.end.pinName === componentPinName) + ); - for (const w of wires) { - const arduinoEndpoint = w.start.componentId === 'arduino-uno' ? w.start : - w.end.componentId === 'arduino-uno' ? w.end : null; - if (arduinoEndpoint) { - const pin = parseInt(arduinoEndpoint.pinName, 10); - if (!isNaN(pin)) return pin; - } + for (const w of wires) { + const arduinoEndpoint = w.start.componentId === 'arduino-uno' ? w.start : + w.end.componentId === 'arduino-uno' ? w.end : null; + if (arduinoEndpoint) { + const pin = parseInt(arduinoEndpoint.pinName, 10); + if (!isNaN(pin)) return pin; } - return null; - }; + } + return null; + }; - cleanupSimulationEvents = logic.attachEvents(el, simulator, getArduinoPin); - } + cleanupSimulationEvents = logic.attachEvents(el, simulator, getArduinoPin); } return () => { if (cleanupSimulationEvents) cleanupSimulationEvents(); - // Old hardcoded events (to be removed in future if Pushbutton registry works fully) el.removeEventListener('button-press', onButtonPress); el.removeEventListener('button-release', onButtonRelease); }; - }, [id, handleComponentEvent, metadata.id]); + }, [id, handleComponentEvent, metadata.id, simulator]); return (
{ - console.log(`[PORTB LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`); - console.log(` Binary: ${value.toString(2).padStart(8, '0')}`); - console.log(` Pin 13 (bit 5) state: ${this.portB!.pinState(5) === PinState.High ? 'HIGH' : 'LOW'}`); - if (value !== this.lastPortBValue) { this.pinManager.updatePort('PORTB', value, this.lastPortBValue); this.lastPortBValue = value; @@ -97,8 +121,6 @@ export class AVRSimulator { // PORTC (Analog pins A0-A5) this.portC!.addListener((value, _oldValue) => { - console.log(`[PORTC LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`); - if (value !== this.lastPortCValue) { this.pinManager.updatePort('PORTC', value, this.lastPortCValue); this.lastPortCValue = value; @@ -107,8 +129,6 @@ export class AVRSimulator { // PORTD (Digital pins 0-7) this.portD!.addListener((value, _oldValue) => { - console.log(`[PORTD LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`); - if (value !== this.lastPortDValue) { this.pinManager.updatePort('PORTD', value, this.lastPortDValue); this.lastPortDValue = value; @@ -118,6 +138,23 @@ export class AVRSimulator { console.log('Pin hooks configured successfully'); } + /** + * Poll OCR registers and notify PinManager of PWM duty cycle changes + */ + private pollPwmRegisters(): void { + if (!this.cpu) return; + + for (let i = 0; i < PWM_PINS.length; i++) { + const { ocrAddr, pin } = PWM_PINS[i]; + const ocrValue = this.cpu.data[ocrAddr]; + if (ocrValue !== this.lastOcrValues[i]) { + this.lastOcrValues[i] = ocrValue; + const dutyCycle = ocrValue / 255; + this.pinManager.updatePwm(pin, dutyCycle); + } + } + } + /** * Start simulation loop */ @@ -129,34 +166,27 @@ export class AVRSimulator { this.running = true; console.log('Starting AVR simulation...'); - console.log('CPU state:', { - pc: this.cpu.pc, - cycles: this.cpu.cycles, - data: this.cpu.data.slice(0, 10) // First 10 bytes of RAM - }); - console.log('Program loaded:', this.program?.length, 'words'); let frameCount = 0; - const execute = (timestamp: number) => { + const execute = (_timestamp: number) => { if (!this.running || !this.cpu) return; - // Execute instructions in batches for performance // ATmega328p @ 16MHz = 16M cycles/sec // At 60fps: 16,000,000 / 60 ≈ 267,000 cycles per frame const cyclesPerFrame = Math.floor(267000 * this.speed); try { for (let i = 0; i < cyclesPerFrame; i++) { - // Execute one AVR instruction and update peripherals - avrInstruction(this.cpu); // CRITICAL: Execute the actual instruction + avrInstruction(this.cpu); // Execute the AVR instruction this.cpu.tick(); // Update peripheral timers and cycles } - // Log every 60 frames (once per second at 60fps) + // Poll PWM registers every frame + this.pollPwmRegisters(); + frameCount++; if (frameCount % 60 === 0) { console.log(`[CPU] Frame ${frameCount}, PC: ${this.cpu.pc}, Cycles: ${this.cpu.cycles}`); - console.log(`[CPU] PORTB register value: 0x${this.cpu.data[0x25].toString(16).padStart(2, '0')}`); } } catch (error) { console.error('Simulation error:', error); @@ -164,7 +194,6 @@ export class AVRSimulator { return; } - // Schedule next frame this.animationFrame = requestAnimationFrame(execute); }; @@ -195,20 +224,21 @@ export class AVRSimulator { if (this.cpu && this.program) { console.log('Resetting AVR CPU...'); - // Reinitialize CPU this.cpu = new CPU(this.program); this.timer0 = new AVRTimer(this.cpu, timer0Config); + this.timer1 = new AVRTimer(this.cpu, timer1Config); + this.timer2 = new AVRTimer(this.cpu, timer2Config); this.usart = new AVRUSART(this.cpu, usart0Config, 16000000); + this.adc = new AVRADC(this.cpu, adcConfig); - // Reinitialize ports this.portB = new AVRIOPort(this.cpu, portBConfig); this.portC = new AVRIOPort(this.cpu, portCConfig); this.portD = new AVRIOPort(this.cpu, portDConfig); - // Reset port values this.lastPortBValue = 0; this.lastPortCValue = 0; this.lastPortDValue = 0; + this.lastOcrValues = new Array(PWM_PINS.length).fill(-1); this.setupPinHooks(); @@ -216,36 +246,23 @@ export class AVRSimulator { } } - /** - * Check if simulator is running - */ isRunning(): boolean { return this.running; } - /** - * Set simulation speed (1.0 = normal, 0.5 = half speed, 2.0 = double speed) - */ setSpeed(speed: number): void { this.speed = Math.max(0.1, Math.min(10.0, speed)); console.log(`Simulation speed set to ${this.speed}x`); } - /** - * Get current simulation speed - */ getSpeed(): number { return this.speed; } - /** - * Execute a single instruction (for step-by-step debugging) - */ step(): void { if (!this.cpu) return; - - avrInstruction(this.cpu); // Execute the instruction - this.cpu.tick(); // Update peripherals + avrInstruction(this.cpu); + this.cpu.tick(); } /** diff --git a/frontend/src/simulation/PinManager.ts b/frontend/src/simulation/PinManager.ts index acb26c3..0c5c091 100644 --- a/frontend/src/simulation/PinManager.ts +++ b/frontend/src/simulation/PinManager.ts @@ -5,44 +5,50 @@ * - PORTB (0x25) → Digital pins 8-13 * - PORTC (0x28) → Analog pins A0-A5 (14-19) * - PORTD (0x2B) → Digital pins 0-7 + * + * Also supports: + * - Analog voltage injection (for potentiometers, sensors) + * - PWM duty cycle tracking (for servos, RGB LEDs, buzzers) */ export type PinState = boolean; export type PinChangeCallback = (pin: number, state: PinState) => void; +export type AnalogCallback = (pin: number, voltage: number) => void; +export type PwmCallback = (pin: number, dutyCycle: number) => void; export class PinManager { private listeners: Map> = new Map(); + private pwmListeners: Map> = new Map(); + private analogListeners: Map> = new Map(); private pinStates: Map = new Map(); + private pwmValues: Map = new Map(); + + // ── Digital pin API ────────────────────────────────────────────────────── /** - * Register callback for pin state changes - * Returns unsubscribe function - * Note: Does NOT call callback immediately to avoid infinite loops + * Register callback for digital pin state changes. + * Returns unsubscribe function. */ onPinChange(arduinoPin: number, callback: PinChangeCallback): () => void { if (!this.listeners.has(arduinoPin)) { this.listeners.set(arduinoPin, new Set()); } this.listeners.get(arduinoPin)!.add(callback); - - // Return unsubscribe function return () => { this.listeners.get(arduinoPin)?.delete(callback); }; } /** - * Update port register and notify listeners + * Update port register and notify digital pin listeners. */ updatePort(portName: 'PORTB' | 'PORTC' | 'PORTD', newValue: number, oldValue: number = 0) { - // Map AVR ports to Arduino pin numbers const pinOffset = { - 'PORTB': 8, // PORTB0-7 → Arduino D8-D13 (only D8-D13 are used) - 'PORTC': 14, // PORTC0-5 → Arduino A0-A5 (14-19) - 'PORTD': 0, // PORTD0-7 → Arduino D0-D7 + 'PORTB': 8, + 'PORTC': 14, + 'PORTD': 0, }[portName]; - // Check each bit for (let bit = 0; bit < 8; bit++) { const mask = 1 << bit; const oldState = (oldValue & mask) !== 0; @@ -50,11 +56,8 @@ export class PinManager { if (oldState !== newState) { const arduinoPin = pinOffset + bit; - - // Update internal state this.pinStates.set(arduinoPin, newState); - // Notify listeners const callbacks = this.listeners.get(arduinoPin); if (callbacks) { callbacks.forEach(cb => cb(arduinoPin, newState)); @@ -65,26 +68,78 @@ export class PinManager { } } - /** - * Get current state of a pin - */ getPinState(arduinoPin: number): boolean { return this.pinStates.get(arduinoPin) || false; } + // ── PWM duty cycle API ─────────────────────────────────────────────────── + /** - * Get all listeners count (for debugging) + * Register callback for PWM duty cycle changes on a pin. + * dutyCycle is 0.0–1.0. */ + onPwmChange(pin: number, callback: PwmCallback): () => void { + if (!this.pwmListeners.has(pin)) { + this.pwmListeners.set(pin, new Set()); + } + this.pwmListeners.get(pin)!.add(callback); + return () => { + this.pwmListeners.get(pin)?.delete(callback); + }; + } + + /** + * Called by AVRSimulator each frame when an OCR register changes. + */ + updatePwm(pin: number, dutyCycle: number): void { + this.pwmValues.set(pin, dutyCycle); + const callbacks = this.pwmListeners.get(pin); + if (callbacks) { + callbacks.forEach(cb => cb(pin, dutyCycle)); + } + } + + getPwmValue(pin: number): number { + return this.pwmValues.get(pin) ?? 0; + } + + // ── Analog voltage API ─────────────────────────────────────────────────── + + /** + * Register callback when external code sets an analog voltage on a pin. + */ + onAnalogChange(pin: number, callback: AnalogCallback): () => void { + if (!this.analogListeners.has(pin)) { + this.analogListeners.set(pin, new Set()); + } + this.analogListeners.get(pin)!.add(callback); + return () => { + this.analogListeners.get(pin)?.delete(callback); + }; + } + + /** + * Inject a simulated analog voltage (0–5V) on an Arduino pin. + * Notifies any registered analog listeners. + */ + setAnalogVoltage(arduinoPin: number, voltage: number): void { + const callbacks = this.analogListeners.get(arduinoPin); + if (callbacks) { + callbacks.forEach(cb => cb(arduinoPin, voltage)); + } + } + + // ── Utility ────────────────────────────────────────────────────────────── + getListenersCount(): number { let count = 0; this.listeners.forEach(set => count += set.size); return count; } - /** - * Clear all listeners - */ clearAllListeners() { this.listeners.clear(); + this.pwmListeners.clear(); + this.analogListeners.clear(); } } diff --git a/frontend/src/simulation/parts/BasicParts.ts b/frontend/src/simulation/parts/BasicParts.ts index 8c1e0b9..445411d 100644 --- a/frontend/src/simulation/parts/BasicParts.ts +++ b/frontend/src/simulation/parts/BasicParts.ts @@ -2,34 +2,27 @@ import { PartSimulationRegistry } from './PartSimulationRegistry'; import type { AVRSimulator } from '../AVRSimulator'; /** - * Basic Pushbutton implementation + * Basic Pushbutton implementation (full-size) */ PartSimulationRegistry.register('pushbutton', { attachEvents: (element, avrSimulator, getArduinoPinHelper) => { - // 1. Find which Arduino pin is connected to terminal '1.L' or '2.L' const arduinoPin = getArduinoPinHelper('1.l') ?? getArduinoPinHelper('2.l') ?? getArduinoPinHelper('1.r') ?? getArduinoPinHelper('2.r'); - if (arduinoPin === null) { - return () => { }; // no-op if unconnected - } + if (arduinoPin === null) return () => { }; const onButtonPress = () => { - // By default wokwi pushbuttons are active LOW (connected to GND) - avrSimulator.setPinState(arduinoPin, false); + avrSimulator.setPinState(arduinoPin, false); // Active LOW (element as any).pressed = true; }; - const onButtonRelease = () => { - // Release lets the internal pull-up pull it HIGH avrSimulator.setPinState(arduinoPin, true); (element as any).pressed = false; }; element.addEventListener('button-press', onButtonPress); element.addEventListener('button-release', onButtonRelease); - return () => { element.removeEventListener('button-press', onButtonPress); element.removeEventListener('button-release', onButtonRelease); @@ -37,6 +30,101 @@ PartSimulationRegistry.register('pushbutton', { }, }); +/** + * 6mm Pushbutton — same behaviour as the full-size pushbutton + */ +PartSimulationRegistry.register('pushbutton-6mm', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const arduinoPin = + getArduinoPinHelper('1.l') ?? getArduinoPinHelper('2.l') ?? + getArduinoPinHelper('1.r') ?? getArduinoPinHelper('2.r'); + + if (arduinoPin === null) return () => { }; + + const onPress = () => { + avrSimulator.setPinState(arduinoPin, false); + (element as any).pressed = true; + }; + const onRelease = () => { + avrSimulator.setPinState(arduinoPin, true); + (element as any).pressed = false; + }; + + element.addEventListener('button-press', onPress); + element.addEventListener('button-release', onRelease); + return () => { + element.removeEventListener('button-press', onPress); + element.removeEventListener('button-release', onRelease); + }; + }, +}); + +/** + * Slide Switch — toggles between HIGH and LOW on each click + */ +PartSimulationRegistry.register('slide-switch', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + // Slide switch has pins: 1, 2, 3 — middle pin (2) is the common output + const arduinoPin = getArduinoPinHelper('2') ?? getArduinoPinHelper('1'); + if (arduinoPin === null) return () => { }; + + // Read initial value from element (0 or 1) + let state = (element as any).value === 1; + avrSimulator.setPinState(arduinoPin, state); + + const onChange = () => { + state = (element as any).value === 1; + avrSimulator.setPinState(arduinoPin, state); + console.log(`[SlideSwitch] pin ${arduinoPin} → ${state ? 'HIGH' : 'LOW'}`); + }; + + element.addEventListener('change', onChange); + // The slide-switch element fires a 'change' event when clicked + element.addEventListener('input', onChange); + return () => { + element.removeEventListener('change', onChange); + element.removeEventListener('input', onChange); + }; + }, +}); + +/** + * DIP Switch 8 — 8 independent toggle switches + * Pin layout: 1A-8A on one side, 1B-8B on the other + */ +PartSimulationRegistry.register('dip-switch-8', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + // Each switch i has pins (i+1)A and (i+1)B; we use the A side as output + const pins: (number | null)[] = []; + for (let i = 1; i <= 8; i++) { + pins.push(getArduinoPinHelper(`${i}A`) ?? getArduinoPinHelper(`${i}a`)); + } + + // Sync initial states + const values: number[] = (element as any).values || new Array(8).fill(0); + pins.forEach((pin, i) => { + if (pin !== null) avrSimulator.setPinState(pin, values[i] === 1); + }); + + const onChange = () => { + const newValues: number[] = (element as any).values || new Array(8).fill(0); + pins.forEach((pin, i) => { + if (pin !== null) { + const state = newValues[i] === 1; + avrSimulator.setPinState(pin, state); + } + }); + }; + + element.addEventListener('change', onChange); + element.addEventListener('input', onChange); + return () => { + element.removeEventListener('change', onChange); + element.removeEventListener('input', onChange); + }; + }, +}); + /** * Basic LED implementation */ @@ -45,6 +133,65 @@ PartSimulationRegistry.register('led', { if (pinName === 'A') { // Anode (element as any).value = state; } - // We ignore cathode 'C' in this simple digital model } }); + +/** + * LED Bar Graph — 10 LEDs, each driven by one pin + * Wokwi pin names: A1-A10 + */ +PartSimulationRegistry.register('led-bar-graph', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const pinManager = (avrSimulator as any).pinManager; + if (!pinManager) return () => { }; + + const values = new Array(10).fill(0); + const unsubscribers: (() => void)[] = []; + + for (let i = 1; i <= 10; i++) { + const pin = getArduinoPinHelper(`A${i}`); + if (pin !== null) { + const idx = i - 1; + unsubscribers.push( + pinManager.onPinChange(pin, (_p: number, state: boolean) => { + values[idx] = state ? 1 : 0; + (element as any).values = [...values]; + }) + ); + } + } + + return () => unsubscribers.forEach(u => u()); + }, +}); + +/** + * 7-Segment Display + * Pins: A, B, C, D, E, F, G, DP (common cathode — segments light when HIGH) + * The wokwi-7segment 'values' property is an array of 8 values (A B C D E F G DP) + */ +PartSimulationRegistry.register('7segment', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const pinManager = (avrSimulator as any).pinManager; + if (!pinManager) return () => { }; + + // Order matches wokwi-elements values array: [A, B, C, D, E, F, G, DP] + const segmentPinNames = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'DP']; + const values = new Array(8).fill(0); + const unsubscribers: (() => void)[] = []; + + segmentPinNames.forEach((segName, idx) => { + const pin = getArduinoPinHelper(segName); + if (pin !== null) { + unsubscribers.push( + pinManager.onPinChange(pin, (_p: number, state: boolean) => { + values[idx] = state ? 1 : 0; + (element as any).values = [...values]; + }) + ); + } + }); + + return () => unsubscribers.forEach(u => u()); + }, +}); diff --git a/frontend/src/simulation/parts/ComplexParts.ts b/frontend/src/simulation/parts/ComplexParts.ts index e0e0bcd..113f46b 100644 --- a/frontend/src/simulation/parts/ComplexParts.ts +++ b/frontend/src/simulation/parts/ComplexParts.ts @@ -1,96 +1,423 @@ import { PartSimulationRegistry } from './PartSimulationRegistry'; import type { AVRSimulator } from '../AVRSimulator'; +// ─── Helpers ──────────────────────────────────────────────────────────────── + +/** Read the ADC instance from the simulator (returns null if not initialized) */ +function getADC(avrSimulator: AVRSimulator): any | null { + return (avrSimulator as any).getADC?.() ?? null; +} + +/** Write an analog voltage (0-5V) to an ADC channel derived from an Arduino pin (A0-A5 = 14-19) */ +function setAdcVoltage(avrSimulator: AVRSimulator, arduinoPin: number, voltage: number): boolean { + if (arduinoPin < 14 || arduinoPin > 19) return false; + const channel = arduinoPin - 14; + const adc = getADC(avrSimulator); + if (!adc) return false; + adc.channelValues[channel] = voltage; + return true; +} + +// ─── RGB LED (PWM-aware) ───────────────────────────────────────────────────── + /** - * RGB LED implementation - * Translates digital HIGH/LOW to corresponding 255/0 values on RGB channels + * RGB LED implementation — supports both digital and PWM (analogWrite) output. + * Falls back to digital mode if no PWM is detected. */ PartSimulationRegistry.register('rgb-led', { - onPinStateChange: (pinName: string, state: boolean, element: HTMLElement) => { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const pinManager = (avrSimulator as any).pinManager; + if (!pinManager) return () => { }; + const el = element as any; - if (pinName === 'R') { - el.ledRed = state ? 255 : 0; - } else if (pinName === 'G') { - el.ledGreen = state ? 255 : 0; - } else if (pinName === 'B') { - el.ledBlue = state ? 255 : 0; + const unsubscribers: (() => void)[] = []; + + const pinR = getArduinoPinHelper('R'); + const pinG = getArduinoPinHelper('G'); + const pinB = getArduinoPinHelper('B'); + + // Digital fallback + if (pinR !== null) { + unsubscribers.push(pinManager.onPinChange(pinR, (_: number, state: boolean) => { + el.ledRed = state ? 255 : 0; + })); } - } + if (pinG !== null) { + unsubscribers.push(pinManager.onPinChange(pinG, (_: number, state: boolean) => { + el.ledGreen = state ? 255 : 0; + })); + } + if (pinB !== null) { + unsubscribers.push(pinManager.onPinChange(pinB, (_: number, state: boolean) => { + el.ledBlue = state ? 255 : 0; + })); + } + + // PWM override — when analogWrite() is used the OCR value supersedes digital + const pwmPins = [ + { pin: pinR, prop: 'ledRed' }, + { pin: pinG, prop: 'ledGreen' }, + { pin: pinB, prop: 'ledBlue' }, + ]; + for (const { pin, prop } of pwmPins) { + if (pin !== null) { + unsubscribers.push(pinManager.onPwmChange(pin, (_: number, dc: number) => { + el[prop] = Math.round(dc * 255); + })); + } + } + + return () => unsubscribers.forEach(u => u()); + }, }); -/** - * Analog Potentiometer implementation - */ +// ─── Potentiometer (rotary) ────────────────────────────────────────────────── + PartSimulationRegistry.register('potentiometer', { - attachEvents: (element: HTMLElement, avrSimulator: AVRSimulator, getArduinoPinHelper: (pin: string) => number | null) => { - // A potentiometer's 'SIG' pin goes to an Analog In (A0-A5). - // We map generic pin integers back to our ADC logic. - // E.g., A0 = pin 14 on UNO + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const arduinoPin = getArduinoPinHelper('SIG'); + if (arduinoPin === null) return () => { }; - // Potentiometer emits 'input' events when dragged - const onInput = (_e: Event) => { - const arduinoPin = getArduinoPinHelper('SIG'); - // If connected to Analog Pin (14-19 is A0-A5 on Uno) - if (arduinoPin !== null && arduinoPin >= 14 && arduinoPin <= 19) { - // Find the analog channel (0-5) - const channel = arduinoPin - 14; - - // Element's value is between 0-1023 - const value = parseInt((element as any).value || '0', 10); - - // Access avr8js ADC to inject analog voltages - // (Assuming getADC is implemented in AVRSimulator) - const adc: any = (avrSimulator as any).getADC?.(); - if (adc) { - // ADC wants a float voltage from 0 to 5V. - // Potentiometer is linearly 0-1023 -> 5V max - const volts = (value / 1023.0) * 5.0; - adc.channelValues[channel] = volts; - } + const onInput = () => { + const raw = parseInt((element as any).value || '0', 10); + const volts = (raw / 1023.0) * 5.0; + if (!setAdcVoltage(avrSimulator, arduinoPin, volts)) { + console.warn('[Potentiometer] ADC not available — is AVRADC initialized?'); } }; element.addEventListener('input', onInput); + return () => element.removeEventListener('input', onInput); + }, +}); + +// ─── Slide Potentiometer ───────────────────────────────────────────────────── + +PartSimulationRegistry.register('slide-potentiometer', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const arduinoPin = getArduinoPinHelper('SIG') ?? getArduinoPinHelper('OUT'); + if (arduinoPin === null) return () => { }; + + const el = element as any; + + const onInput = () => { + const min = el.min ?? 0; + const max = el.max ?? 1023; + const value = el.value ?? 0; + const normalized = (value - min) / (max - min || 1); + const volts = normalized * 5.0; + setAdcVoltage(avrSimulator, arduinoPin, volts); + }; + + element.addEventListener('input', onInput); + return () => element.removeEventListener('input', onInput); + }, +}); + +// ─── Photoresistor Sensor ──────────────────────────────────────────────────── + +/** + * Photoresistor sensor — the wokwi element does not emit input events, + * so we simulate light level with a slider drawn via the component's + * luminance property when available, or simply set a mid-range voltage. + * + * The element exposes `ledDO` and `ledPower` for display only. + * We inject a static mid-range voltage on the AO pin so analogRead() + * returns a valid value. Users can modify the element's `value` attribute. + */ +PartSimulationRegistry.register('photoresistor-sensor', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const pinAO = getArduinoPinHelper('AO') ?? getArduinoPinHelper('A0'); + const pinDO = getArduinoPinHelper('DO') ?? getArduinoPinHelper('D0'); + const pinManager = (avrSimulator as any).pinManager; + + const unsubscribers: (() => void)[] = []; + + // Inject initial mid-range voltage (simulate moderate light) + if (pinAO !== null) { + setAdcVoltage(avrSimulator, pinAO, 2.5); + } + + // Watch element's 'input' events in case the element supports it + const onInput = () => { + const val = (element as any).value; + if (val !== undefined && pinAO !== null) { + const volts = (val / 1023.0) * 5.0; + setAdcVoltage(avrSimulator, pinAO, volts); + } + }; + element.addEventListener('input', onInput); + unsubscribers.push(() => element.removeEventListener('input', onInput)); + + // DO (digital output) — if connected, update element's LED indicator + if (pinDO !== null && pinManager) { + unsubscribers.push(pinManager.onPinChange(pinDO, (_: number, state: boolean) => { + (element as any).ledDO = state; + })); + } + + return () => unsubscribers.forEach(u => u()); + }, +}); + +// ─── Analog Joystick ───────────────────────────────────────────────────────── + +/** + * Analog Joystick — two axes (xValue/yValue 0-1023) + button press + * Wokwi pins: VRX (X axis), VRY (Y axis), SW (button) + */ +PartSimulationRegistry.register('analog-joystick', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const pinX = getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT'); + const pinY = getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT'); + const pinSW = getArduinoPinHelper('SW'); + const el = element as any; + + // Center position is mid-range (~2.5V) + if (pinX !== null) setAdcVoltage(avrSimulator, pinX, 2.5); + if (pinY !== null) setAdcVoltage(avrSimulator, pinY, 2.5); + + const onMove = () => { + // xValue / yValue are 0-1023 + if (pinX !== null) { + const vx = ((el.xValue ?? 512) / 1023.0) * 5.0; + setAdcVoltage(avrSimulator, pinX, vx); + } + if (pinY !== null) { + const vy = ((el.yValue ?? 512) / 1023.0) * 5.0; + setAdcVoltage(avrSimulator, pinY, vy); + } + }; + + const onPress = () => { + if (pinSW !== null) avrSimulator.setPinState(pinSW, false); // Active LOW + el.pressed = true; + }; + const onRelease = () => { + if (pinSW !== null) avrSimulator.setPinState(pinSW, true); + el.pressed = false; + }; + + element.addEventListener('input', onMove); + element.addEventListener('joystick-move', onMove); + element.addEventListener('button-press', onPress); + element.addEventListener('button-release', onRelease); return () => { - element.removeEventListener('input', onInput); + element.removeEventListener('input', onMove); + element.removeEventListener('joystick-move', onMove); + element.removeEventListener('button-press', onPress); + element.removeEventListener('button-release', onRelease); }; }, }); +// ─── Servo ─────────────────────────────────────────────────────────────────── + /** - * HD44780 LCD Controller Simulation (for LCD 1602 and LCD 2004) - * - * Implements the 4-bit mode protocol used by the Arduino LiquidCrystal library. - * The HD44780 controller uses 6 signal lines in 4-bit mode: - * RS (Register Select): 0 = command, 1 = data - * E (Enable): Data is latched on falling edge (HIGH→LOW) - * D4-D7: 4 data bits (high nibble first, then low nibble) - * - * DDRAM address mapping for multi-line displays: - * Line 0: 0x00-0x13 (or 0x00-0x0F for 16x2) - * Line 1: 0x40-0x53 (or 0x40-0x4F for 16x2) - * Line 2: 0x14-0x27 (20x4 only) - * Line 3: 0x54-0x67 (20x4 only) + * Servo motor — reads OCR1A and ICR1 to calculate pulse width and angle. + * + * Standard RC servo protocol: + * - 50 Hz signal (20 ms period) + * - Pulse width 1 ms → 0°, 1.5 ms → 90°, 2 ms → 180° + * + * With Timer1, prescaler=8, F_CPU=16MHz: + * - ICR1 = 20000 for 50Hz + * - OCR1A = 1000 → 0°, 1500 → 90°, 2000 → 180° + * + * We poll these registers every animation frame via a requestAnimationFrame loop. */ +PartSimulationRegistry.register('servo', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const pinSIG = getArduinoPinHelper('PWM') ?? getArduinoPinHelper('SIG') ?? getArduinoPinHelper('1'); + const el = element as any; + + // OCR1A low byte = 0x88, OCR1A high byte = 0x89 + // ICR1L = 0x86, ICR1H = 0x87 + const OCR1AL = 0x88; + const OCR1AH = 0x89; + const ICR1L = 0x86; + const ICR1H = 0x87; + + let rafId: number | null = null; + let lastOcr1a = -1; + + const poll = () => { + const cpu = (avrSimulator as any).cpu; + if (!cpu) { rafId = requestAnimationFrame(poll); return; } + + const ocr1a = cpu.data[OCR1AL] | (cpu.data[OCR1AH] << 8); + if (ocr1a !== lastOcr1a) { + lastOcr1a = ocr1a; + const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8); + + // Calculate pulse width in microseconds + // prescaler 8, F_CPU 16MHz → 1 tick = 0.5µs + // pulse_us = ocr1a * 0.5 + // But also handle prescaler 64 (1 tick = 4µs) and default ICR1 detection + let pulseUs: number; + if (icr1 > 0) { + // Proportional to ICR1 period (assume 20ms period) + pulseUs = 1000 + (ocr1a / icr1) * 1000; + } else { + // Fallback: prescaler 8 + pulseUs = ocr1a * 0.5; + } + + // Clamp to 1000-2000µs and map to 0-180° + const clamped = Math.max(1000, Math.min(2000, pulseUs)); + const angle = Math.round(((clamped - 1000) / 1000) * 180); + el.angle = angle; + } + + // Also support PWM duty cycle approach via PinManager + if (pinSIG !== null) { + const pinManager = (avrSimulator as any).pinManager; + // Only override angle if cpu-based approach doesn't work + // (ICR1 = 0 means Timer1 not configured as servo) + const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8); + if (icr1 === 0 && pinManager) { + const dc = pinManager.getPwmValue(pinSIG); + if (dc > 0) { + el.angle = Math.round(dc * 180); + } + } + } + + rafId = requestAnimationFrame(poll); + }; + + rafId = requestAnimationFrame(poll); + + return () => { + if (rafId !== null) cancelAnimationFrame(rafId); + }; + }, +}); + +// ─── Buzzer ────────────────────────────────────────────────────────────────── + +/** + * Buzzer — uses Web Audio API to generate a tone. + * + * Reads OCR2A (Timer2 CTC mode) to determine frequency: + * f = F_CPU / (2 × prescaler × (OCR2A + 1)) + * + * Prescaler detected from TCCR2B[2:0] bits. + * Activates when duty cycle > 0 (pin is driven HIGH). + */ +PartSimulationRegistry.register('buzzer', { + attachEvents: (element, avrSimulator, getArduinoPinHelper) => { + const pinSIG = getArduinoPinHelper('1') ?? getArduinoPinHelper('+') ?? getArduinoPinHelper('POS'); + const pinManager = (avrSimulator as any).pinManager; + + let audioCtx: AudioContext | null = null; + let oscillator: OscillatorNode | null = null; + let gainNode: GainNode | null = null; + let isSounding = false; + const el = element as any; + + // Timer2 register addresses + const OCR2A = 0xB3; + const TCCR2B = 0xB1; + const F_CPU = 16_000_000; + + const prescalerTable: Record = { + 1: 1, 2: 8, 3: 32, 4: 64, 5: 128, 6: 256, 7: 1024, + }; + + function getFrequency(cpu: any): number { + const ocr2a = cpu.data[OCR2A] ?? 0; + const tccr2b = cpu.data[TCCR2B] ?? 0; + const csField = tccr2b & 0x07; + const prescaler = prescalerTable[csField] ?? 64; + // CTC mode: f = F_CPU / (2 × prescaler × (OCR2A + 1)) + return F_CPU / (2 * prescaler * (ocr2a + 1)); + } + + function startTone(freq: number) { + if (!audioCtx) { + audioCtx = new AudioContext(); + gainNode = audioCtx.createGain(); + gainNode.gain.value = 0.1; + gainNode.connect(audioCtx.destination); + } + if (oscillator) { + oscillator.frequency.setTargetAtTime(freq, audioCtx.currentTime, 0.01); + return; + } + oscillator = audioCtx.createOscillator(); + oscillator.type = 'square'; + oscillator.frequency.value = freq; + oscillator.connect(gainNode!); + oscillator.start(); + isSounding = true; + if (el.playing !== undefined) el.playing = true; + } + + function stopTone() { + if (oscillator) { + oscillator.stop(); + oscillator.disconnect(); + oscillator = null; + } + isSounding = false; + if (el.playing !== undefined) el.playing = false; + } + + // Poll via PWM duty cycle on the buzzer pin + const unsubscribers: (() => void)[] = []; + + if (pinSIG !== null && pinManager) { + unsubscribers.push(pinManager.onPwmChange(pinSIG, (_: number, dc: number) => { + const cpu = (avrSimulator as any).cpu; + if (dc > 0) { + const freq = cpu ? getFrequency(cpu) : 440; + startTone(Math.max(20, Math.min(20000, freq))); + } else { + stopTone(); + } + })); + + // Also respond to digital HIGH/LOW (tone() toggles the pin) + unsubscribers.push(pinManager.onPinChange(pinSIG, (_: number, state: boolean) => { + if (!isSounding && state) { + const cpu = (avrSimulator as any).cpu; + const freq = cpu ? getFrequency(cpu) : 440; + startTone(Math.max(20, Math.min(20000, freq))); + } else if (isSounding && !state) { + // Don't stop on every LOW — tone() generates a square wave + // We stop only when duty cycle drops to 0 via onPwmChange + } + })); + } + + return () => { + stopTone(); + if (audioCtx) { audioCtx.close(); audioCtx = null; } + unsubscribers.forEach(u => u()); + }; + }, +}); + +// ─── LCD 1602 / 2004 ───────────────────────────────────────────────────────── + function createLcdSimulation(cols: number, rows: number) { return { attachEvents: (element: HTMLElement, avrSimulator: AVRSimulator, getArduinoPinHelper: (pin: string) => number | null) => { const el = element as any; - // HD44780 internal state - const ddram = new Uint8Array(128).fill(0x20); // Display Data RAM (space = 0x20) - let ddramAddress = 0; // Current DDRAM address - let entryIncrement = true; // true = increment, false = decrement - let displayOn = true; // Is display on? - let cursorOn = false; // Underline cursor visible? - let blinkOn = false; // Blinking block cursor? - let nibbleState: 'high' | 'low' = 'high'; // 4-bit mode nibble tracking - let highNibble = 0; // Stored high nibble - let initialized = false; // Has initialization sequence completed? - let initCount = 0; // Count initialization nibbles + const ddram = new Uint8Array(128).fill(0x20); + let ddramAddress = 0; + let entryIncrement = true; + let displayOn = true; + let cursorOn = false; + let blinkOn = false; + let nibbleState: 'high' | 'low' = 'high'; + let highNibble = 0; + let initialized = false; + let initCount = 0; - // Pin states tracked locally let rsState = false; let eState = false; let d4State = false; @@ -98,12 +425,10 @@ function createLcdSimulation(cols: number, rows: number) { let d6State = false; let d7State = false; - // DDRAM line offsets for the HD44780 const lineOffsets = rows >= 4 - ? [0x00, 0x40, 0x14, 0x54] // 20x4 LCD - : [0x00, 0x40]; // 16x2 LCD + ? [0x00, 0x40, 0x14, 0x54] + : [0x00, 0x40]; - // Convert DDRAM address to linear buffer index for the element function ddramToLinear(addr: number): number { for (let row = 0; row < rows; row++) { const offset = lineOffsets[row]; @@ -111,17 +436,14 @@ function createLcdSimulation(cols: number, rows: number) { return row * cols + (addr - offset); } } - return -1; // Address not visible + return -1; } - // Refresh the wokwi-element's characters from our DDRAM function refreshDisplay() { if (!displayOn) { - // Blank display el.characters = new Uint8Array(cols * rows).fill(0x20); return; } - const chars = new Uint8Array(cols * rows); for (let row = 0; row < rows; row++) { const offset = lineOffsets[row]; @@ -132,8 +454,6 @@ function createLcdSimulation(cols: number, rows: number) { el.characters = chars; el.cursor = cursorOn; el.blink = blinkOn; - - // Set cursor position const cursorLinear = ddramToLinear(ddramAddress); if (cursorLinear >= 0) { el.cursorX = cursorLinear % cols; @@ -141,169 +461,96 @@ function createLcdSimulation(cols: number, rows: number) { } } - // Process a complete byte (command or data) function processByte(rs: boolean, data: number) { if (!rs) { - // === COMMAND === if (data & 0x80) { - // Set DDRAM Address (bit 7 = 1) ddramAddress = data & 0x7F; } else if (data & 0x40) { - // Set CGRAM Address - not implemented for display + // CGRAM — not implemented } else if (data & 0x20) { - // Function Set (usually during init) - // DL=0 means 4-bit mode (already assumed) initialized = true; } else if (data & 0x10) { - // Cursor/Display Shift const sc = (data >> 3) & 1; const rl = (data >> 2) & 1; - if (!sc) { - // Move cursor - ddramAddress += rl ? 1 : -1; - ddramAddress &= 0x7F; - } + if (!sc) { ddramAddress = (ddramAddress + (rl ? 1 : -1)) & 0x7F; } } else if (data & 0x08) { - // Display On/Off Control displayOn = !!(data & 0x04); - cursorOn = !!(data & 0x02); - blinkOn = !!(data & 0x01); + cursorOn = !!(data & 0x02); + blinkOn = !!(data & 0x01); } else if (data & 0x04) { - // Entry Mode Set entryIncrement = !!(data & 0x02); } else if (data & 0x02) { - // Return Home ddramAddress = 0; } else if (data & 0x01) { - // Clear Display ddram.fill(0x20); ddramAddress = 0; } } else { - // === DATA (character write) === ddram[ddramAddress & 0x7F] = data; - - // Auto-increment/decrement address - if (entryIncrement) { - ddramAddress = (ddramAddress + 1) & 0x7F; - } else { - ddramAddress = (ddramAddress - 1) & 0x7F; - } + ddramAddress = entryIncrement + ? (ddramAddress + 1) & 0x7F + : (ddramAddress - 1) & 0x7F; } - refreshDisplay(); } - // Handle Enable pin falling edge - this is where data is latched function onEnableFallingEdge() { - // Read D4-D7 to form a nibble const nibble = (d4State ? 0x01 : 0) | (d5State ? 0x02 : 0) | (d6State ? 0x04 : 0) | (d7State ? 0x08 : 0); - // During initialization, the LiquidCrystal library sends - // several single-nibble commands (0x03, 0x03, 0x03, 0x02) - // before switching to 4-bit mode proper if (!initialized) { initCount++; - if (initCount >= 4) { - initialized = true; - nibbleState = 'high'; - } + if (initCount >= 4) { initialized = true; nibbleState = 'high'; } return; } if (nibbleState === 'high') { - // First nibble (high 4 bits) highNibble = nibble << 4; nibbleState = 'low'; } else { - // Second nibble (low 4 bits) → combine and process - const fullByte = highNibble | nibble; + processByte(rsState, highNibble | nibble); nibbleState = 'high'; - processByte(rsState, fullByte); } } - // Get Arduino pin numbers for LCD pins const pinRS = getArduinoPinHelper('RS'); - const pinE = getArduinoPinHelper('E'); + const pinE = getArduinoPinHelper('E'); const pinD4 = getArduinoPinHelper('D4'); const pinD5 = getArduinoPinHelper('D5'); const pinD6 = getArduinoPinHelper('D6'); const pinD7 = getArduinoPinHelper('D7'); - console.log(`[LCD] Pin mapping: RS=${pinRS}, E=${pinE}, D4=${pinD4}, D5=${pinD5}, D6=${pinD6}, D7=${pinD7}`); - - // Subscribe to pin changes via PinManager const pinManager = (avrSimulator as any).pinManager; - if (!pinManager) { - console.warn('[LCD] No pinManager found on AVRSimulator'); - return () => { }; - } + if (!pinManager) return () => { }; const unsubscribers: (() => void)[] = []; - if (pinRS !== null) { - unsubscribers.push(pinManager.onPinChange(pinRS, (_p: number, state: boolean) => { - rsState = state; - })); - } + if (pinRS !== null) unsubscribers.push(pinManager.onPinChange(pinRS, (_: number, s: boolean) => { rsState = s; })); + if (pinD4 !== null) unsubscribers.push(pinManager.onPinChange(pinD4, (_: number, s: boolean) => { d4State = s; })); + if (pinD5 !== null) unsubscribers.push(pinManager.onPinChange(pinD5, (_: number, s: boolean) => { d5State = s; })); + if (pinD6 !== null) unsubscribers.push(pinManager.onPinChange(pinD6, (_: number, s: boolean) => { d6State = s; })); + if (pinD7 !== null) unsubscribers.push(pinManager.onPinChange(pinD7, (_: number, s: boolean) => { d7State = s; })); - if (pinD4 !== null) { - unsubscribers.push(pinManager.onPinChange(pinD4, (_p: number, state: boolean) => { - d4State = state; - })); - } - - if (pinD5 !== null) { - unsubscribers.push(pinManager.onPinChange(pinD5, (_p: number, state: boolean) => { - d5State = state; - })); - } - - if (pinD6 !== null) { - unsubscribers.push(pinManager.onPinChange(pinD6, (_p: number, state: boolean) => { - d6State = state; - })); - } - - if (pinD7 !== null) { - unsubscribers.push(pinManager.onPinChange(pinD7, (_p: number, state: boolean) => { - d7State = state; - })); - } - - // Enable pin: watch for falling edge (HIGH → LOW) if (pinE !== null) { - unsubscribers.push(pinManager.onPinChange(pinE, (_p: number, state: boolean) => { + unsubscribers.push(pinManager.onPinChange(pinE, (_: number, s: boolean) => { const wasHigh = eState; - eState = state; - - // Falling edge: data is latched - if (wasHigh && !state) { - onEnableFallingEdge(); - } + eState = s; + if (wasHigh && !s) onEnableFallingEdge(); })); } - // Initialize display as blank refreshDisplay(); console.log(`[LCD] ${cols}x${rows} simulation initialized`); return () => { unsubscribers.forEach(u => u()); - console.log(`[LCD] ${cols}x${rows} simulation cleaned up`); }; }, }; } -// Register LCD 1602 (16x2) PartSimulationRegistry.register('lcd1602', createLcdSimulation(16, 2)); - -// Register LCD 2004 (20x4) PartSimulationRegistry.register('lcd2004', createLcdSimulation(20, 4)); -