diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index a59e95c..bfa9e74 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -262,8 +262,8 @@ export const SimulatorCanvas = () => { const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null; const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null; - if (componentWrapper && !runningRef.current) { - // ── Single finger on a component: start drag ── + if (componentWrapper) { + // ── Single finger on a component: track for click/drag ── const componentId = componentWrapper.getAttribute('data-component-id'); if (componentId) { const component = componentsRef.current.find((c) => c.id === componentId); @@ -985,10 +985,7 @@ export const SimulatorCanvas = () => { y={component.y} isSelected={isSelected} onMouseDown={(e) => { - // Only handle UI events when simulation is NOT running - if (!running) { - handleComponentMouseDown(component.id, e); - } + handleComponentMouseDown(component.id, e); }} onDoubleClick={(e) => { // Only handle UI events when simulation is NOT running diff --git a/frontend/src/simulation/AVRSimulator.ts b/frontend/src/simulation/AVRSimulator.ts index 07e3a98..28b531a 100644 --- a/frontend/src/simulation/AVRSimulator.ts +++ b/frontend/src/simulation/AVRSimulator.ts @@ -152,6 +152,9 @@ export class AVRSimulator { /** 'uno' for ATmega328P boards (Uno, Nano); 'mega' for ATmega2560; 'tiny85' for ATtiny85 */ private boardVariant: 'uno' | 'mega' | 'tiny85'; + /** Cycle-accurate pin change queue — used by timing-sensitive peripherals (e.g. DHT22). */ + private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = []; + /** Serial output buffer — subscribers receive each byte or line */ public onSerialData: ((char: string) => void) | null = null; /** Fires whenever the sketch changes Serial baud rate (Serial.begin) */ @@ -306,6 +309,37 @@ export class AVRSimulator { return this.adc; } + /** + * Returns the current CPU cycle count. + * Used by timing-sensitive peripherals to schedule future pin changes. + */ + getCurrentCycles(): number { + return this.cpu?.cycles ?? 0; + } + + /** + * Schedule a pin state change at a specific future CPU cycle count. + * The change fires between AVR instructions, enabling cycle-accurate protocol simulation. + * Used by DHT22 and other timing-sensitive single-wire peripherals. + */ + schedulePinChange(pin: number, state: boolean, atCycle: number): void { + // Callers are expected to push entries in ascending cycle order. + // Insert at the correct position to maintain sort (linear scan from end, O(1) for ordered pushes). + let i = this.scheduledPinChanges.length; + while (i > 0 && this.scheduledPinChanges[i - 1].cycle > atCycle) i--; + this.scheduledPinChanges.splice(i, 0, { cycle: atCycle, pin, state }); + } + + /** Flush all scheduled pin changes whose target cycle has been reached. */ + private flushScheduledPinChanges(): void { + if (this.scheduledPinChanges.length === 0 || !this.cpu) return; + const now = this.cpu.cycles; + while (this.scheduledPinChanges.length > 0 && this.scheduledPinChanges[0].cycle <= now) { + const { pin, state } = this.scheduledPinChanges.shift()!; + this.setPinState(pin, state); + } + } + /** * Fire onPinChangeWithTime for every bit that differs between newVal and oldVal. * @param pinMap Optional explicit per-bit Arduino pin numbers (Mega). @@ -444,6 +478,7 @@ export class AVRSimulator { for (let i = 0; i < cyclesPerFrame; i++) { avrInstruction(this.cpu); // Execute the AVR instruction this.cpu.tick(); // Update peripheral timers and cycles + if (this.scheduledPinChanges.length > 0) this.flushScheduledPinChanges(); } // Poll PWM registers every frame @@ -476,6 +511,7 @@ export class AVRSimulator { cancelAnimationFrame(this.animationFrame); this.animationFrame = null; } + this.scheduledPinChanges = []; console.log('AVR simulation stopped'); } diff --git a/frontend/src/simulation/parts/ProtocolParts.ts b/frontend/src/simulation/parts/ProtocolParts.ts index 5a11141..d55c84d 100644 --- a/frontend/src/simulation/parts/ProtocolParts.ts +++ b/frontend/src/simulation/parts/ProtocolParts.ts @@ -384,31 +384,68 @@ function buildDHT22Payload(element: HTMLElement): Uint8Array { } /** - * Drive 40 bits on the DATA pin as fast as synchronous setPinState allows. - * Each bit produces: LOW → then HIGH if bit=1, LOW if bit=0. - * This saturates the timing but ensures the pin is toggled correctly. + * Schedule the full DHT22 waveform on DATA using cycle-accurate pin changes. + * + * DHT22 protocol (after MCU releases DATA HIGH): + * - 80 µs LOW → 80 µs HIGH (response preamble) + * - 40 bits, each: 50 µs LOW + (26 µs HIGH = '0', 70 µs HIGH = '1') + * - Line released HIGH after last bit + * + * At 16 MHz: 1 µs = 16 cycles + * - 80 µs = 1280 cycles, 50 µs = 800 cycles, 26 µs = 416 cycles, 70 µs = 1120 cycles */ -function driveDHT22Response(simulator: any, pin: number, element: HTMLElement): void { +function scheduleDHT22Response(simulator: any, pin: number, element: HTMLElement): void { + if (typeof simulator.schedulePinChange !== 'function') { + // Fallback: synchronous drive (legacy / non-AVR simulators) + const payload = buildDHT22Payload(element); + simulator.setPinState(pin, false); + simulator.setPinState(pin, true); + for (const byte of payload) { + for (let b = 7; b >= 0; b--) { + const bit = (byte >> b) & 1; + simulator.setPinState(pin, false); + simulator.setPinState(pin, !!bit); + } + } + simulator.setPinState(pin, true); + return; + } + const payload = buildDHT22Payload(element); - // Response preamble: drive LOW (response start) - simulator.setPinState(pin, false); - // Then HIGH (ready) - simulator.setPinState(pin, true); - // Transmit 40 bits MSB first + const now = simulator.getCurrentCycles() as number; + + // Timing constants at 16 MHz (cycles per µs = 16) + const LOW80 = 1280; // 80 µs LOW preamble + const HIGH80 = 1280; // 80 µs HIGH preamble + const LOW50 = 800; // 50 µs LOW marker before each bit + const HIGH0 = 416; // 26 µs HIGH → bit '0' + const HIGH1 = 1120; // 70 µs HIGH → bit '1' + + let t = now; + + // Preamble: 80 µs LOW then 80 µs HIGH + t += LOW80; simulator.schedulePinChange(pin, false, t); + t += HIGH80; simulator.schedulePinChange(pin, true, t); + + // 40 data bits, MSB first for (const byte of payload) { for (let b = 7; b >= 0; b--) { const bit = (byte >> b) & 1; - simulator.setPinState(pin, false); // 50 µs LOW marker - simulator.setPinState(pin, !!bit); // HIGH duration encodes 0 or 1 + t += LOW50; simulator.schedulePinChange(pin, false, t); + t += bit ? HIGH1 : HIGH0; + simulator.schedulePinChange(pin, true, t); } } - // Line idle HIGH - simulator.setPinState(pin, true); + + // Release line HIGH (it already is, but explicit for clarity) + t += LOW50; simulator.schedulePinChange(pin, false, t); + t += HIGH0; simulator.schedulePinChange(pin, true, t); } PartSimulationRegistry.register('dht22', { attachEvents: (element, simulator, getPin, componentId) => { - const pin = getPin('DATA'); + // wokwi-dht22 element uses 'SDA' as the data pin name (not 'DATA') + const pin = getPin('SDA') ?? getPin('DATA'); if (pin === null) return () => {}; let wasLow = false; @@ -424,7 +461,7 @@ PartSimulationRegistry.register('dht22', { if (wasLow) { // MCU released DATA HIGH — begin DHT22 response wasLow = false; - driveDHT22Response(simulator, pin, element); + scheduleDHT22Response(simulator, pin, element); } }, ); diff --git a/frontend/src/simulation/parts/SensorParts.ts b/frontend/src/simulation/parts/SensorParts.ts index ec0d906..da11d74 100644 --- a/frontend/src/simulation/parts/SensorParts.ts +++ b/frontend/src/simulation/parts/SensorParts.ts @@ -634,20 +634,26 @@ PartSimulationRegistry.register('hc-sr04', { simulator.setPinState(echoPin, false); // ECHO LOW initially let distanceCm = 10; // default distance in cm - let echoTimer: ReturnType | null = null; const cleanup = simulator.pinManager.onPinChange(trigPin, (_: number, state: boolean) => { - if (state) { - if (echoTimer !== null) clearTimeout(echoTimer); - // echoMs = distanceCm / 17.15 (min 1 ms for setTimeout reliability) + if (!state) return; // only react on TRIG HIGH + // HC-SR04 timing (at 16 MHz): + // - Sensor processing delay after TRIG: ~600 µs = 9600 cycles + // - Echo duration = distanceCm / 17150 s × 16 000 000 cycles/s + // (17150 cm/s = speed of sound, one-way = round-trip/2) + if (typeof simulator.schedulePinChange === 'function') { + const now = simulator.getCurrentCycles() as number; + const processingCycles = 9600; // ~600 µs sensor overhead + const echoCycles = Math.round((distanceCm / 17150) * 16_000_000); + simulator.schedulePinChange(echoPin, true, now + processingCycles); + simulator.schedulePinChange(echoPin, false, now + processingCycles + echoCycles); + console.log(`[HC-SR04] Scheduled ECHO (${distanceCm} cm, echo=${(echoCycles/16000).toFixed(1)} µs)`); + } else { + // Fallback: best-effort async (works with delay()-based sketches, not pulseIn) const echoMs = Math.max(1, distanceCm / 17.15); - echoTimer = setTimeout(() => { + setTimeout(() => { simulator.setPinState(echoPin, true); - console.log(`[HC-SR04] ECHO HIGH (${distanceCm} cm)`); - echoTimer = setTimeout(() => { - simulator.setPinState(echoPin, false); - echoTimer = null; - }, echoMs); + setTimeout(() => { simulator.setPinState(echoPin, false); }, echoMs); }, 1); } }); @@ -660,7 +666,6 @@ PartSimulationRegistry.register('hc-sr04', { return () => { cleanup(); - if (echoTimer !== null) clearTimeout(echoTimer); unregisterSensorUpdate(componentId); }; },