diff --git a/backend/app/services/esp32_worker.py b/backend/app/services/esp32_worker.py index 707cb9f9..675f779c 100644 --- a/backend/app/services/esp32_worker.py +++ b/backend/app/services/esp32_worker.py @@ -287,6 +287,29 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) if sensor: sensor['responding'] = False + def _hcsr04_respond(trig_pin: int, echo_pin: int, distance_cm: float) -> None: + """Thread function: inject the HC-SR04 echo pulse via qemu_picsimlab_set_pin.""" + echo_slot = echo_pin + 1 # identity pinmap: slot = gpio + 1 + # Echo pulse width = distance_cm * 58 µs (speed of sound round trip) + echo_us = max(100, int(distance_cm * 58)) + + try: + # Wait for TRIG pulse to finish + propagation delay (~600 µs) + _busy_wait_us(600) + # Drive ECHO HIGH + lib.qemu_picsimlab_set_pin(echo_slot, 1) + # Hold ECHO HIGH for distance-proportional duration + _busy_wait_us(echo_us) + # Drive ECHO LOW + lib.qemu_picsimlab_set_pin(echo_slot, 0) + except Exception as exc: + _log(f'HC-SR04 respond error on TRIG {trig_pin} ECHO {echo_pin}: {exc}') + finally: + with _sensors_lock: + sensor = _sensors.get(trig_pin) + if sensor: + sensor['responding'] = False + # ── 5. ctypes callbacks (called from QEMU thread) ───────────────────────── def _on_pin_change(slot: int, value: int) -> None: @@ -298,7 +321,12 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) # Sensor protocol dispatch by type with _sensors_lock: sensor = _sensors.get(gpio) - if sensor is not None and sensor.get('type') == 'dht22': + if sensor is None: + return + + stype = sensor.get('type', '') + + if stype == 'dht22': if value == 0 and not sensor.get('responding', False): sensor['saw_low'] = True elif value == 1 and sensor.get('saw_low', False): @@ -312,6 +340,19 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) name=f'dht22-gpio{gpio}', ).start() + elif stype == 'hc-sr04': + # HC-SR04: detect TRIG going HIGH (firmware sends 10µs pulse) + if value == 1 and not sensor.get('responding', False): + sensor['responding'] = True + echo_pin = int(sensor.get('echo_pin', gpio + 1)) + distance = float(sensor.get('distance', 40.0)) + threading.Thread( + target=_hcsr04_respond, + args=(gpio, echo_pin, distance), + daemon=True, + name=f'hcsr04-gpio{gpio}', + ).start() + def _on_dir_change(slot: int, direction: int) -> None: if _stopped.is_set(): return diff --git a/frontend/src/__tests__/simulation-parts.test.ts b/frontend/src/__tests__/simulation-parts.test.ts index 6dd6d2be..c4d371ad 100644 --- a/frontend/src/__tests__/simulation-parts.test.ts +++ b/frontend/src/__tests__/simulation-parts.test.ts @@ -71,7 +71,10 @@ function makeSimulator(adc?: ReturnType | null) { pinManager, getADC: vi.fn().mockReturnValue(adc ?? null), setPinState: vi.fn(), - cpu: { data: new Uint8Array(512).fill(0) }, + isRunning: vi.fn().mockReturnValue(true), + getCurrentCycles: vi.fn().mockReturnValue(1000), + getClockHz: vi.fn().mockReturnValue(16_000_000), + cpu: { data: new Uint8Array(512).fill(0), cycles: 1000 }, }; } diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index af9b6458..e7868920 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -2034,10 +2034,25 @@ void loop() { // Blinks the built-in LED (GPIO2) and an external LED (GPIO4) // Requires arduino-esp32 2.0.17 (IDF 4.4.x) — see docs/ESP32_EMULATION.md +#include +#include +#include + #define LED_BUILTIN_PIN 2 // Built-in blue LED on ESP32 DevKit #define LED_EXT_PIN 4 // External red LED +void disableAllWDT() { + esp_task_wdt_deinit(); + TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_config0.en = 0; + TIMERG0.wdt_wprotect = 0; + TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config0.en = 0; + TIMERG1.wdt_wprotect = 0; +} + void setup() { + disableAllWDT(); Serial.begin(115200); pinMode(LED_BUILTIN_PIN, OUTPUT); pinMode(LED_EXT_PIN, OUTPUT); @@ -2045,6 +2060,7 @@ void setup() { } void loop() { + disableAllWDT(); digitalWrite(LED_BUILTIN_PIN, HIGH); digitalWrite(LED_EXT_PIN, HIGH); Serial.println("LED ON"); @@ -2076,7 +2092,22 @@ void loop() { // Echoes anything received on Serial (UART0) back to the sender. // Open the Serial Monitor, type something, and see it echoed back. +#include +#include +#include + +void disableAllWDT() { + esp_task_wdt_deinit(); + TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_config0.en = 0; + TIMERG0.wdt_wprotect = 0; + TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config0.en = 0; + TIMERG1.wdt_wprotect = 0; +} + void setup() { + disableAllWDT(); Serial.begin(115200); delay(500); Serial.println("ESP32 Serial Echo ready!"); @@ -2084,6 +2115,7 @@ void setup() { } void loop() { + disableAllWDT(); if (Serial.available()) { String input = Serial.readStringUntil('\\n'); input.trim(); @@ -2905,6 +2937,10 @@ void loop() { code: `// ESP32 — 7-Segment Display Counter 0-9 // Segments: a=12, b=13, c=14, d=25, e=26, f=27, g=32 +#include +#include +#include + const int SEG[7] = {12, 13, 14, 25, 26, 27, 32}; const bool DIGITS[10][7] = { @@ -2920,18 +2956,30 @@ const bool DIGITS[10][7] = { {1,1,1,1,0,1,1}, // 9 }; +void disableAllWDT() { + esp_task_wdt_deinit(); + TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_config0.en = 0; + TIMERG0.wdt_wprotect = 0; + TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config0.en = 0; + TIMERG1.wdt_wprotect = 0; +} + void showDigit(int d) { for (int i = 0; i < 7; i++) digitalWrite(SEG[i], DIGITS[d][i] ? HIGH : LOW); } void setup() { + disableAllWDT(); for (int i = 0; i < 7; i++) pinMode(SEG[i], OUTPUT); Serial.begin(115200); Serial.println("ESP32 7-Segment Counter"); } void loop() { + disableAllWDT(); for (int d = 0; d <= 9; d++) { showDigit(d); Serial.print("Digit: "); Serial.println(d); @@ -3770,10 +3818,25 @@ void loop() { code: `// ESP32 — HC-SR04 Ultrasonic Distance Sensor // Wiring: TRIG → D18 | ECHO → D19 | VCC → 3V3 | GND → GND +#include +#include +#include + #define TRIG_PIN 18 #define ECHO_PIN 19 +void disableAllWDT() { + esp_task_wdt_deinit(); + TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_config0.en = 0; + TIMERG0.wdt_wprotect = 0; + TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config0.en = 0; + TIMERG1.wdt_wprotect = 0; +} + void setup() { + disableAllWDT(); Serial.begin(115200); pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT); @@ -3791,6 +3854,7 @@ long measureCm() { } void loop() { + disableAllWDT(); long cm = measureCm(); if (cm < 0) Serial.println("Out of range"); else Serial.printf("Distance: %ld cm\\n", cm); @@ -3818,13 +3882,27 @@ void loop() { // Requires: Adafruit MPU6050, Adafruit Unified Sensor libraries // Wiring: SDA → D21 | SCL → D22 | VCC → 3V3 | GND → GND +#include +#include +#include #include #include #include Adafruit_MPU6050 mpu; +void disableAllWDT() { + esp_task_wdt_deinit(); + TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_config0.en = 0; + TIMERG0.wdt_wprotect = 0; + TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config0.en = 0; + TIMERG1.wdt_wprotect = 0; +} + void setup() { + disableAllWDT(); Serial.begin(115200); Wire.begin(21, 22); // SDA=21, SCL=22 if (!mpu.begin()) { @@ -3838,6 +3916,7 @@ void setup() { } void loop() { + disableAllWDT(); sensors_event_t a, g, temp; mpu.getEvent(&a, &g, &temp); @@ -3869,13 +3948,28 @@ void loop() { code: `// ESP32 — PIR Motion Sensor // Wiring: OUT → D5 | VCC → 3V3 | GND → GND +#include +#include +#include + #define PIR_PIN 5 #define LED_PIN 2 // built-in blue LED on ESP32 DevKit bool prevMotion = false; unsigned long detections = 0; +void disableAllWDT() { + esp_task_wdt_deinit(); + TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_config0.en = 0; + TIMERG0.wdt_wprotect = 0; + TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config0.en = 0; + TIMERG1.wdt_wprotect = 0; +} + void setup() { + disableAllWDT(); Serial.begin(115200); pinMode(PIR_PIN, INPUT); pinMode(LED_PIN, OUTPUT); @@ -3885,6 +3979,7 @@ void setup() { } void loop() { + disableAllWDT(); bool motion = (digitalRead(PIR_PIN) == HIGH); if (motion && !prevMotion) { detections++; @@ -3980,17 +4075,33 @@ void loop() { // Wiring: HORZ → D35 | VERT → D34 | SEL → D15 // VCC → 3V3 | GND → GND +#include +#include +#include + #define JOY_HORZ 35 // input-only ADC pin #define JOY_VERT 34 // input-only ADC pin #define JOY_BTN 15 // GPIO with pull-up +void disableAllWDT() { + esp_task_wdt_deinit(); + TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_config0.en = 0; + TIMERG0.wdt_wprotect = 0; + TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config0.en = 0; + TIMERG1.wdt_wprotect = 0; +} + void setup() { + disableAllWDT(); Serial.begin(115200); pinMode(JOY_BTN, INPUT_PULLUP); Serial.println("ESP32 Joystick ready"); } void loop() { + disableAllWDT(); int x = analogRead(JOY_HORZ); // 0–4095 int y = analogRead(JOY_VERT); // 0–4095 bool btn = (digitalRead(JOY_BTN) == LOW); diff --git a/frontend/src/simulation/parts/ProtocolParts.ts b/frontend/src/simulation/parts/ProtocolParts.ts index 40851d92..39b4798c 100644 --- a/frontend/src/simulation/parts/ProtocolParts.ts +++ b/frontend/src/simulation/parts/ProtocolParts.ts @@ -414,17 +414,18 @@ function scheduleDHT22Response(simulator: any, pin: number, element: HTMLElement const payload = buildDHT22Payload(element); const now = simulator.getCurrentCycles() as number; - // Timing constants at 16 MHz (cycles per µs = 16) - // DHT22 starts pulling the line LOW ~20 µs after MCU releases it HIGH. - // The Adafruit DHT library v1.4.7 calls expectPulse(LOW) at ~55 µs (pullTime default), - // so the preamble LOW must already be active by then. Starting at 20 µs (320 cycles) - // guarantees the pin IS LOW when the library checks. - const RESPONSE_START = 320; // 20 µs — DHT22 response start - 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' + // Scale timing by CPU clock — AVR runs at 16 MHz, RP2040 at 125 MHz. + const clockHz: number = typeof simulator.getClockHz === 'function' + ? simulator.getClockHz() + : 16_000_000; + const us = (microseconds: number) => Math.round(microseconds * clockHz / 1_000_000); + + const RESPONSE_START = us(20); // DHT22 response start (~20 µs after MCU releases) + const LOW80 = us(80); // 80 µs LOW preamble + const HIGH80 = us(80); // 80 µs HIGH preamble + const LOW50 = us(50); // 50 µs LOW marker before each bit + const HIGH0 = us(26); // 26 µs HIGH → bit '0' + const HIGH1 = us(70); // 70 µs HIGH → bit '1' let t = now + RESPONSE_START; @@ -487,9 +488,13 @@ PartSimulationRegistry.register('dht22', { // Prevent DHT22's own scheduled pin changes from re-triggering the response. // After the MCU releases DATA HIGH and we begin responding, we ignore all // pin-change callbacks until the full waveform has been emitted. - // DHT22 response is ~5 ms; at 16 MHz that is ~80 000 cycles. We gate for - // 200 000 cycles (~12.5 ms) to give plenty of headroom. - const RESPONSE_GATE_CYCLES = 200_000; + // DHT22 response is ~5 ms; gate for ~12.5 ms scaled to the CPU clock. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const clockHz: number = typeof (simulator as any).getClockHz === 'function' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ? (simulator as any).getClockHz() + : 16_000_000; + const RESPONSE_GATE_CYCLES = Math.round(12_500 * clockHz / 1_000_000); let responseEndCycle = 0; let responseEndTimeMs = 0; // time-based fallback for ESP32 (no cycle counter) diff --git a/frontend/src/simulation/parts/SensorParts.ts b/frontend/src/simulation/parts/SensorParts.ts index 8330e252..240b7cdc 100644 --- a/frontend/src/simulation/parts/SensorParts.ts +++ b/frontend/src/simulation/parts/SensorParts.ts @@ -631,18 +631,46 @@ PartSimulationRegistry.register('hc-sr04', { const echoPin = getArduinoPinHelper('ECHO'); if (trigPin === null || echoPin === null) return () => {}; - simulator.setPinState(echoPin, false); // ECHO LOW initially + const el = element as any; + let distanceCm = parseFloat(el.distance) || 10; // default distance in cm - let distanceCm = 10; // default distance in cm + // ── ESP32 path: delegate protocol to backend QEMU worker ── + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handledNatively = typeof (simulator as any).registerSensor === 'function' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + && (simulator as any).registerSensor('hc-sr04', trigPin, { + distance: distanceCm, + echo_pin: echoPin, + }); + + if (handledNatively) { + registerSensorUpdate(componentId, (values) => { + if ('distance' in values) { + distanceCm = Math.max(2, Math.min(400, values.distance as number)); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (simulator as any).updateSensor(trigPin, { + distance: distanceCm, + echo_pin: echoPin, + }); + }); + + return () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (simulator as any).unregisterSensor(trigPin); + unregisterSensorUpdate(componentId); + }; + } + + // ── AVR / RP2040 path: local pin scheduling ── + simulator.setPinState(echoPin, false); // ECHO LOW initially const cleanup = simulator.pinManager.onPinChange(trigPin, (_: number, state: boolean) => { 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') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const clockHz: number = typeof (simulator as any).getClockHz === 'function' + // eslint-disable-next-line @typescript-eslint/no-explicit-any ? (simulator as any).getClockHz() : 16_000_000; const now = simulator.getCurrentCycles() as number; diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 957d5bf4..4b99cd94 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -24,8 +24,10 @@ const SENSOR_COMPONENT_MAP: Record; // extra pin mappings: prop name → component pin name }> = { 'dht22': { sensorType: 'dht22', dataPinName: 'SDA', propertyKeys: ['temperature', 'humidity'] }, + 'hc-sr04': { sensorType: 'hc-sr04', dataPinName: 'TRIG', propertyKeys: ['distance'], extraPins: { echo_pin: 'ECHO' } }, }; // ── Legacy type aliases (keep external consumers working) ────────────────── @@ -590,6 +592,24 @@ export const useSimulatorStore = create((set, get) => { const val = comp.properties[key]; if (val !== undefined) props[key] = typeof val === 'string' ? parseFloat(val) : val; } + // Resolve extra pins (e.g. echo_pin for HC-SR04) from wires + if (sensorDef.extraPins) { + for (const [propName, compPinName] of Object.entries(sensorDef.extraPins)) { + for (const ew of wires) { + const epComp = (ew.start.componentId === comp.id && ew.start.pinName === compPinName) + ? ew.start : (ew.end.componentId === comp.id && ew.end.pinName === compPinName) + ? ew.end : null; + if (!epComp) continue; + const epBoard = epComp === ew.start ? ew.end : ew.start; + if (!isBoardComponent(epBoard.componentId)) continue; + const extraGpio = boardPinToNumber(board.boardKind, epBoard.pinName); + if (extraGpio !== null && extraGpio >= 0) { + props[propName] = extraGpio; + } + break; + } + } + } sensors.push(props); break; // only one data pin per sensor }