diff --git a/backend/app/api/routes/simulation.py b/backend/app/api/routes/simulation.py index 07d8eb3..9a113e4 100644 --- a/backend/app/api/routes/simulation.py +++ b/backend/app/api/routes/simulation.py @@ -173,6 +173,32 @@ async def simulation_websocket(websocket: WebSocket, client_id: str): client_id, bytes(raw_bytes), uart_id=2 ) + # ── ESP32 DHT22 sensor (backend-side protocol emulation) ───── + elif msg_type == 'esp32_dht22_attach': + pin = int(msg_data.get('pin', 0)) + temperature = float(msg_data.get('temperature', 25.0)) + humidity = float(msg_data.get('humidity', 50.0)) + if _use_lib(): + esp_lib_manager.dht22_attach(client_id, pin, temperature, humidity) + else: + esp_qemu_manager.dht22_attach(client_id, pin, temperature, humidity) + + elif msg_type == 'esp32_dht22_update': + pin = int(msg_data.get('pin', 0)) + temperature = float(msg_data.get('temperature', 25.0)) + humidity = float(msg_data.get('humidity', 50.0)) + if _use_lib(): + esp_lib_manager.dht22_update(client_id, pin, temperature, humidity) + else: + esp_qemu_manager.dht22_update(client_id, pin, temperature, humidity) + + elif msg_type == 'esp32_dht22_detach': + pin = int(msg_data.get('pin', 0)) + if _use_lib(): + esp_lib_manager.dht22_detach(client_id, pin) + else: + esp_qemu_manager.dht22_detach(client_id, pin) + # ── ESP32 status query ──────────────────────────────────────── elif msg_type == 'esp32_status': if _use_lib(): diff --git a/backend/app/services/esp32_lib_manager.py b/backend/app/services/esp32_lib_manager.py index d47c65b..181c384 100644 --- a/backend/app/services/esp32_lib_manager.py +++ b/backend/app/services/esp32_lib_manager.py @@ -343,6 +343,31 @@ class EspLibManager: if inst and inst.running and inst.process.returncode is None: self._write_cmd(inst, {'cmd': 'set_spi_response', 'response': response_byte & 0xFF}) + # ── DHT22 sensor (backend-side protocol emulation) ───────────────────── + + def dht22_attach(self, client_id: str, pin: int, temperature: float, humidity: float) -> None: + """Register a DHT22 sensor on a GPIO pin — the worker handles the protocol.""" + with self._instances_lock: + inst = self._instances.get(client_id) + if inst and inst.running and inst.process.returncode is None: + self._write_cmd(inst, {'cmd': 'dht22_attach', 'pin': pin, + 'temperature': temperature, 'humidity': humidity}) + + def dht22_update(self, client_id: str, pin: int, temperature: float, humidity: float) -> None: + """Update a DHT22 sensor's temperature and humidity values.""" + with self._instances_lock: + inst = self._instances.get(client_id) + if inst and inst.running and inst.process.returncode is None: + self._write_cmd(inst, {'cmd': 'dht22_update', 'pin': pin, + 'temperature': temperature, 'humidity': humidity}) + + def dht22_detach(self, client_id: str, pin: int) -> None: + """Remove a DHT22 sensor from a GPIO pin.""" + with self._instances_lock: + inst = self._instances.get(client_id) + if inst and inst.running and inst.process.returncode is None: + self._write_cmd(inst, {'cmd': 'dht22_detach', 'pin': pin}) + # ── LEDC polling (no-op: worker polls automatically) ───────────────────── async def poll_ledc(self, client_id: str) -> None: diff --git a/backend/app/services/esp32_worker.py b/backend/app/services/esp32_worker.py index a465efb..dc4256c 100644 --- a/backend/app/services/esp32_worker.py +++ b/backend/app/services/esp32_worker.py @@ -40,6 +40,7 @@ import os import sys import tempfile import threading +import time # ─── stdout helpers ────────────────────────────────────────────────────────── @@ -228,6 +229,62 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) # ESP32 signal indices: 72-79 = LEDC HS ch 0-7, 80-87 = LEDC LS ch 0-7 _ledc_gpio_map: dict[int, int] = {} + # DHT22 sensor state: gpio_pin → {temperature, humidity, saw_low, responding} + _dht22_sensors: dict[int, dict] = {} + _dht22_lock = threading.Lock() + + def _busy_wait_us(us: int) -> None: + """Busy-wait for the given number of microseconds using perf_counter_ns.""" + end = time.perf_counter_ns() + us * 1000 + while time.perf_counter_ns() < end: + pass + + def _dht22_build_payload(temperature: float, humidity: float) -> list[int]: + """Build 5-byte DHT22 data payload: [hum_H, hum_L, temp_H, temp_L, checksum].""" + hum = round(humidity * 10) + tmp = round(temperature * 10) + h_H = (hum >> 8) & 0xFF + h_L = hum & 0xFF + raw_t = ((-tmp) & 0x7FFF) | 0x8000 if tmp < 0 else tmp & 0x7FFF + t_H = (raw_t >> 8) & 0xFF + t_L = raw_t & 0xFF + chk = (h_H + h_L + t_H + t_L) & 0xFF + return [h_H, h_L, t_H, t_L, chk] + + def _dht22_respond(gpio_pin: int, temperature: float, humidity: float) -> None: + """Thread function: inject the DHT22 protocol waveform via qemu_picsimlab_set_pin.""" + slot = gpio_pin + 1 # identity pinmap: slot = gpio + 1 + payload = _dht22_build_payload(temperature, humidity) + + try: + # Preamble: 80 µs LOW → 80 µs HIGH (use 2x margins for QEMU speed variation) + lib.qemu_picsimlab_set_pin(slot, 0) + _busy_wait_us(160) + lib.qemu_picsimlab_set_pin(slot, 1) + _busy_wait_us(160) + + # 40 data bits: 50 µs LOW + (26 µs HIGH = 0, 70 µs HIGH = 1) + # Use 2x margins: 100 µs LOW, 52 µs HIGH (0) / 140 µs HIGH (1) + for byte_val in payload: + for b in range(7, -1, -1): + bit = (byte_val >> b) & 1 + lib.qemu_picsimlab_set_pin(slot, 0) + _busy_wait_us(100) + lib.qemu_picsimlab_set_pin(slot, 1) + _busy_wait_us(140 if bit else 52) + + # Final: release line HIGH + lib.qemu_picsimlab_set_pin(slot, 0) + _busy_wait_us(100) + lib.qemu_picsimlab_set_pin(slot, 1) + except Exception as exc: + _log(f'DHT22 respond error on GPIO {gpio_pin}: {exc}') + finally: + with _dht22_lock: + sensor = _dht22_sensors.get(gpio_pin) + if sensor: + sensor['responding'] = False + # ── 5. ctypes callbacks (called from QEMU thread) ───────────────────────── def _on_pin_change(slot: int, value: int) -> None: @@ -236,6 +293,22 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) gpio = int(_PINMAP[slot]) if 1 <= slot <= _GPIO_COUNT else slot _emit({'type': 'gpio_change', 'pin': gpio, 'state': value}) + # DHT22: detect start signal (firmware drives pin LOW then HIGH) + with _dht22_lock: + sensor = _dht22_sensors.get(gpio) + if sensor is not None: + if value == 0 and not sensor.get('responding', False): + sensor['saw_low'] = True + elif value == 1 and sensor.get('saw_low', False): + sensor['saw_low'] = False + sensor['responding'] = True + threading.Thread( + target=_dht22_respond, + args=(gpio, sensor['temperature'], sensor['humidity']), + daemon=True, + name=f'dht22-gpio{gpio}', + ).start() + def _on_dir_change(slot: int, direction: int) -> None: if _stopped.is_set(): return @@ -404,6 +477,33 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) elif c == 'set_spi_response': _spi_response[0] = int(cmd['response']) & 0xFF + elif c == 'dht22_attach': + gpio = int(cmd['pin']) + with _dht22_lock: + _dht22_sensors[gpio] = { + 'temperature': float(cmd.get('temperature', 25.0)), + 'humidity': float(cmd.get('humidity', 50.0)), + 'saw_low': False, + 'responding': False, + } + _log(f'DHT22 attached on GPIO {gpio}') + + elif c == 'dht22_update': + gpio = int(cmd['pin']) + with _dht22_lock: + sensor = _dht22_sensors.get(gpio) + if sensor: + if 'temperature' in cmd: + sensor['temperature'] = float(cmd['temperature']) + if 'humidity' in cmd: + sensor['humidity'] = float(cmd['humidity']) + + elif c == 'dht22_detach': + gpio = int(cmd['pin']) + with _dht22_lock: + _dht22_sensors.pop(gpio, None) + _log(f'DHT22 detached from GPIO {gpio}') + elif c == 'stop': _stopped.set() # Signal QEMU to shut down. The assertion that fires on Windows diff --git a/backend/app/services/esp_qemu_manager.py b/backend/app/services/esp_qemu_manager.py index 61642f4..1396263 100644 --- a/backend/app/services/esp_qemu_manager.py +++ b/backend/app/services/esp_qemu_manager.py @@ -27,6 +27,7 @@ import logging import os import socket import tempfile +import time from typing import Callable, Awaitable logger = logging.getLogger(__name__) @@ -69,6 +70,9 @@ class EspInstance: self._tasks: list[asyncio.Task] = [] self.running: bool = False + # DHT22 sensor state: gpio_pin → {temperature, humidity, saw_low, responding} + self.dht22_sensors: dict[int, dict] = {} + async def emit(self, event_type: str, data: dict) -> None: try: await self.callback(event_type, data) @@ -121,6 +125,33 @@ class EspQemuManager: if inst and inst._gpio_writer: asyncio.create_task(self._send_gpio(inst, int(pin), bool(state))) + # ── DHT22 sensor API ────────────────────────────────────────────────────── + + def dht22_attach(self, client_id: str, pin: int, + temperature: float, humidity: float) -> None: + inst = self._instances.get(client_id) + if inst: + inst.dht22_sensors[pin] = { + 'temperature': temperature, + 'humidity': humidity, + 'saw_low': False, + 'responding': False, + } + logger.info('[%s] DHT22 attached on GPIO %d (T=%.1f H=%.1f)', + client_id, pin, temperature, humidity) + + def dht22_update(self, client_id: str, pin: int, + temperature: float, humidity: float) -> None: + inst = self._instances.get(client_id) + if inst and pin in inst.dht22_sensors: + inst.dht22_sensors[pin]['temperature'] = temperature + inst.dht22_sensors[pin]['humidity'] = humidity + + def dht22_detach(self, client_id: str, pin: int) -> None: + inst = self._instances.get(client_id) + if inst: + inst.dht22_sensors.pop(pin, None) + async def send_serial_bytes(self, client_id: str, data: bytes) -> None: inst = self._instances.get(client_id) if inst and inst._serial_writer: @@ -263,9 +294,102 @@ class EspQemuManager: pin = int(parts[1]) state = int(parts[2]) await inst.emit('gpio_change', {'pin': pin, 'state': state}) + + # DHT22: detect start signal (firmware drives pin LOW then HIGH) + sensor = inst.dht22_sensors.get(pin) + if sensor is not None: + if state == 0 and not sensor.get('responding', False): + sensor['saw_low'] = True + elif state == 1 and sensor.get('saw_low', False): + sensor['saw_low'] = False + sensor['responding'] = True + asyncio.create_task( + self._dht22_respond(inst, pin, + sensor['temperature'], + sensor['humidity']) + ) except ValueError: pass + # ── DHT22 protocol emulation ──────────────────────────────────────────── + + @staticmethod + def _dht22_build_payload(temperature: float, humidity: float) -> list[int]: + """Build 5-byte DHT22 data payload: [hum_H, hum_L, temp_H, temp_L, checksum].""" + hum = round(humidity * 10) + tmp = round(temperature * 10) + h_H = (hum >> 8) & 0xFF + h_L = hum & 0xFF + raw_t = ((-tmp) & 0x7FFF) | 0x8000 if tmp < 0 else tmp & 0x7FFF + t_H = (raw_t >> 8) & 0xFF + t_L = raw_t & 0xFF + chk = (h_H + h_L + t_H + t_L) & 0xFF + return [h_H, h_L, t_H, t_L, chk] + + @staticmethod + def _busy_wait_us(us: int) -> None: + """Busy-wait for the given number of microseconds using perf_counter_ns.""" + end = time.perf_counter_ns() + us * 1000 + while time.perf_counter_ns() < end: + pass + + def _dht22_respond_sync(self, inst: EspInstance, gpio_pin: int, + temperature: float, humidity: float) -> None: + """Thread function: inject DHT22 protocol waveform via GPIO SET commands. + + Uses synchronous socket writes + busy-wait to achieve µs-level timing. + asyncio.sleep() is too coarse (15ms on Windows) for this protocol. + """ + payload = self._dht22_build_payload(temperature, humidity) + + def _send_pin(state: bool) -> None: + """Synchronous GPIO write directly on the TCP socket.""" + if inst._gpio_writer and inst._gpio_writer.transport: + msg = f'SET {gpio_pin} {1 if state else 0}\n'.encode() + inst._gpio_writer.transport.write(msg) + + try: + # Preamble: 80 µs LOW → 80 µs HIGH (use 2x margins for QEMU speed) + _send_pin(False) + self._busy_wait_us(160) + _send_pin(True) + self._busy_wait_us(160) + + # 40 data bits: 50 µs LOW + (26 µs HIGH = 0, 70 µs HIGH = 1) + # Use 2x margins: 100 µs LOW, 52 µs HIGH (0) / 140 µs HIGH (1) + for byte_val in payload: + for b in range(7, -1, -1): + bit = (byte_val >> b) & 1 + _send_pin(False) + self._busy_wait_us(100) + _send_pin(True) + self._busy_wait_us(140 if bit else 52) + + # Final: release line HIGH + _send_pin(False) + self._busy_wait_us(100) + _send_pin(True) + logger.debug('[%s] DHT22 response sent on GPIO %d', inst.client_id, gpio_pin) + except Exception as exc: + logger.warning('[%s] DHT22 respond error on GPIO %d: %s', + inst.client_id, gpio_pin, exc) + finally: + sensor = inst.dht22_sensors.get(gpio_pin) + if sensor: + sensor['responding'] = False + + async def _dht22_respond(self, inst: EspInstance, gpio_pin: int, + temperature: float, humidity: float) -> None: + """Run the DHT22 response in a thread for accurate µs timing.""" + import threading + t = threading.Thread( + target=self._dht22_respond_sync, + args=(inst, gpio_pin, temperature, humidity), + daemon=True, + name=f'dht22-gpio{gpio_pin}', + ) + t.start() + async def _send_gpio(self, inst: EspInstance, pin: int, state: bool) -> None: if inst._gpio_writer: msg = f'SET {pin} {1 if state else 0}\n'.encode() diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 8d32fd7..7eed683 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "generatedAt": "2026-03-21T20:08:10.580Z", + "generatedAt": "2026-03-22T15:21:16.907Z", "components": [ { "id": "arduino-mega", diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 0ea5fec..64f2c2b 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -3706,7 +3706,7 @@ void loop() { boardFilter: 'esp32', code: `// ESP32 — DHT22 Temperature & Humidity Sensor // Requires: Adafruit DHT sensor library -// Wiring: DATA → D4 | VCC → 3V3 | GND → GND +// Wiring: DATA → GPIO4 | VCC → 3V3 | GND → GND #include @@ -3717,17 +3717,22 @@ DHT dht(DHT_PIN, DHT_TYPE); void setup() { Serial.begin(115200); + // Disable watchdog — DHT pulseIn() can block in emulation + disableCore0WDT(); + disableCore1WDT(); dht.begin(); - delay(1000); + delay(2000); Serial.println("ESP32 DHT22 ready!"); } void loop() { delay(2000); + float h = dht.readHumidity(); float t = dht.readTemperature(); + if (isnan(h) || isnan(t)) { - Serial.println("DHT22 read error!"); + Serial.println("DHT22: waiting for sensor..."); return; } Serial.printf("Temp: %.1f C Humidity: %.1f %%\\n", t, h); @@ -3736,9 +3741,9 @@ void loop() { { type: 'wokwi-dht22', id: 'e32-dht1', x: 430, y: 150, properties: { temperature: '28', humidity: '65' } }, ], wires: [ - { id: 'e32d-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-dht1', pinName: 'VCC' }, color: '#ff4444' }, - { id: 'e32d-gnd', start: { componentId: 'esp32', pinName: 'GND.1'}, end: { componentId: 'e32-dht1', pinName: 'GND' }, color: '#000000' }, - { id: 'e32d-sda', start: { componentId: 'esp32', pinName: 'D4' }, end: { componentId: 'e32-dht1', pinName: 'SDA' }, color: '#22aaff' }, + { id: 'e32d-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-dht1', pinName: 'VCC' }, color: '#ff4444' }, + { id: 'e32d-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'e32-dht1', pinName: 'GND' }, color: '#000000' }, + { id: 'e32d-sda', start: { componentId: 'esp32', pinName: '4' }, end: { componentId: 'e32-dht1', pinName: 'SDA' }, color: '#22aaff' }, ], }, { @@ -3783,9 +3788,9 @@ void loop() { ], wires: [ { id: 'e32s-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-sr1', pinName: 'VCC' }, color: '#ff4444' }, - { id: 'e32s-gnd', start: { componentId: 'esp32', pinName: 'GND.1' }, end: { componentId: 'e32-sr1', pinName: 'GND' }, color: '#000000' }, - { id: 'e32s-trig', start: { componentId: 'esp32', pinName: 'D18' }, end: { componentId: 'e32-sr1', pinName: 'TRIG' }, color: '#ff8800' }, - { id: 'e32s-echo', start: { componentId: 'esp32', pinName: 'D19' }, end: { componentId: 'e32-sr1', pinName: 'ECHO' }, color: '#22cc22' }, + { id: 'e32s-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'e32-sr1', pinName: 'GND' }, color: '#000000' }, + { id: 'e32s-trig', start: { componentId: 'esp32', pinName: '18' }, end: { componentId: 'e32-sr1', pinName: 'TRIG' }, color: '#ff8800' }, + { id: 'e32s-echo', start: { componentId: 'esp32', pinName: '19' }, end: { componentId: 'e32-sr1', pinName: 'ECHO' }, color: '#22cc22' }, ], }, { @@ -3835,9 +3840,9 @@ void loop() { ], wires: [ { id: 'e32m-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-mpu1', pinName: 'VCC' }, color: '#ff4444' }, - { id: 'e32m-gnd', start: { componentId: 'esp32', pinName: 'GND.1' }, end: { componentId: 'e32-mpu1', pinName: 'GND' }, color: '#000000' }, - { id: 'e32m-sda', start: { componentId: 'esp32', pinName: 'D21' }, end: { componentId: 'e32-mpu1', pinName: 'SDA' }, color: '#22aaff' }, - { id: 'e32m-scl', start: { componentId: 'esp32', pinName: 'D22' }, end: { componentId: 'e32-mpu1', pinName: 'SCL' }, color: '#ff8800' }, + { id: 'e32m-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'e32-mpu1', pinName: 'GND' }, color: '#000000' }, + { id: 'e32m-sda', start: { componentId: 'esp32', pinName: '21' }, end: { componentId: 'e32-mpu1', pinName: 'SDA' }, color: '#22aaff' }, + { id: 'e32m-scl', start: { componentId: 'esp32', pinName: '22' }, end: { componentId: 'e32-mpu1', pinName: 'SCL' }, color: '#ff8800' }, ], }, { @@ -3885,8 +3890,8 @@ void loop() { ], wires: [ { id: 'e32p-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-pir1', pinName: 'VCC' }, color: '#ff4444' }, - { id: 'e32p-gnd', start: { componentId: 'esp32', pinName: 'GND.1' }, end: { componentId: 'e32-pir1', pinName: 'GND' }, color: '#000000' }, - { id: 'e32p-out', start: { componentId: 'esp32', pinName: 'D5' }, end: { componentId: 'e32-pir1', pinName: 'OUT' }, color: '#ffcc00' }, + { id: 'e32p-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'e32-pir1', pinName: 'GND' }, color: '#000000' }, + { id: 'e32p-out', start: { componentId: 'esp32', pinName: '5' }, end: { componentId: 'e32-pir1', pinName: 'OUT' }, color: '#ffcc00' }, ], }, { @@ -3927,11 +3932,11 @@ void loop() { ], wires: [ { id: 'e32sv-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-sv1', pinName: 'V+' }, color: '#ff4444' }, - { id: 'e32sv-gnd', start: { componentId: 'esp32', pinName: 'GND.1' }, end: { componentId: 'e32-sv1', pinName: 'GND' }, color: '#000000' }, - { id: 'e32sv-pwm', start: { componentId: 'esp32', pinName: 'D13' }, end: { componentId: 'e32-sv1', pinName: 'PWM' }, color: '#ff8800' }, - { id: 'e32pt-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-pot1', pinName: 'VCC' }, color: '#ff4444' }, - { id: 'e32pt-gnd', start: { componentId: 'esp32', pinName: 'GND.1' }, end: { componentId: 'e32-pot1', pinName: 'GND' }, color: '#000000' }, - { id: 'e32pt-sig', start: { componentId: 'esp32', pinName: 'D34' }, end: { componentId: 'e32-pot1', pinName: 'SIG' }, color: '#aa44ff' }, + { id: 'e32sv-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'e32-sv1', pinName: 'GND' }, color: '#000000' }, + { id: 'e32sv-pwm', start: { componentId: 'esp32', pinName: '13' }, end: { componentId: 'e32-sv1', pinName: 'PWM' }, color: '#ff8800' }, + { id: 'e32pt-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-pot1', pinName: 'VCC' }, color: '#ff4444' }, + { id: 'e32pt-gnd', start: { componentId: 'esp32', pinName: 'GND2' }, end: { componentId: 'e32-pot1', pinName: 'GND' }, color: '#000000' }, + { id: 'e32pt-sig', start: { componentId: 'esp32', pinName: '34' }, end: { componentId: 'e32-pot1', pinName: 'SIG' }, color: '#aa44ff' }, ], }, { @@ -3973,10 +3978,10 @@ void loop() { ], wires: [ { id: 'e32j-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-joy1', pinName: 'VCC' }, color: '#ff4444' }, - { id: 'e32j-gnd', start: { componentId: 'esp32', pinName: 'GND.1' }, end: { componentId: 'e32-joy1', pinName: 'GND' }, color: '#000000' }, - { id: 'e32j-vert', start: { componentId: 'esp32', pinName: 'D34' }, end: { componentId: 'e32-joy1', pinName: 'VERT' }, color: '#22aaff' }, - { id: 'e32j-horz', start: { componentId: 'esp32', pinName: 'D35' }, end: { componentId: 'e32-joy1', pinName: 'HORZ' }, color: '#22cc44' }, - { id: 'e32j-sel', start: { componentId: 'esp32', pinName: 'D15' }, end: { componentId: 'e32-joy1', pinName: 'SEL' }, color: '#aa44ff' }, + { id: 'e32j-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'e32-joy1', pinName: 'GND' }, color: '#000000' }, + { id: 'e32j-vert', start: { componentId: 'esp32', pinName: '34' }, end: { componentId: 'e32-joy1', pinName: 'VERT' }, color: '#22aaff' }, + { id: 'e32j-horz', start: { componentId: 'esp32', pinName: '35' }, end: { componentId: 'e32-joy1', pinName: 'HORZ' }, color: '#22cc44' }, + { id: 'e32j-sel', start: { componentId: 'esp32', pinName: '15' }, end: { componentId: 'e32-joy1', pinName: 'SEL' }, color: '#aa44ff' }, ], }, @@ -4002,17 +4007,21 @@ DHT dht(DHT_PIN, DHT_TYPE); void setup() { Serial.begin(115200); + // Disable watchdog — DHT pulseIn() can block in emulation + disableCore0WDT(); dht.begin(); - delay(1000); + delay(2000); Serial.println("ESP32-C3 DHT22 ready!"); } void loop() { delay(2000); + float h = dht.readHumidity(); float t = dht.readTemperature(); + if (isnan(h) || isnan(t)) { - Serial.println("DHT22 read error!"); + Serial.println("DHT22: waiting for sensor..."); return; } Serial.printf("Temp: %.1f C Humidity: %.1f %%\\n", t, h); diff --git a/frontend/src/simulation/Esp32Bridge.ts b/frontend/src/simulation/Esp32Bridge.ts index 8c0f601..a66821f 100644 --- a/frontend/src/simulation/Esp32Bridge.ts +++ b/frontend/src/simulation/Esp32Bridge.ts @@ -244,6 +244,21 @@ export class Esp32Bridge { this._send({ type: 'esp32_spi_response', data: { response } }); } + /** Attach a DHT22 sensor to a GPIO pin — backend will handle the protocol */ + dht22Attach(pin: number, temperature: number, humidity: number): void { + this._send({ type: 'esp32_dht22_attach', data: { pin, temperature, humidity } }); + } + + /** Update DHT22 sensor readings */ + dht22Update(pin: number, temperature: number, humidity: number): void { + this._send({ type: 'esp32_dht22_update', data: { pin, temperature, humidity } }); + } + + /** Detach the DHT22 sensor from a GPIO pin */ + dht22Detach(pin: number): void { + this._send({ type: 'esp32_dht22_detach', data: { pin } }); + } + private _send(payload: unknown): void { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(payload)); diff --git a/frontend/src/simulation/parts/ProtocolParts.ts b/frontend/src/simulation/parts/ProtocolParts.ts index f2ef7c1..6c3fefc 100644 --- a/frontend/src/simulation/parts/ProtocolParts.ts +++ b/frontend/src/simulation/parts/ProtocolParts.ts @@ -458,6 +458,28 @@ PartSimulationRegistry.register('dht22', { const pin = getPin('SDA') ?? getPin('DATA'); if (pin === null) return () => {}; + // ESP32: delegate DHT22 protocol to the backend QEMU worker. + // The frontend can't provide µs-level GPIO timing over WebSocket. + const sim = simulator as any; + if (sim.isEsp32) { + const bridge = sim.getBridge(); + const el = element as any; + const temperature = el.temperature ?? 25.0; + const humidity = el.humidity ?? 50.0; + bridge.dht22Attach(pin, temperature, humidity); + + registerSensorUpdate(componentId, (values) => { + if ('temperature' in values) el.temperature = values.temperature as number; + if ('humidity' in values) el.humidity = values.humidity as number; + bridge.dht22Update(pin, el.temperature ?? 25.0, el.humidity ?? 50.0); + }); + + return () => { + bridge.dht22Detach(pin); + unregisterSensorUpdate(componentId); + }; + } + let wasLow = false; // 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 @@ -466,6 +488,7 @@ PartSimulationRegistry.register('dht22', { // 200 000 cycles (~12.5 ms) to give plenty of headroom. const RESPONSE_GATE_CYCLES = 200_000; let responseEndCycle = 0; + let responseEndTimeMs = 0; // time-based fallback for ESP32 (no cycle counter) const getCycles = (): number => typeof (simulator as any).getCurrentCycles === 'function' @@ -478,6 +501,8 @@ PartSimulationRegistry.register('dht22', { // While DHT22 is driving the line, ignore our own scheduled changes. const now = getCycles(); if (now >= 0 && now < responseEndCycle) return; + // Time-based fallback for ESP32 (no cycle counter available) + if (now < 0 && Date.now() < responseEndTimeMs) return; if (!state) { // MCU drove DATA LOW — start signal detected @@ -489,6 +514,7 @@ PartSimulationRegistry.register('dht22', { wasLow = false; const cur = getCycles(); responseEndCycle = cur >= 0 ? cur + RESPONSE_GATE_CYCLES : 0; + responseEndTimeMs = Date.now() + 20; // 20ms gate for non-cycle simulators scheduleDHT22Response(simulator, pin, element); } }, diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 39988d7..b65edbd 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -50,6 +50,8 @@ class Esp32BridgeShim { } setPinState(pin: number, state: boolean): void { this.bridge.sendPinEvent(pin, state); } + getBridge(): Esp32Bridge { return this.bridge; } + get isEsp32(): boolean { return true; } getCurrentCycles(): number { return -1; } getClockHz(): number { return 240_000_000; } isRunning(): boolean { return this.bridge.connected; } @@ -737,26 +739,59 @@ export const useSimulatorStore = create((set, get) => { getBoardSimulator(boardId)?.stop(); simulatorMap.delete(boardId); + getEsp32Bridge(boardId)?.disconnect(); + esp32BridgeMap.delete(boardId); - const sim = createSimulator( - boardType as BoardKind, - pm, - (ch) => set((s) => { - const boards = s.boards.map((b) => - b.id === boardId ? { ...b, serialOutput: b.serialOutput + ch } : b - ); - return { boards, serialOutput: s.serialOutput + ch }; - }), - (baud) => set((s) => { - const boards = s.boards.map((b) => - b.id === boardId ? { ...b, serialBaudRate: baud } : b - ); - return { boards, serialBaudRate: baud }; - }), - getOscilloscopeCallback(), - ); - simulatorMap.set(boardId, sim); - set({ simulator: sim, serialOutput: '', serialBaudRate: 0 }); + const serialCallback = (ch: string) => set((s) => { + const boards = s.boards.map((b) => + b.id === boardId ? { ...b, serialOutput: b.serialOutput + ch } : b + ); + return { boards, serialOutput: s.serialOutput + ch }; + }); + + if (isEsp32Kind(boardType as BoardKind)) { + // ESP32: create bridge + shim (same as setBoardType) + const bridge = new Esp32Bridge(boardId, boardType as BoardKind); + bridge.onSerialData = serialCallback; + bridge.onPinChange = (gpioPin, state) => { + const boardPm = pinManagerMap.get(boardId); + if (boardPm) boardPm.triggerPinChange(gpioPin, state); + }; + bridge.onCrash = () => { set({ esp32CrashBoardId: boardId }); }; + bridge.onDisconnected = () => { + set((s) => { + const boards = s.boards.map((b) => b.id === boardId ? { ...b, running: false } : b); + const isActive = s.activeBoardId === boardId; + return { boards, ...(isActive ? { running: false } : {}) }; + }); + }; + bridge.onLedcUpdate = (update) => { + const boardPm = pinManagerMap.get(boardId); + if (boardPm && typeof boardPm.updatePwm === 'function') { + boardPm.updatePwm(update.channel, update.duty_pct); + } + }; + esp32BridgeMap.set(boardId, bridge); + const shim = new Esp32BridgeShim(bridge, pm); + shim.onSerialData = serialCallback; + simulatorMap.set(boardId, shim); + set({ simulator: shim as any, serialOutput: '', serialBaudRate: 0 }); + } else { + const sim = createSimulator( + boardType as BoardKind, + pm, + serialCallback, + (baud) => set((s) => { + const boards = s.boards.map((b) => + b.id === boardId ? { ...b, serialBaudRate: baud } : b + ); + return { boards, serialBaudRate: baud }; + }), + getOscilloscopeCallback(), + ); + simulatorMap.set(boardId, sim); + set({ simulator: sim, serialOutput: '', serialBaudRate: 0 }); + } console.log(`Simulator initialized: ${boardType}`); },