feat: implement DHT22 sensor support with attach, update, and detach functionality in ESP32 simulation
parent
84370e6f8d
commit
f5257009cd
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue