feat: implement DHT22 sensor support with attach, update, and detach functionality in ESP32 simulation

pull/47/head
David Montero Crespo 2026-03-22 15:17:44 -03:00
parent 84370e6f8d
commit f5257009cd
9 changed files with 405 additions and 45 deletions

View File

@ -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():

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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",

View File

@ -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 <DHT.h>
@ -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);

View File

@ -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));

View File

@ -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);
}
},

View File

@ -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<SimulatorState>((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}`);
},