feat: Implement generic sensor registration for ESP32 and RP2040
- Added board-agnostic sensor registration methods in RP2040Simulator. - Enhanced ComplexParts to handle LEDC PWM duty updates for ESP32. - Updated ProtocolParts to check if the simulator handles sensor protocols natively, delegating to backend if applicable. - Introduced pre-registration of sensors in useSimulatorStore for ESP32 to prevent race conditions. - Added tests for ESP32 DHT22 sensor registration flow, ensuring proper delegation and fallback mechanisms. - Created tests for ESP32 Servo and Potentiometer interactions, verifying PWM subscriptions and ADC handling.pull/47/head
parent
f5257009cd
commit
6a55f58e46
|
|
@ -81,12 +81,13 @@ async def simulation_websocket(websocket: WebSocket, client_id: str):
|
||||||
elif msg_type == 'start_esp32':
|
elif msg_type == 'start_esp32':
|
||||||
board = msg_data.get('board', 'esp32')
|
board = msg_data.get('board', 'esp32')
|
||||||
firmware_b64 = msg_data.get('firmware_b64')
|
firmware_b64 = msg_data.get('firmware_b64')
|
||||||
|
sensors = msg_data.get('sensors', [])
|
||||||
fw_size_kb = round(len(firmware_b64) * 0.75 / 1024) if firmware_b64 else 0
|
fw_size_kb = round(len(firmware_b64) * 0.75 / 1024) if firmware_b64 else 0
|
||||||
lib_available = _use_lib()
|
lib_available = _use_lib()
|
||||||
logger.info('[%s] start_esp32 board=%s firmware=%dKB lib_available=%s',
|
logger.info('[%s] start_esp32 board=%s firmware=%dKB lib_available=%s sensors=%d',
|
||||||
client_id, board, fw_size_kb, lib_available)
|
client_id, board, fw_size_kb, lib_available, len(sensors))
|
||||||
if lib_available:
|
if lib_available:
|
||||||
await esp_lib_manager.start_instance(client_id, board, qemu_callback, firmware_b64)
|
await esp_lib_manager.start_instance(client_id, board, qemu_callback, firmware_b64, sensors)
|
||||||
else:
|
else:
|
||||||
logger.warning('[%s] libqemu-xtensa not available — using subprocess fallback', client_id)
|
logger.warning('[%s] libqemu-xtensa not available — using subprocess fallback', client_id)
|
||||||
esp_qemu_manager.start_instance(client_id, board, qemu_callback, firmware_b64)
|
esp_qemu_manager.start_instance(client_id, board, qemu_callback, firmware_b64)
|
||||||
|
|
@ -173,31 +174,28 @@ async def simulation_websocket(websocket: WebSocket, client_id: str):
|
||||||
client_id, bytes(raw_bytes), uart_id=2
|
client_id, bytes(raw_bytes), uart_id=2
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── ESP32 DHT22 sensor (backend-side protocol emulation) ─────
|
# ── ESP32 sensor protocol offloading (generic) ────────────────
|
||||||
elif msg_type == 'esp32_dht22_attach':
|
elif msg_type == 'esp32_sensor_attach':
|
||||||
|
sensor_type = msg_data.get('sensor_type', '')
|
||||||
pin = int(msg_data.get('pin', 0))
|
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():
|
if _use_lib():
|
||||||
esp_lib_manager.dht22_attach(client_id, pin, temperature, humidity)
|
esp_lib_manager.sensor_attach(client_id, sensor_type, pin, msg_data)
|
||||||
else:
|
else:
|
||||||
esp_qemu_manager.dht22_attach(client_id, pin, temperature, humidity)
|
esp_qemu_manager.sensor_attach(client_id, sensor_type, pin, msg_data)
|
||||||
|
|
||||||
elif msg_type == 'esp32_dht22_update':
|
elif msg_type == 'esp32_sensor_update':
|
||||||
pin = int(msg_data.get('pin', 0))
|
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():
|
if _use_lib():
|
||||||
esp_lib_manager.dht22_update(client_id, pin, temperature, humidity)
|
esp_lib_manager.sensor_update(client_id, pin, msg_data)
|
||||||
else:
|
else:
|
||||||
esp_qemu_manager.dht22_update(client_id, pin, temperature, humidity)
|
esp_qemu_manager.sensor_update(client_id, pin, msg_data)
|
||||||
|
|
||||||
elif msg_type == 'esp32_dht22_detach':
|
elif msg_type == 'esp32_sensor_detach':
|
||||||
pin = int(msg_data.get('pin', 0))
|
pin = int(msg_data.get('pin', 0))
|
||||||
if _use_lib():
|
if _use_lib():
|
||||||
esp_lib_manager.dht22_detach(client_id, pin)
|
esp_lib_manager.sensor_detach(client_id, pin)
|
||||||
else:
|
else:
|
||||||
esp_qemu_manager.dht22_detach(client_id, pin)
|
esp_qemu_manager.sensor_detach(client_id, pin)
|
||||||
|
|
||||||
# ── ESP32 status query ────────────────────────────────────────
|
# ── ESP32 status query ────────────────────────────────────────
|
||||||
elif msg_type == 'esp32_status':
|
elif msg_type == 'esp32_status':
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,7 @@ class EspLibManager:
|
||||||
board_type: str,
|
board_type: str,
|
||||||
callback: EventCallback,
|
callback: EventCallback,
|
||||||
firmware_b64: str | None = None,
|
firmware_b64: str | None = None,
|
||||||
|
sensors: list | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Stop any existing instance for this client_id first
|
# Stop any existing instance for this client_id first
|
||||||
if client_id in self._instances:
|
if client_id in self._instances:
|
||||||
|
|
@ -177,6 +178,7 @@ class EspLibManager:
|
||||||
'lib_path': lib_path,
|
'lib_path': lib_path,
|
||||||
'firmware_b64': firmware_b64,
|
'firmware_b64': firmware_b64,
|
||||||
'machine': machine,
|
'machine': machine,
|
||||||
|
'sensors': sensors or [],
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('Launching esp32_worker for %s (machine=%s, script=%s, python=%s)',
|
logger.info('Launching esp32_worker for %s (machine=%s, script=%s, python=%s)',
|
||||||
|
|
@ -343,30 +345,37 @@ class EspLibManager:
|
||||||
if inst and inst.running and inst.process.returncode is None:
|
if inst and inst.running and inst.process.returncode is None:
|
||||||
self._write_cmd(inst, {'cmd': 'set_spi_response', 'response': response_byte & 0xFF})
|
self._write_cmd(inst, {'cmd': 'set_spi_response', 'response': response_byte & 0xFF})
|
||||||
|
|
||||||
# ── DHT22 sensor (backend-side protocol emulation) ─────────────────────
|
# ── Generic sensor protocol offloading ──────────────────────────────────
|
||||||
|
|
||||||
def dht22_attach(self, client_id: str, pin: int, temperature: float, humidity: float) -> None:
|
def sensor_attach(self, client_id: str, sensor_type: str, pin: int,
|
||||||
"""Register a DHT22 sensor on a GPIO pin — the worker handles the protocol."""
|
properties: dict) -> None:
|
||||||
|
"""Register a sensor on a GPIO pin — the worker handles its protocol."""
|
||||||
with self._instances_lock:
|
with self._instances_lock:
|
||||||
inst = self._instances.get(client_id)
|
inst = self._instances.get(client_id)
|
||||||
if inst and inst.running and inst.process.returncode is None:
|
if inst and inst.running and inst.process.returncode is None:
|
||||||
self._write_cmd(inst, {'cmd': 'dht22_attach', 'pin': pin,
|
self._write_cmd(inst, {
|
||||||
'temperature': temperature, 'humidity': humidity})
|
'cmd': 'sensor_attach', 'sensor_type': sensor_type,
|
||||||
|
'pin': pin, **{k: v for k, v in properties.items()
|
||||||
|
if k not in ('sensor_type', 'pin')},
|
||||||
|
})
|
||||||
|
|
||||||
def dht22_update(self, client_id: str, pin: int, temperature: float, humidity: float) -> None:
|
def sensor_update(self, client_id: str, pin: int,
|
||||||
"""Update a DHT22 sensor's temperature and humidity values."""
|
properties: dict) -> None:
|
||||||
|
"""Update a sensor's properties (temperature, humidity, distance…)."""
|
||||||
with self._instances_lock:
|
with self._instances_lock:
|
||||||
inst = self._instances.get(client_id)
|
inst = self._instances.get(client_id)
|
||||||
if inst and inst.running and inst.process.returncode is None:
|
if inst and inst.running and inst.process.returncode is None:
|
||||||
self._write_cmd(inst, {'cmd': 'dht22_update', 'pin': pin,
|
self._write_cmd(inst, {
|
||||||
'temperature': temperature, 'humidity': humidity})
|
'cmd': 'sensor_update', 'pin': pin,
|
||||||
|
**{k: v for k, v in properties.items() if k != 'pin'},
|
||||||
|
})
|
||||||
|
|
||||||
def dht22_detach(self, client_id: str, pin: int) -> None:
|
def sensor_detach(self, client_id: str, pin: int) -> None:
|
||||||
"""Remove a DHT22 sensor from a GPIO pin."""
|
"""Remove a sensor from a GPIO pin."""
|
||||||
with self._instances_lock:
|
with self._instances_lock:
|
||||||
inst = self._instances.get(client_id)
|
inst = self._instances.get(client_id)
|
||||||
if inst and inst.running and inst.process.returncode is None:
|
if inst and inst.running and inst.process.returncode is None:
|
||||||
self._write_cmd(inst, {'cmd': 'dht22_detach', 'pin': pin})
|
self._write_cmd(inst, {'cmd': 'sensor_detach', 'pin': pin})
|
||||||
|
|
||||||
# ── LEDC polling (no-op: worker polls automatically) ─────────────────────
|
# ── LEDC polling (no-op: worker polls automatically) ─────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,9 +174,10 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
||||||
_log(f'Bad config JSON: {exc}')
|
_log(f'Bad config JSON: {exc}')
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
lib_path = cfg['lib_path']
|
lib_path = cfg['lib_path']
|
||||||
firmware_b64 = cfg['firmware_b64']
|
firmware_b64 = cfg['firmware_b64']
|
||||||
machine = cfg.get('machine', 'esp32-picsimlab')
|
machine = cfg.get('machine', 'esp32-picsimlab')
|
||||||
|
initial_sensors = cfg.get('sensors', [])
|
||||||
|
|
||||||
# Adjust GPIO pinmap based on chip: ESP32-C3 has only 22 GPIOs
|
# Adjust GPIO pinmap based on chip: ESP32-C3 has only 22 GPIOs
|
||||||
if 'c3' in machine:
|
if 'c3' in machine:
|
||||||
|
|
@ -217,6 +218,7 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
||||||
# ── 4. Shared mutable state ───────────────────────────────────────────────
|
# ── 4. Shared mutable state ───────────────────────────────────────────────
|
||||||
_stopped = threading.Event() # set on "stop" command
|
_stopped = threading.Event() # set on "stop" command
|
||||||
_init_done = threading.Event() # set when qemu_init() returns
|
_init_done = threading.Event() # set when qemu_init() returns
|
||||||
|
_sensors_ready = threading.Event() # set after pre-registering initial sensors
|
||||||
_i2c_responses: dict[int, int] = {} # 7-bit addr → response byte
|
_i2c_responses: dict[int, int] = {} # 7-bit addr → response byte
|
||||||
_spi_response = [0xFF] # MISO byte for SPI transfers
|
_spi_response = [0xFF] # MISO byte for SPI transfers
|
||||||
_rmt_decoders: dict[int, _RmtDecoder] = {}
|
_rmt_decoders: dict[int, _RmtDecoder] = {}
|
||||||
|
|
@ -229,9 +231,9 @@ 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
|
# ESP32 signal indices: 72-79 = LEDC HS ch 0-7, 80-87 = LEDC LS ch 0-7
|
||||||
_ledc_gpio_map: dict[int, int] = {}
|
_ledc_gpio_map: dict[int, int] = {}
|
||||||
|
|
||||||
# DHT22 sensor state: gpio_pin → {temperature, humidity, saw_low, responding}
|
# Sensor state: gpio_pin → {type, properties..., saw_low, responding}
|
||||||
_dht22_sensors: dict[int, dict] = {}
|
_sensors: dict[int, dict] = {}
|
||||||
_dht22_lock = threading.Lock()
|
_sensors_lock = threading.Lock()
|
||||||
|
|
||||||
def _busy_wait_us(us: int) -> None:
|
def _busy_wait_us(us: int) -> None:
|
||||||
"""Busy-wait for the given number of microseconds using perf_counter_ns."""
|
"""Busy-wait for the given number of microseconds using perf_counter_ns."""
|
||||||
|
|
@ -280,8 +282,8 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_log(f'DHT22 respond error on GPIO {gpio_pin}: {exc}')
|
_log(f'DHT22 respond error on GPIO {gpio_pin}: {exc}')
|
||||||
finally:
|
finally:
|
||||||
with _dht22_lock:
|
with _sensors_lock:
|
||||||
sensor = _dht22_sensors.get(gpio_pin)
|
sensor = _sensors.get(gpio_pin)
|
||||||
if sensor:
|
if sensor:
|
||||||
sensor['responding'] = False
|
sensor['responding'] = False
|
||||||
|
|
||||||
|
|
@ -293,10 +295,10 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
||||||
gpio = int(_PINMAP[slot]) if 1 <= slot <= _GPIO_COUNT else slot
|
gpio = int(_PINMAP[slot]) if 1 <= slot <= _GPIO_COUNT else slot
|
||||||
_emit({'type': 'gpio_change', 'pin': gpio, 'state': value})
|
_emit({'type': 'gpio_change', 'pin': gpio, 'state': value})
|
||||||
|
|
||||||
# DHT22: detect start signal (firmware drives pin LOW then HIGH)
|
# Sensor protocol dispatch by type
|
||||||
with _dht22_lock:
|
with _sensors_lock:
|
||||||
sensor = _dht22_sensors.get(gpio)
|
sensor = _sensors.get(gpio)
|
||||||
if sensor is not None:
|
if sensor is not None and sensor.get('type') == 'dht22':
|
||||||
if value == 0 and not sensor.get('responding', False):
|
if value == 0 and not sensor.get('responding', False):
|
||||||
sensor['saw_low'] = True
|
sensor['saw_low'] = True
|
||||||
elif value == 1 and sensor.get('saw_low', False):
|
elif value == 1 and sensor.get('saw_low', False):
|
||||||
|
|
@ -304,7 +306,8 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
||||||
sensor['responding'] = True
|
sensor['responding'] = True
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=_dht22_respond,
|
target=_dht22_respond,
|
||||||
args=(gpio, sensor['temperature'], sensor['humidity']),
|
args=(gpio, sensor.get('temperature', 25.0),
|
||||||
|
sensor.get('humidity', 50.0)),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f'dht22-gpio{gpio}',
|
name=f'dht22-gpio{gpio}',
|
||||||
).start()
|
).start()
|
||||||
|
|
@ -395,6 +398,10 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
||||||
_emit({'type': 'error', 'message': f'qemu_init failed: {exc}'})
|
_emit({'type': 'error', 'message': f'qemu_init failed: {exc}'})
|
||||||
finally:
|
finally:
|
||||||
_init_done.set()
|
_init_done.set()
|
||||||
|
# Wait for initial sensors to be pre-registered before executing firmware.
|
||||||
|
# This prevents race conditions where the firmware tries to read a sensor
|
||||||
|
# (e.g. DHT22 pulseIn) before the sensor handler is registered.
|
||||||
|
_sensors_ready.wait(timeout=5.0)
|
||||||
lib.qemu_main_loop()
|
lib.qemu_main_loop()
|
||||||
|
|
||||||
# With -nographic, qemu_init registers the stdio mux chardev which reads
|
# With -nographic, qemu_init registers the stdio mux chardev which reads
|
||||||
|
|
@ -415,6 +422,20 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
||||||
_emit({'type': 'error', 'message': 'qemu_init timed out after 30 s'})
|
_emit({'type': 'error', 'message': 'qemu_init timed out after 30 s'})
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
|
# Pre-register initial sensors before letting QEMU execute firmware.
|
||||||
|
for s in initial_sensors:
|
||||||
|
gpio = int(s.get('pin', 0))
|
||||||
|
sensor_type = s.get('sensor_type', '')
|
||||||
|
with _sensors_lock:
|
||||||
|
_sensors[gpio] = {
|
||||||
|
'type': sensor_type,
|
||||||
|
**{k: v for k, v in s.items() if k not in ('sensor_type', 'pin')},
|
||||||
|
'saw_low': False,
|
||||||
|
'responding': False,
|
||||||
|
}
|
||||||
|
_log(f'Pre-registered sensor {sensor_type} on GPIO {gpio}')
|
||||||
|
_sensors_ready.set()
|
||||||
|
|
||||||
_emit({'type': 'system', 'event': 'booted'})
|
_emit({'type': 'system', 'event': 'booted'})
|
||||||
_log(f'QEMU started: machine={machine} firmware={firmware_path}')
|
_log(f'QEMU started: machine={machine} firmware={firmware_path}')
|
||||||
|
|
||||||
|
|
@ -477,32 +498,33 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
||||||
elif c == 'set_spi_response':
|
elif c == 'set_spi_response':
|
||||||
_spi_response[0] = int(cmd['response']) & 0xFF
|
_spi_response[0] = int(cmd['response']) & 0xFF
|
||||||
|
|
||||||
elif c == 'dht22_attach':
|
elif c == 'sensor_attach':
|
||||||
gpio = int(cmd['pin'])
|
gpio = int(cmd['pin'])
|
||||||
with _dht22_lock:
|
sensor_type = cmd.get('sensor_type', '')
|
||||||
_dht22_sensors[gpio] = {
|
with _sensors_lock:
|
||||||
'temperature': float(cmd.get('temperature', 25.0)),
|
_sensors[gpio] = {
|
||||||
'humidity': float(cmd.get('humidity', 50.0)),
|
'type': sensor_type,
|
||||||
|
**{k: v for k, v in cmd.items()
|
||||||
|
if k not in ('cmd', 'pin', 'sensor_type')},
|
||||||
'saw_low': False,
|
'saw_low': False,
|
||||||
'responding': False,
|
'responding': False,
|
||||||
}
|
}
|
||||||
_log(f'DHT22 attached on GPIO {gpio}')
|
_log(f'Sensor {sensor_type} attached on GPIO {gpio}')
|
||||||
|
|
||||||
elif c == 'dht22_update':
|
elif c == 'sensor_update':
|
||||||
gpio = int(cmd['pin'])
|
gpio = int(cmd['pin'])
|
||||||
with _dht22_lock:
|
with _sensors_lock:
|
||||||
sensor = _dht22_sensors.get(gpio)
|
sensor = _sensors.get(gpio)
|
||||||
if sensor:
|
if sensor:
|
||||||
if 'temperature' in cmd:
|
for k, v in cmd.items():
|
||||||
sensor['temperature'] = float(cmd['temperature'])
|
if k not in ('cmd', 'pin'):
|
||||||
if 'humidity' in cmd:
|
sensor[k] = v
|
||||||
sensor['humidity'] = float(cmd['humidity'])
|
|
||||||
|
|
||||||
elif c == 'dht22_detach':
|
elif c == 'sensor_detach':
|
||||||
gpio = int(cmd['pin'])
|
gpio = int(cmd['pin'])
|
||||||
with _dht22_lock:
|
with _sensors_lock:
|
||||||
_dht22_sensors.pop(gpio, None)
|
_sensors.pop(gpio, None)
|
||||||
_log(f'DHT22 detached from GPIO {gpio}')
|
_log(f'Sensor detached from GPIO {gpio}')
|
||||||
|
|
||||||
elif c == 'stop':
|
elif c == 'stop':
|
||||||
_stopped.set()
|
_stopped.set()
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,8 @@ class EspInstance:
|
||||||
self._tasks: list[asyncio.Task] = []
|
self._tasks: list[asyncio.Task] = []
|
||||||
self.running: bool = False
|
self.running: bool = False
|
||||||
|
|
||||||
# DHT22 sensor state: gpio_pin → {temperature, humidity, saw_low, responding}
|
# Sensor state: gpio_pin → {type, properties..., saw_low, responding}
|
||||||
self.dht22_sensors: dict[int, dict] = {}
|
self.sensors: dict[int, dict] = {}
|
||||||
|
|
||||||
async def emit(self, event_type: str, data: dict) -> None:
|
async def emit(self, event_type: str, data: dict) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -125,32 +125,34 @@ class EspQemuManager:
|
||||||
if inst and inst._gpio_writer:
|
if inst and inst._gpio_writer:
|
||||||
asyncio.create_task(self._send_gpio(inst, int(pin), bool(state)))
|
asyncio.create_task(self._send_gpio(inst, int(pin), bool(state)))
|
||||||
|
|
||||||
# ── DHT22 sensor API ──────────────────────────────────────────────────────
|
# ── Generic sensor API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
def dht22_attach(self, client_id: str, pin: int,
|
def sensor_attach(self, client_id: str, sensor_type: str, pin: int,
|
||||||
temperature: float, humidity: float) -> None:
|
properties: dict) -> None:
|
||||||
inst = self._instances.get(client_id)
|
inst = self._instances.get(client_id)
|
||||||
if inst:
|
if inst:
|
||||||
inst.dht22_sensors[pin] = {
|
inst.sensors[pin] = {
|
||||||
'temperature': temperature,
|
'type': sensor_type,
|
||||||
'humidity': humidity,
|
**{k: v for k, v in properties.items()
|
||||||
|
if k not in ('sensor_type', 'pin')},
|
||||||
'saw_low': False,
|
'saw_low': False,
|
||||||
'responding': False,
|
'responding': False,
|
||||||
}
|
}
|
||||||
logger.info('[%s] DHT22 attached on GPIO %d (T=%.1f H=%.1f)',
|
logger.info('[%s] Sensor %s attached on GPIO %d',
|
||||||
client_id, pin, temperature, humidity)
|
client_id, sensor_type, pin)
|
||||||
|
|
||||||
def dht22_update(self, client_id: str, pin: int,
|
def sensor_update(self, client_id: str, pin: int,
|
||||||
temperature: float, humidity: float) -> None:
|
properties: dict) -> None:
|
||||||
inst = self._instances.get(client_id)
|
inst = self._instances.get(client_id)
|
||||||
if inst and pin in inst.dht22_sensors:
|
if inst and pin in inst.sensors:
|
||||||
inst.dht22_sensors[pin]['temperature'] = temperature
|
for k, v in properties.items():
|
||||||
inst.dht22_sensors[pin]['humidity'] = humidity
|
if k != 'pin':
|
||||||
|
inst.sensors[pin][k] = v
|
||||||
|
|
||||||
def dht22_detach(self, client_id: str, pin: int) -> None:
|
def sensor_detach(self, client_id: str, pin: int) -> None:
|
||||||
inst = self._instances.get(client_id)
|
inst = self._instances.get(client_id)
|
||||||
if inst:
|
if inst:
|
||||||
inst.dht22_sensors.pop(pin, None)
|
inst.sensors.pop(pin, None)
|
||||||
|
|
||||||
async def send_serial_bytes(self, client_id: str, data: bytes) -> None:
|
async def send_serial_bytes(self, client_id: str, data: bytes) -> None:
|
||||||
inst = self._instances.get(client_id)
|
inst = self._instances.get(client_id)
|
||||||
|
|
@ -295,9 +297,9 @@ class EspQemuManager:
|
||||||
state = int(parts[2])
|
state = int(parts[2])
|
||||||
await inst.emit('gpio_change', {'pin': pin, 'state': state})
|
await inst.emit('gpio_change', {'pin': pin, 'state': state})
|
||||||
|
|
||||||
# DHT22: detect start signal (firmware drives pin LOW then HIGH)
|
# Sensor protocol: dispatch by sensor type
|
||||||
sensor = inst.dht22_sensors.get(pin)
|
sensor = inst.sensors.get(pin)
|
||||||
if sensor is not None:
|
if sensor is not None and sensor.get('type') == 'dht22':
|
||||||
if state == 0 and not sensor.get('responding', False):
|
if state == 0 and not sensor.get('responding', False):
|
||||||
sensor['saw_low'] = True
|
sensor['saw_low'] = True
|
||||||
elif state == 1 and sensor.get('saw_low', False):
|
elif state == 1 and sensor.get('saw_low', False):
|
||||||
|
|
@ -305,8 +307,8 @@ class EspQemuManager:
|
||||||
sensor['responding'] = True
|
sensor['responding'] = True
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self._dht22_respond(inst, pin,
|
self._dht22_respond(inst, pin,
|
||||||
sensor['temperature'],
|
sensor.get('temperature', 25.0),
|
||||||
sensor['humidity'])
|
sensor.get('humidity', 50.0))
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
@ -374,7 +376,7 @@ class EspQemuManager:
|
||||||
logger.warning('[%s] DHT22 respond error on GPIO %d: %s',
|
logger.warning('[%s] DHT22 respond error on GPIO %d: %s',
|
||||||
inst.client_id, gpio_pin, exc)
|
inst.client_id, gpio_pin, exc)
|
||||||
finally:
|
finally:
|
||||||
sensor = inst.dht22_sensors.get(gpio_pin)
|
sensor = inst.sensors.get(gpio_pin)
|
||||||
if sensor:
|
if sensor:
|
||||||
sensor['responding'] = False
|
sensor['responding'] = False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,430 @@
|
||||||
|
/**
|
||||||
|
* esp32-dht22-flow.test.ts
|
||||||
|
*
|
||||||
|
* Tests for the ESP32 sensor registration flow, focusing on the DHT22 example.
|
||||||
|
* Verifies:
|
||||||
|
* 1. Esp32BridgeShim delegates registerSensor → bridge.sendSensorAttach
|
||||||
|
* 2. DHT22 attachEvents detects ESP32 shim and calls registerSensor
|
||||||
|
* 3. DHT22 attachEvents falls back to local protocol on AVR (registerSensor → false)
|
||||||
|
* 4. Sensor update flow: updateSensor → bridge.sendSensorUpdate
|
||||||
|
* 5. Cleanup: unregisterSensor → bridge.sendSensorDetach
|
||||||
|
* 6. Esp32Bridge includes pre-registered sensors in start_esp32 payload
|
||||||
|
* 7. Race condition fix: sensors are pre-registered before firmware executes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
|
||||||
|
// ── Mocks ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
vi.mock('../simulation/AVRSimulator', () => ({
|
||||||
|
AVRSimulator: vi.fn(function (this: any) {
|
||||||
|
this.onSerialData = null;
|
||||||
|
this.onBaudRateChange = null;
|
||||||
|
this.onPinChangeWithTime = null;
|
||||||
|
this.start = vi.fn();
|
||||||
|
this.stop = vi.fn();
|
||||||
|
this.reset = vi.fn();
|
||||||
|
this.loadHex = vi.fn();
|
||||||
|
this.addI2CDevice = vi.fn();
|
||||||
|
this.setPinState = vi.fn();
|
||||||
|
this.isRunning = vi.fn().mockReturnValue(false);
|
||||||
|
this.registerSensor = vi.fn().mockReturnValue(false);
|
||||||
|
this.updateSensor = vi.fn();
|
||||||
|
this.unregisterSensor = vi.fn();
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../simulation/RP2040Simulator', () => ({
|
||||||
|
RP2040Simulator: vi.fn(function (this: any) {
|
||||||
|
this.onSerialData = null;
|
||||||
|
this.onPinChangeWithTime = null;
|
||||||
|
this.start = vi.fn();
|
||||||
|
this.stop = vi.fn();
|
||||||
|
this.reset = vi.fn();
|
||||||
|
this.loadBinary = vi.fn();
|
||||||
|
this.addI2CDevice = vi.fn();
|
||||||
|
this.isRunning = vi.fn().mockReturnValue(false);
|
||||||
|
this.registerSensor = vi.fn().mockReturnValue(false);
|
||||||
|
this.updateSensor = vi.fn();
|
||||||
|
this.unregisterSensor = vi.fn();
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../simulation/PinManager', () => ({
|
||||||
|
PinManager: vi.fn(function (this: any) {
|
||||||
|
this.updatePort = vi.fn();
|
||||||
|
this.onPinChange = vi.fn().mockReturnValue(() => {});
|
||||||
|
this.onPwmChange = vi.fn().mockReturnValue(() => {});
|
||||||
|
this.getListenersCount = vi.fn().mockReturnValue(0);
|
||||||
|
this.updatePwm = vi.fn();
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../simulation/I2CBusManager', () => ({
|
||||||
|
VirtualDS1307: vi.fn(function (this: any) {}),
|
||||||
|
VirtualTempSensor: vi.fn(function (this: any) {}),
|
||||||
|
I2CMemoryDevice: vi.fn(function (this: any) {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../store/useOscilloscopeStore', () => ({
|
||||||
|
useOscilloscopeStore: {
|
||||||
|
getState: vi.fn().mockReturnValue({ channels: [], pushSample: vi.fn() }),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// WebSocket mock
|
||||||
|
class MockWebSocket {
|
||||||
|
static OPEN = 1;
|
||||||
|
static CLOSING = 2;
|
||||||
|
static CLOSED = 3;
|
||||||
|
|
||||||
|
readyState = MockWebSocket.OPEN;
|
||||||
|
onopen: (() => void) | null = null;
|
||||||
|
onmessage: ((e: { data: string }) => void) | null = null;
|
||||||
|
onclose: ((e?: any) => void) | null = null;
|
||||||
|
onerror: ((e?: any) => void) | null = null;
|
||||||
|
sent: string[] = [];
|
||||||
|
|
||||||
|
send(data: string) { this.sent.push(data); }
|
||||||
|
close() { this.readyState = MockWebSocket.CLOSED; this.onclose?.({ code: 1000 }); }
|
||||||
|
open() { this.readyState = MockWebSocket.OPEN; this.onopen?.(); }
|
||||||
|
receive(payload: object) {
|
||||||
|
this.onmessage?.({ data: JSON.stringify(payload) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse all sent JSON messages */
|
||||||
|
get messages(): Array<{ type: string; data: Record<string, unknown> }> {
|
||||||
|
return this.sent.map((s) => JSON.parse(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.stubGlobal('WebSocket', MockWebSocket);
|
||||||
|
vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => 1);
|
||||||
|
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
||||||
|
vi.stubGlobal('sessionStorage', {
|
||||||
|
getItem: vi.fn().mockReturnValue('test-session-id'),
|
||||||
|
setItem: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Imports (after mocks) ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { Esp32Bridge } from '../simulation/Esp32Bridge';
|
||||||
|
import { PartSimulationRegistry } from '../simulation/parts/PartSimulationRegistry';
|
||||||
|
import '../simulation/parts/ProtocolParts';
|
||||||
|
|
||||||
|
// ─── Mock factories ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function makeElement(props: Record<string, unknown> = {}): HTMLElement {
|
||||||
|
return {
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
...props,
|
||||||
|
} as unknown as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simulator mock that mimics Esp32BridgeShim (registerSensor returns true) */
|
||||||
|
function makeEsp32Shim() {
|
||||||
|
const bridge = {
|
||||||
|
sendSensorAttach: vi.fn(),
|
||||||
|
sendSensorUpdate: vi.fn(),
|
||||||
|
sendSensorDetach: vi.fn(),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
bridge,
|
||||||
|
pinManager: {
|
||||||
|
onPinChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
onPwmChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
},
|
||||||
|
setPinState: vi.fn(),
|
||||||
|
isRunning: vi.fn().mockReturnValue(true),
|
||||||
|
registerSensor(type: string, pin: number, properties: Record<string, unknown>): boolean {
|
||||||
|
bridge.sendSensorAttach(type, pin, properties);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
updateSensor(pin: number, properties: Record<string, unknown>): void {
|
||||||
|
bridge.sendSensorUpdate(pin, properties);
|
||||||
|
},
|
||||||
|
unregisterSensor(pin: number): void {
|
||||||
|
bridge.sendSensorDetach(pin);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simulator mock that mimics AVR (registerSensor returns false) */
|
||||||
|
function makeAVRSim() {
|
||||||
|
return {
|
||||||
|
pinManager: {
|
||||||
|
onPinChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
onPwmChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
},
|
||||||
|
setPinState: vi.fn(),
|
||||||
|
isRunning: vi.fn().mockReturnValue(true),
|
||||||
|
registerSensor: vi.fn().mockReturnValue(false),
|
||||||
|
updateSensor: vi.fn(),
|
||||||
|
unregisterSensor: vi.fn(),
|
||||||
|
schedulePinChange: vi.fn(),
|
||||||
|
getCurrentCycles: vi.fn().mockReturnValue(1000),
|
||||||
|
cpu: { data: new Uint8Array(512).fill(0), cycles: 1000 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinMap =
|
||||||
|
(map: Record<string, number>) =>
|
||||||
|
(name: string): number | null =>
|
||||||
|
name in map ? map[name] : null;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 1. Esp32BridgeShim — registerSensor delegates to bridge
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Esp32BridgeShim — sensor registration', () => {
|
||||||
|
it('registerSensor calls bridge.sendSensorAttach and returns true', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const result = shim.registerSensor('dht22', 4, { temperature: 28, humidity: 65 });
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(shim.bridge.sendSensorAttach).toHaveBeenCalledWith(
|
||||||
|
'dht22', 4, { temperature: 28, humidity: 65 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updateSensor calls bridge.sendSensorUpdate', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
shim.updateSensor(4, { temperature: 30, humidity: 70 });
|
||||||
|
expect(shim.bridge.sendSensorUpdate).toHaveBeenCalledWith(
|
||||||
|
4, { temperature: 30, humidity: 70 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unregisterSensor calls bridge.sendSensorDetach', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
shim.unregisterSensor(4);
|
||||||
|
expect(shim.bridge.sendSensorDetach).toHaveBeenCalledWith(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 2. DHT22 attachEvents — ESP32 shim detection
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('dht22 — ESP32 shim detection', () => {
|
||||||
|
const logic = () => PartSimulationRegistry.get('dht22')!;
|
||||||
|
|
||||||
|
it('detects ESP32 shim and delegates sensor to backend', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement({ temperature: 28, humidity: 65 });
|
||||||
|
logic().attachEvents!(el, shim as any, pinMap({ SDA: 4 }), 'dht22-1');
|
||||||
|
|
||||||
|
// Should have called registerSensor → sendSensorAttach
|
||||||
|
expect(shim.bridge.sendSensorAttach).toHaveBeenCalledWith(
|
||||||
|
'dht22', 4, { temperature: 28, humidity: 65 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should NOT register onPinChange (local protocol) — ESP32 backend handles it
|
||||||
|
expect(shim.pinManager.onPinChange).not.toHaveBeenCalled();
|
||||||
|
// Should NOT call setPinState (no local pin driving)
|
||||||
|
expect(shim.setPinState).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleanup calls unregisterSensor', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement({ temperature: 28, humidity: 65 });
|
||||||
|
const cleanup = logic().attachEvents!(el, shim as any, pinMap({ SDA: 4 }), 'dht22-1');
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
expect(shim.bridge.sendSensorDetach).toHaveBeenCalledWith(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 3. DHT22 attachEvents — AVR fallback (local protocol)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('dht22 — AVR local protocol fallback', () => {
|
||||||
|
const logic = () => PartSimulationRegistry.get('dht22')!;
|
||||||
|
|
||||||
|
it('falls back to local protocol when registerSensor returns false', () => {
|
||||||
|
const sim = makeAVRSim();
|
||||||
|
const el = makeElement({ temperature: 25, humidity: 50 });
|
||||||
|
logic().attachEvents!(el, sim as any, pinMap({ SDA: 7 }), 'dht22-avr');
|
||||||
|
|
||||||
|
// AVR registerSensor returns false → local protocol path
|
||||||
|
expect(sim.registerSensor).toHaveBeenCalledWith('dht22', 7, { temperature: 25, humidity: 50 });
|
||||||
|
|
||||||
|
// Local protocol: onPinChange registered for start-signal detection
|
||||||
|
expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(7, expect.any(Function));
|
||||||
|
// Local protocol: DATA pin set HIGH (idle)
|
||||||
|
expect(sim.setPinState).toHaveBeenCalledWith(7, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('local protocol does NOT call registerSensor on bridge methods', () => {
|
||||||
|
const sim = makeAVRSim();
|
||||||
|
const el = makeElement({ temperature: 25, humidity: 50 });
|
||||||
|
logic().attachEvents!(el, sim as any, pinMap({ SDA: 7 }), 'dht22-avr2');
|
||||||
|
|
||||||
|
// AVR path: uses onPinChange, not bridge sensor methods
|
||||||
|
expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(7, expect.any(Function));
|
||||||
|
// updateSensor/unregisterSensor should NOT have been called
|
||||||
|
expect(sim.updateSensor).not.toHaveBeenCalled();
|
||||||
|
expect(sim.unregisterSensor).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 4. Sensor update flow
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('dht22 — sensor update via SensorUpdateRegistry', () => {
|
||||||
|
const logic = () => PartSimulationRegistry.get('dht22')!;
|
||||||
|
|
||||||
|
it('registerSensorUpdate callback forwards to updateSensor', async () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement({ temperature: 28, humidity: 65 }) as any;
|
||||||
|
logic().attachEvents!(el, shim as any, pinMap({ SDA: 4 }), 'dht22-update');
|
||||||
|
|
||||||
|
// Import the registry and dispatch an update
|
||||||
|
const { dispatchSensorUpdate } = await import('../simulation/SensorUpdateRegistry');
|
||||||
|
dispatchSensorUpdate('dht22-update', { temperature: 35 });
|
||||||
|
|
||||||
|
// Should have forwarded the update to the bridge
|
||||||
|
expect(shim.bridge.sendSensorUpdate).toHaveBeenCalledWith(
|
||||||
|
4,
|
||||||
|
expect.objectContaining({ temperature: 35 })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 5. Esp32Bridge — WebSocket sensor messages
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Esp32Bridge — sensor WebSocket protocol', () => {
|
||||||
|
let bridge: Esp32Bridge;
|
||||||
|
let ws: MockWebSocket;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bridge = new Esp32Bridge('test-esp32', 'esp32');
|
||||||
|
bridge.connect();
|
||||||
|
// Get the WebSocket instance created in connect()
|
||||||
|
ws = (bridge as any).socket as MockWebSocket;
|
||||||
|
ws.open(); // Trigger onopen → sends start_esp32
|
||||||
|
ws.sent = []; // Clear the start_esp32 message
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
bridge.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sendSensorAttach sends esp32_sensor_attach message', () => {
|
||||||
|
bridge.sendSensorAttach('dht22', 4, { temperature: 28, humidity: 65 });
|
||||||
|
expect(ws.messages).toEqual([
|
||||||
|
{
|
||||||
|
type: 'esp32_sensor_attach',
|
||||||
|
data: { sensor_type: 'dht22', pin: 4, temperature: 28, humidity: 65 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sendSensorUpdate sends esp32_sensor_update message', () => {
|
||||||
|
bridge.sendSensorUpdate(4, { temperature: 35, humidity: 70 });
|
||||||
|
expect(ws.messages).toEqual([
|
||||||
|
{
|
||||||
|
type: 'esp32_sensor_update',
|
||||||
|
data: { pin: 4, temperature: 35, humidity: 70 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sendSensorDetach sends esp32_sensor_detach message', () => {
|
||||||
|
bridge.sendSensorDetach(4);
|
||||||
|
expect(ws.messages).toEqual([
|
||||||
|
{
|
||||||
|
type: 'esp32_sensor_detach',
|
||||||
|
data: { pin: 4 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 6. Esp32Bridge — sensor pre-registration in start_esp32 payload
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Esp32Bridge — sensor pre-registration', () => {
|
||||||
|
it('includes sensors in start_esp32 payload when setSensors is called before connect', () => {
|
||||||
|
const bridge = new Esp32Bridge('test-esp32', 'esp32');
|
||||||
|
bridge.setSensors([
|
||||||
|
{ sensor_type: 'dht22', pin: 4, temperature: 28, humidity: 65 },
|
||||||
|
]);
|
||||||
|
bridge.loadFirmware('AAAA'); // base64 firmware
|
||||||
|
|
||||||
|
bridge.connect();
|
||||||
|
const ws = (bridge as any).socket as MockWebSocket;
|
||||||
|
ws.open(); // Trigger onopen → sends start_esp32
|
||||||
|
|
||||||
|
const startMsg = ws.messages.find((m) => m.type === 'start_esp32');
|
||||||
|
expect(startMsg).toBeDefined();
|
||||||
|
expect(startMsg!.data.sensors).toEqual([
|
||||||
|
{ sensor_type: 'dht22', pin: 4, temperature: 28, humidity: 65 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('start_esp32 payload has empty sensors array when none pre-registered', () => {
|
||||||
|
const bridge = new Esp32Bridge('test-esp32', 'esp32');
|
||||||
|
bridge.loadFirmware('AAAA');
|
||||||
|
bridge.connect();
|
||||||
|
const ws = (bridge as any).socket as MockWebSocket;
|
||||||
|
ws.open();
|
||||||
|
|
||||||
|
const startMsg = ws.messages.find((m) => m.type === 'start_esp32');
|
||||||
|
expect(startMsg).toBeDefined();
|
||||||
|
expect(startMsg!.data.sensors).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 7. Race condition: sensors pre-registered before firmware starts
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Race condition fix — sensors arrive before firmware', () => {
|
||||||
|
it('sensor_attach message is part of start_esp32, not a separate later message', () => {
|
||||||
|
const bridge = new Esp32Bridge('test-esp32', 'esp32');
|
||||||
|
bridge.setSensors([
|
||||||
|
{ sensor_type: 'dht22', pin: 4, temperature: 28, humidity: 65 },
|
||||||
|
]);
|
||||||
|
bridge.loadFirmware('AAAA');
|
||||||
|
|
||||||
|
bridge.connect();
|
||||||
|
const ws = (bridge as any).socket as MockWebSocket;
|
||||||
|
ws.open();
|
||||||
|
|
||||||
|
// The very first message should be start_esp32 with sensors included
|
||||||
|
const firstMsg = ws.messages[0];
|
||||||
|
expect(firstMsg.type).toBe('start_esp32');
|
||||||
|
expect(firstMsg.data.sensors).toHaveLength(1);
|
||||||
|
expect(firstMsg.data.sensors[0].sensor_type).toBe('dht22');
|
||||||
|
expect(firstMsg.data.sensors[0].pin).toBe(4);
|
||||||
|
|
||||||
|
// No separate sensor_attach message should be needed at this point
|
||||||
|
const sensorAttachMsgs = ws.messages.filter((m) => m.type === 'esp32_sensor_attach');
|
||||||
|
expect(sensorAttachMsgs).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sensors can still be attached dynamically after start', () => {
|
||||||
|
const bridge = new Esp32Bridge('test-esp32', 'esp32');
|
||||||
|
bridge.loadFirmware('AAAA');
|
||||||
|
bridge.connect();
|
||||||
|
const ws = (bridge as any).socket as MockWebSocket;
|
||||||
|
ws.open();
|
||||||
|
ws.sent = []; // Clear start_esp32
|
||||||
|
|
||||||
|
// Dynamic attachment (e.g. user adds a sensor during simulation)
|
||||||
|
bridge.sendSensorAttach('hc-sr04', 12, { distance: 50 });
|
||||||
|
expect(ws.messages).toEqual([
|
||||||
|
{
|
||||||
|
type: 'esp32_sensor_attach',
|
||||||
|
data: { sensor_type: 'hc-sr04', pin: 12, distance: 50 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,375 @@
|
||||||
|
/**
|
||||||
|
* esp32-servo-pot.test.ts
|
||||||
|
*
|
||||||
|
* Tests for the ESP32 Servo + Potentiometer example, focusing on:
|
||||||
|
* 1. Servo subscribes to onPwmChange for ESP32 (not AVR cycle measurement)
|
||||||
|
* 2. Servo uses onPinChange for AVR (existing behavior)
|
||||||
|
* 3. Servo uses onPinChangeWithTime for RP2040
|
||||||
|
* 4. LEDC update routes to correct GPIO pin (not LEDC channel)
|
||||||
|
* 5. LEDC duty_pct is normalized to 0.0–1.0
|
||||||
|
* 6. LEDC fallback to channel when gpio=-1
|
||||||
|
* 7. Servo angle maps correctly from duty cycle
|
||||||
|
* 8. Potentiometer setAdcVoltage returns false for ESP32 (SimulatorCanvas handles it)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
|
||||||
|
// ── Mocks ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
vi.mock('../simulation/AVRSimulator', () => ({
|
||||||
|
AVRSimulator: vi.fn(function (this: any) {
|
||||||
|
this.onSerialData = null;
|
||||||
|
this.onBaudRateChange = null;
|
||||||
|
this.onPinChangeWithTime = null;
|
||||||
|
this.start = vi.fn();
|
||||||
|
this.stop = vi.fn();
|
||||||
|
this.reset = vi.fn();
|
||||||
|
this.loadHex = vi.fn();
|
||||||
|
this.addI2CDevice = vi.fn();
|
||||||
|
this.setPinState = vi.fn();
|
||||||
|
this.isRunning = vi.fn().mockReturnValue(true);
|
||||||
|
this.registerSensor = vi.fn().mockReturnValue(false);
|
||||||
|
this.pinManager = {
|
||||||
|
onPinChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
onPwmChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
updatePwm: vi.fn(),
|
||||||
|
};
|
||||||
|
this.getCurrentCycles = vi.fn().mockReturnValue(1000);
|
||||||
|
this.getClockHz = vi.fn().mockReturnValue(16_000_000);
|
||||||
|
this.cpu = { data: new Uint8Array(512).fill(0), cycles: 1000 };
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../simulation/RP2040Simulator', () => ({
|
||||||
|
RP2040Simulator: vi.fn(function (this: any) {
|
||||||
|
this.onSerialData = null;
|
||||||
|
this.onPinChangeWithTime = null;
|
||||||
|
this.start = vi.fn();
|
||||||
|
this.stop = vi.fn();
|
||||||
|
this.reset = vi.fn();
|
||||||
|
this.loadBinary = vi.fn();
|
||||||
|
this.addI2CDevice = vi.fn();
|
||||||
|
this.isRunning = vi.fn().mockReturnValue(true);
|
||||||
|
this.registerSensor = vi.fn().mockReturnValue(false);
|
||||||
|
this.pinManager = {
|
||||||
|
onPinChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
onPwmChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
updatePwm: vi.fn(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../simulation/PinManager', () => ({
|
||||||
|
PinManager: vi.fn(function (this: any) {
|
||||||
|
this.updatePort = vi.fn();
|
||||||
|
this.onPinChange = vi.fn().mockReturnValue(() => {});
|
||||||
|
this.onPwmChange = vi.fn().mockReturnValue(() => {});
|
||||||
|
this.getListenersCount = vi.fn().mockReturnValue(0);
|
||||||
|
this.updatePwm = vi.fn();
|
||||||
|
this.triggerPinChange = vi.fn();
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../simulation/I2CBusManager', () => ({
|
||||||
|
VirtualDS1307: vi.fn(function (this: any) {}),
|
||||||
|
VirtualTempSensor: vi.fn(function (this: any) {}),
|
||||||
|
I2CMemoryDevice: vi.fn(function (this: any) {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../store/useOscilloscopeStore', () => ({
|
||||||
|
useOscilloscopeStore: {
|
||||||
|
getState: vi.fn().mockReturnValue({ channels: [], pushSample: vi.fn() }),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => 1);
|
||||||
|
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
||||||
|
vi.stubGlobal('sessionStorage', {
|
||||||
|
getItem: vi.fn().mockReturnValue('test-session-id'),
|
||||||
|
setItem: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Imports (after mocks) ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { PartSimulationRegistry } from '../simulation/parts/PartSimulationRegistry';
|
||||||
|
import '../simulation/parts/ComplexParts';
|
||||||
|
import { PinManager } from '../simulation/PinManager';
|
||||||
|
import { RP2040Simulator } from '../simulation/RP2040Simulator';
|
||||||
|
import { setAdcVoltage } from '../simulation/parts/partUtils';
|
||||||
|
|
||||||
|
// ─── Mock factories ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function makeElement(props: Record<string, unknown> = {}): HTMLElement {
|
||||||
|
return {
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
angle: 0,
|
||||||
|
...props,
|
||||||
|
} as unknown as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simulator mock that mimics Esp32BridgeShim (no valid CPU cycles) */
|
||||||
|
function makeEsp32Shim() {
|
||||||
|
let pwmCallback: ((pin: number, duty: number) => void) | null = null;
|
||||||
|
const unsubPwm = vi.fn();
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinManager: {
|
||||||
|
onPinChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
onPwmChange: vi.fn().mockImplementation((_pin: number, cb: (pin: number, duty: number) => void) => {
|
||||||
|
pwmCallback = cb;
|
||||||
|
return unsubPwm;
|
||||||
|
}),
|
||||||
|
updatePwm: vi.fn(),
|
||||||
|
triggerPinChange: vi.fn(),
|
||||||
|
},
|
||||||
|
setPinState: vi.fn(),
|
||||||
|
isRunning: vi.fn().mockReturnValue(true),
|
||||||
|
getCurrentCycles: vi.fn().mockReturnValue(-1), // ESP32: no valid cycles
|
||||||
|
getClockHz: vi.fn().mockReturnValue(240_000_000),
|
||||||
|
registerSensor: vi.fn().mockReturnValue(true),
|
||||||
|
updateSensor: vi.fn(),
|
||||||
|
unregisterSensor: vi.fn(),
|
||||||
|
// Test helpers
|
||||||
|
_getPwmCallback: () => pwmCallback,
|
||||||
|
_unsubPwm: unsubPwm,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simulator mock that mimics AVR (has valid CPU cycles) */
|
||||||
|
function makeAVRSim() {
|
||||||
|
let pinCallback: ((pin: number, state: boolean) => void) | null = null;
|
||||||
|
const unsubPin = vi.fn();
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinManager: {
|
||||||
|
onPinChange: vi.fn().mockImplementation((_pin: number, cb: (pin: number, state: boolean) => void) => {
|
||||||
|
pinCallback = cb;
|
||||||
|
return unsubPin;
|
||||||
|
}),
|
||||||
|
onPwmChange: vi.fn().mockReturnValue(() => {}),
|
||||||
|
updatePwm: vi.fn(),
|
||||||
|
},
|
||||||
|
setPinState: vi.fn(),
|
||||||
|
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 },
|
||||||
|
registerSensor: vi.fn().mockReturnValue(false),
|
||||||
|
// Test helpers
|
||||||
|
_getPinCallback: () => pinCallback,
|
||||||
|
_unsubPin: unsubPin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinMap =
|
||||||
|
(map: Record<string, number>) =>
|
||||||
|
(name: string): number | null =>
|
||||||
|
name in map ? map[name] : null;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 1. Servo — ESP32 path: subscribes to onPwmChange
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Servo — ESP32 PWM subscription', () => {
|
||||||
|
const logic = () => PartSimulationRegistry.get('servo')!;
|
||||||
|
|
||||||
|
it('subscribes to onPwmChange when simulator has no valid CPU cycles (ESP32 shim)', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement();
|
||||||
|
logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-esp32');
|
||||||
|
|
||||||
|
expect(shim.pinManager.onPwmChange).toHaveBeenCalledWith(13, expect.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates angle when PWM duty cycle changes', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement() as any;
|
||||||
|
logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-esp32-angle');
|
||||||
|
|
||||||
|
const cb = shim._getPwmCallback();
|
||||||
|
expect(cb).not.toBeNull();
|
||||||
|
|
||||||
|
// duty 0.0 → 0°
|
||||||
|
cb!(13, 0.0);
|
||||||
|
expect(el.angle).toBe(0);
|
||||||
|
|
||||||
|
// duty 0.5 → 90°
|
||||||
|
cb!(13, 0.5);
|
||||||
|
expect(el.angle).toBe(90);
|
||||||
|
|
||||||
|
// duty 1.0 → 180°
|
||||||
|
cb!(13, 1.0);
|
||||||
|
expect(el.angle).toBe(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps angle to 0-180 range', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement() as any;
|
||||||
|
logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-clamp');
|
||||||
|
|
||||||
|
const cb = shim._getPwmCallback();
|
||||||
|
|
||||||
|
// Negative duty → 0°
|
||||||
|
cb!(13, -0.1);
|
||||||
|
expect(el.angle).toBe(0);
|
||||||
|
|
||||||
|
// Duty > 1 → 180°
|
||||||
|
cb!(13, 1.5);
|
||||||
|
expect(el.angle).toBe(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleanup unsubscribes from onPwmChange', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement();
|
||||||
|
const cleanup = logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-cleanup');
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
expect(shim._unsubPwm).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT subscribe to onPinChange (AVR cycle measurement)', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement();
|
||||||
|
logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-no-pin');
|
||||||
|
|
||||||
|
expect(shim.pinManager.onPinChange).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 2. Servo — AVR path: uses onPinChange + cycle measurement
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Servo — AVR cycle-based measurement', () => {
|
||||||
|
const logic = () => PartSimulationRegistry.get('servo')!;
|
||||||
|
|
||||||
|
it('subscribes to onPinChange (not onPwmChange) when simulator has valid CPU cycles', () => {
|
||||||
|
const avr = makeAVRSim();
|
||||||
|
const el = makeElement();
|
||||||
|
logic().attachEvents!(el, avr as any, pinMap({ PWM: 9 }), 'servo-avr');
|
||||||
|
|
||||||
|
expect(avr.pinManager.onPinChange).toHaveBeenCalledWith(9, expect.any(Function));
|
||||||
|
// Should NOT use onPwmChange for AVR
|
||||||
|
expect(avr.pinManager.onPwmChange).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 3. Servo — RP2040 path: uses onPinChangeWithTime (instanceof check)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Servo — RP2040 timing-based measurement', () => {
|
||||||
|
const logic = () => PartSimulationRegistry.get('servo')!;
|
||||||
|
|
||||||
|
it('uses onPinChangeWithTime when simulator is RP2040Simulator instance', () => {
|
||||||
|
const rp = new RP2040Simulator() as any;
|
||||||
|
const el = makeElement();
|
||||||
|
logic().attachEvents!(el, rp as any, pinMap({ PWM: 15 }), 'servo-rp2040');
|
||||||
|
|
||||||
|
// RP2040 path sets onPinChangeWithTime
|
||||||
|
expect(rp.onPinChangeWithTime).toBeTypeOf('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 4-6. LEDC update routing — PinManager.updatePwm
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('LEDC update routing', () => {
|
||||||
|
let pm: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pm = new PinManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('routes to GPIO pin when update.gpio >= 0', () => {
|
||||||
|
const update = { channel: 0, duty: 4096, duty_pct: 50, gpio: 13 };
|
||||||
|
const targetPin = (update.gpio !== undefined && update.gpio >= 0)
|
||||||
|
? update.gpio
|
||||||
|
: update.channel;
|
||||||
|
pm.updatePwm(targetPin, update.duty_pct / 100);
|
||||||
|
|
||||||
|
expect(pm.updatePwm).toHaveBeenCalledWith(13, 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to channel when gpio is -1', () => {
|
||||||
|
const update = { channel: 2, duty: 4096, duty_pct: 50, gpio: -1 };
|
||||||
|
const targetPin = (update.gpio !== undefined && update.gpio >= 0)
|
||||||
|
? update.gpio
|
||||||
|
: update.channel;
|
||||||
|
pm.updatePwm(targetPin, update.duty_pct / 100);
|
||||||
|
|
||||||
|
expect(pm.updatePwm).toHaveBeenCalledWith(2, 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to channel when gpio is undefined', () => {
|
||||||
|
const update = { channel: 3, duty: 8192, duty_pct: 100 } as any;
|
||||||
|
const targetPin = (update.gpio !== undefined && update.gpio >= 0)
|
||||||
|
? update.gpio
|
||||||
|
: update.channel;
|
||||||
|
pm.updatePwm(targetPin, update.duty_pct / 100);
|
||||||
|
|
||||||
|
expect(pm.updatePwm).toHaveBeenCalledWith(3, 1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes duty_pct to 0.0–1.0 (divides by 100)', () => {
|
||||||
|
const update = { channel: 0, duty: 2048, duty_pct: 25, gpio: 5 };
|
||||||
|
const targetPin = (update.gpio !== undefined && update.gpio >= 0)
|
||||||
|
? update.gpio
|
||||||
|
: update.channel;
|
||||||
|
pm.updatePwm(targetPin, update.duty_pct / 100);
|
||||||
|
|
||||||
|
expect(pm.updatePwm).toHaveBeenCalledWith(5, 0.25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 7. Servo angle mapping from duty cycle
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Servo angle mapping', () => {
|
||||||
|
const logic = () => PartSimulationRegistry.get('servo')!;
|
||||||
|
|
||||||
|
it('maps duty 0.0 → angle 0, duty 0.5 → angle 90, duty 1.0 → angle 180', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
const el = makeElement() as any;
|
||||||
|
logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-map');
|
||||||
|
|
||||||
|
const cb = shim._getPwmCallback();
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ duty: 0.0, expectedAngle: 0 },
|
||||||
|
{ duty: 0.25, expectedAngle: 45 },
|
||||||
|
{ duty: 0.5, expectedAngle: 90 },
|
||||||
|
{ duty: 0.75, expectedAngle: 135 },
|
||||||
|
{ duty: 1.0, expectedAngle: 180 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { duty, expectedAngle } of testCases) {
|
||||||
|
cb!(13, duty);
|
||||||
|
expect(el.angle).toBe(expectedAngle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 8. Potentiometer — setAdcVoltage on ESP32
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Potentiometer — ESP32 ADC path', () => {
|
||||||
|
it('setAdcVoltage returns false for ESP32 shim (GPIO 34 is not AVR/RP2040 ADC range)', () => {
|
||||||
|
const shim = makeEsp32Shim();
|
||||||
|
// GPIO 34 on ESP32 — not in AVR range (14-19) nor RP2040 range (26-29)
|
||||||
|
const result = setAdcVoltage(shim as any, 34, 1.65);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setAdcVoltage works for AVR (pin 14-19)', () => {
|
||||||
|
const avrSim = makeAVRSim() as any;
|
||||||
|
avrSim.getADC = () => ({ channelValues: new Array(6).fill(0) });
|
||||||
|
const result = setAdcVoltage(avrSim as any, 14, 2.5);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -3709,23 +3709,36 @@ void loop() {
|
||||||
// Wiring: DATA → GPIO4 | VCC → 3V3 | GND → GND
|
// Wiring: DATA → GPIO4 | VCC → 3V3 | GND → GND
|
||||||
|
|
||||||
#include <DHT.h>
|
#include <DHT.h>
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
|
#include <soc/timer_group_struct.h>
|
||||||
|
#include <soc/timer_group_reg.h>
|
||||||
|
|
||||||
#define DHT_PIN 4 // GPIO 4
|
#define DHT_PIN 4 // GPIO 4
|
||||||
#define DHT_TYPE DHT22
|
#define DHT_TYPE DHT22
|
||||||
|
|
||||||
DHT dht(DHT_PIN, DHT_TYPE);
|
DHT dht(DHT_PIN, DHT_TYPE);
|
||||||
|
|
||||||
|
// Disable hardware Timer Group WDTs (QEMU emulation is slower than real time)
|
||||||
|
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() {
|
void setup() {
|
||||||
|
disableAllWDT();
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
// Disable watchdog — DHT pulseIn() can block in emulation
|
|
||||||
disableCore0WDT();
|
|
||||||
disableCore1WDT();
|
|
||||||
dht.begin();
|
dht.begin();
|
||||||
delay(2000);
|
delay(2000);
|
||||||
Serial.println("ESP32 DHT22 ready!");
|
Serial.println("ESP32 DHT22 ready!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
disableAllWDT();
|
||||||
delay(2000);
|
delay(2000);
|
||||||
|
|
||||||
float h = dht.readHumidity();
|
float h = dht.readHumidity();
|
||||||
|
|
@ -3907,19 +3920,35 @@ void loop() {
|
||||||
// Pot : SIG → D34 | VCC → 3V3 | GND → GND
|
// Pot : SIG → D34 | VCC → 3V3 | GND → GND
|
||||||
|
|
||||||
#include <ESP32Servo.h>
|
#include <ESP32Servo.h>
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
|
#include <soc/timer_group_struct.h>
|
||||||
|
#include <soc/timer_group_reg.h>
|
||||||
|
|
||||||
#define SERVO_PIN 13
|
#define SERVO_PIN 13
|
||||||
#define POT_PIN 34 // input-only GPIO (ADC)
|
#define POT_PIN 34 // input-only GPIO (ADC)
|
||||||
|
|
||||||
Servo myServo;
|
Servo myServo;
|
||||||
|
|
||||||
|
// Disable hardware Timer Group WDTs (QEMU emulation is slower than real time)
|
||||||
|
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() {
|
void setup() {
|
||||||
|
disableAllWDT();
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
myServo.attach(SERVO_PIN, 500, 2400); // standard servo pulse range
|
myServo.attach(SERVO_PIN, 500, 2400); // standard servo pulse range
|
||||||
Serial.println("ESP32 Servo + Pot control");
|
Serial.println("ESP32 Servo + Pot control");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
disableAllWDT(); // keep WDTs disabled (FreeRTOS may re-enable)
|
||||||
int raw = analogRead(POT_PIN); // 0–4095 (12-bit ADC)
|
int raw = analogRead(POT_PIN); // 0–4095 (12-bit ADC)
|
||||||
int angle = map(raw, 0, 4095, 0, 180);
|
int angle = map(raw, 0, 4095, 0, 180);
|
||||||
myServo.write(angle);
|
myServo.write(angle);
|
||||||
|
|
@ -3999,22 +4028,36 @@ void loop() {
|
||||||
// Wiring: DATA → GPIO3 | VCC → 3V3 | GND → GND
|
// Wiring: DATA → GPIO3 | VCC → 3V3 | GND → GND
|
||||||
|
|
||||||
#include <DHT.h>
|
#include <DHT.h>
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
|
#include <soc/timer_group_struct.h>
|
||||||
|
#include <soc/timer_group_reg.h>
|
||||||
|
|
||||||
#define DHT_PIN 3 // GPIO 3
|
#define DHT_PIN 3 // GPIO 3
|
||||||
#define DHT_TYPE DHT22
|
#define DHT_TYPE DHT22
|
||||||
|
|
||||||
DHT dht(DHT_PIN, DHT_TYPE);
|
DHT dht(DHT_PIN, DHT_TYPE);
|
||||||
|
|
||||||
|
// Disable hardware Timer Group WDTs (QEMU emulation is slower than real time)
|
||||||
|
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() {
|
void setup() {
|
||||||
|
disableAllWDT();
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
// Disable watchdog — DHT pulseIn() can block in emulation
|
|
||||||
disableCore0WDT();
|
|
||||||
dht.begin();
|
dht.begin();
|
||||||
delay(2000);
|
delay(2000);
|
||||||
Serial.println("ESP32-C3 DHT22 ready!");
|
Serial.println("ESP32-C3 DHT22 ready!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
disableAllWDT();
|
||||||
delay(2000);
|
delay(2000);
|
||||||
|
|
||||||
float h = dht.readHumidity();
|
float h = dht.readHumidity();
|
||||||
|
|
|
||||||
|
|
@ -651,4 +651,12 @@ export class AVRSimulator {
|
||||||
this.i2cBus.addDevice(device);
|
this.i2cBus.addDevice(device);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Generic sensor registration (board-agnostic API) ──────────────────────
|
||||||
|
// AVR handles all sensor protocols locally via schedulePinChange,
|
||||||
|
// so these return false / no-op — the sensor runs its own frontend logic.
|
||||||
|
|
||||||
|
registerSensor(_type: string, _pin: number, _props: Record<string, unknown>): boolean { return false; }
|
||||||
|
updateSensor(_pin: number, _props: Record<string, unknown>): void {}
|
||||||
|
unregisterSensor(_pin: number): void {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@
|
||||||
* { type: 'esp32_adc_set', data: { channel: number, millivolts: number } }
|
* { type: 'esp32_adc_set', data: { channel: number, millivolts: number } }
|
||||||
* { type: 'esp32_i2c_response', data: { addr: number, response: number } }
|
* { type: 'esp32_i2c_response', data: { addr: number, response: number } }
|
||||||
* { type: 'esp32_spi_response', data: { response: number } }
|
* { type: 'esp32_spi_response', data: { response: number } }
|
||||||
|
* { type: 'esp32_sensor_attach', data: { sensor_type: string, pin: number, ... } }
|
||||||
|
* { type: 'esp32_sensor_update', data: { pin: number, ... } }
|
||||||
|
* { type: 'esp32_sensor_detach', data: { pin: number } }
|
||||||
*
|
*
|
||||||
* Backend → Frontend
|
* Backend → Frontend
|
||||||
* { type: 'serial_output', data: { data: string, uart?: number } }
|
* { type: 'serial_output', data: { data: string, uart?: number } }
|
||||||
|
|
@ -77,6 +80,7 @@ export class Esp32Bridge {
|
||||||
private socket: WebSocket | null = null;
|
private socket: WebSocket | null = null;
|
||||||
private _connected = false;
|
private _connected = false;
|
||||||
private _pendingFirmware: string | null = null;
|
private _pendingFirmware: string | null = null;
|
||||||
|
private _pendingSensors: Array<Record<string, unknown>> = [];
|
||||||
|
|
||||||
constructor(boardId: string, boardKind: BoardKind) {
|
constructor(boardId: string, boardKind: BoardKind) {
|
||||||
this.boardId = boardId;
|
this.boardId = boardId;
|
||||||
|
|
@ -108,6 +112,7 @@ export class Esp32Bridge {
|
||||||
data: {
|
data: {
|
||||||
board: toQemuBoardType(this.boardKind),
|
board: toQemuBoardType(this.boardKind),
|
||||||
...(this._pendingFirmware ? { firmware_b64: this._pendingFirmware } : {}),
|
...(this._pendingFirmware ? { firmware_b64: this._pendingFirmware } : {}),
|
||||||
|
sensors: this._pendingSensors,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -202,6 +207,16 @@ export class Esp32Bridge {
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-register sensors so they are included in the start_esp32 payload.
|
||||||
|
* This ensures sensors are ready in the QEMU worker BEFORE the firmware
|
||||||
|
* begins executing, preventing race conditions where pulseIn() times out
|
||||||
|
* because the sensor handler hasn't been registered yet.
|
||||||
|
*/
|
||||||
|
setSensors(sensors: Array<Record<string, unknown>>): void {
|
||||||
|
this._pendingSensors = sensors;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a compiled firmware (base64-encoded .bin) into the running ESP32.
|
* Load a compiled firmware (base64-encoded .bin) into the running ESP32.
|
||||||
* If not yet connected, the firmware will be sent on next connect().
|
* If not yet connected, the firmware will be sent on next connect().
|
||||||
|
|
@ -244,19 +259,25 @@ export class Esp32Bridge {
|
||||||
this._send({ type: 'esp32_spi_response', data: { response } });
|
this._send({ type: 'esp32_spi_response', data: { response } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attach a DHT22 sensor to a GPIO pin — backend will handle the protocol */
|
// ── Generic sensor protocol offloading ────────────────────────────────────
|
||||||
dht22Attach(pin: number, temperature: number, humidity: number): void {
|
// Sensors call these to delegate their protocol to the backend QEMU.
|
||||||
this._send({ type: 'esp32_dht22_attach', data: { pin, temperature, humidity } });
|
// The sensor type (e.g. 'dht22', 'hc-sr04') tells the backend which
|
||||||
|
// protocol handler to use. Sensor-specific properties (temperature,
|
||||||
|
// humidity, distance …) are passed as a generic Record.
|
||||||
|
|
||||||
|
/** Register a sensor on a GPIO pin — backend handles its protocol */
|
||||||
|
sendSensorAttach(sensorType: string, pin: number, properties: Record<string, unknown>): void {
|
||||||
|
this._send({ type: 'esp32_sensor_attach', data: { sensor_type: sensorType, pin, ...properties } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update DHT22 sensor readings */
|
/** Update sensor properties (temperature, humidity, distance, etc.) */
|
||||||
dht22Update(pin: number, temperature: number, humidity: number): void {
|
sendSensorUpdate(pin: number, properties: Record<string, unknown>): void {
|
||||||
this._send({ type: 'esp32_dht22_update', data: { pin, temperature, humidity } });
|
this._send({ type: 'esp32_sensor_update', data: { pin, ...properties } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Detach the DHT22 sensor from a GPIO pin */
|
/** Detach a sensor from a GPIO pin */
|
||||||
dht22Detach(pin: number): void {
|
sendSensorDetach(pin: number): void {
|
||||||
this._send({ type: 'esp32_dht22_detach', data: { pin } });
|
this._send({ type: 'esp32_sensor_detach', data: { pin } });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _send(payload: unknown): void {
|
private _send(payload: unknown): void {
|
||||||
|
|
|
||||||
|
|
@ -490,4 +490,12 @@ export class RP2040Simulator {
|
||||||
spi.completeTransmit(response);
|
spi.completeTransmit(response);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Generic sensor registration (board-agnostic API) ──────────────────────
|
||||||
|
// RP2040 handles all sensor protocols locally via schedulePinChange,
|
||||||
|
// so these return false / no-op — the sensor runs its own frontend logic.
|
||||||
|
|
||||||
|
registerSensor(_type: string, _pin: number, _props: Record<string, unknown>): boolean { return false; }
|
||||||
|
updateSensor(_pin: number, _props: Record<string, unknown>): void {}
|
||||||
|
unregisterSensor(_pin: number): void {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,26 @@ PartSimulationRegistry.register('servo', {
|
||||||
return () => { avrSimulator.onPinChangeWithTime = null; };
|
return () => { avrSimulator.onPinChangeWithTime = null; };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── ESP32 path: subscribe to LEDC PWM duty updates via PinManager ──
|
||||||
|
// Esp32BridgeShim has pinManager but getCurrentCycles() returns -1
|
||||||
|
// (no local CPU cycle counter — QEMU runs on the backend).
|
||||||
|
if (pinSIG !== null && !(avrSimulator instanceof RP2040Simulator)) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const pinManager = (avrSimulator as any).pinManager as import('../PinManager').PinManager | undefined;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const hasCpuCycles = typeof (avrSimulator as any).getCurrentCycles === 'function'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
&& (avrSimulator as any).getCurrentCycles() >= 0;
|
||||||
|
|
||||||
|
if (pinManager && !hasCpuCycles) {
|
||||||
|
const unsubscribe = pinManager.onPwmChange(pinSIG, (_pin, dutyCycle) => {
|
||||||
|
const angle = Math.round(dutyCycle * 180);
|
||||||
|
el.angle = Math.max(0, Math.min(180, angle));
|
||||||
|
});
|
||||||
|
return () => { unsubscribe(); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── AVR primary: cycle-accurate pulse width measurement ────────────
|
// ── AVR primary: cycle-accurate pulse width measurement ────────────
|
||||||
if (pinSIG !== null) {
|
if (pinSIG !== null) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
|
||||||
|
|
@ -458,24 +458,27 @@ PartSimulationRegistry.register('dht22', {
|
||||||
const pin = getPin('SDA') ?? getPin('DATA');
|
const pin = getPin('SDA') ?? getPin('DATA');
|
||||||
if (pin === null) return () => {};
|
if (pin === null) return () => {};
|
||||||
|
|
||||||
// ESP32: delegate DHT22 protocol to the backend QEMU worker.
|
// Ask the simulator if it handles sensor protocols natively (e.g. ESP32
|
||||||
// The frontend can't provide µs-level GPIO timing over WebSocket.
|
// delegates to backend QEMU). If so, we only forward property updates.
|
||||||
const sim = simulator as any;
|
const el = element as any;
|
||||||
if (sim.isEsp32) {
|
const temperature = el.temperature ?? 25.0;
|
||||||
const bridge = sim.getBridge();
|
const humidity = el.humidity ?? 50.0;
|
||||||
const el = element as any;
|
|
||||||
const temperature = el.temperature ?? 25.0;
|
|
||||||
const humidity = el.humidity ?? 50.0;
|
|
||||||
bridge.dht22Attach(pin, temperature, humidity);
|
|
||||||
|
|
||||||
|
const handledNatively = typeof (simulator as any).registerSensor === 'function'
|
||||||
|
&& (simulator as any).registerSensor('dht22', pin, { temperature, humidity });
|
||||||
|
|
||||||
|
if (handledNatively) {
|
||||||
registerSensorUpdate(componentId, (values) => {
|
registerSensorUpdate(componentId, (values) => {
|
||||||
if ('temperature' in values) el.temperature = values.temperature as number;
|
if ('temperature' in values) el.temperature = values.temperature as number;
|
||||||
if ('humidity' in values) el.humidity = values.humidity as number;
|
if ('humidity' in values) el.humidity = values.humidity as number;
|
||||||
bridge.dht22Update(pin, el.temperature ?? 25.0, el.humidity ?? 50.0);
|
(simulator as any).updateSensor(pin, {
|
||||||
|
temperature: el.temperature ?? 25.0,
|
||||||
|
humidity: el.humidity ?? 50.0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
bridge.dht22Detach(pin);
|
(simulator as any).unregisterSensor(pin);
|
||||||
unregisterSensorUpdate(componentId);
|
unregisterSensorUpdate(componentId);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,19 @@ import { RaspberryPi3Bridge } from '../simulation/RaspberryPi3Bridge';
|
||||||
import { Esp32Bridge } from '../simulation/Esp32Bridge';
|
import { Esp32Bridge } from '../simulation/Esp32Bridge';
|
||||||
import { useEditorStore } from './useEditorStore';
|
import { useEditorStore } from './useEditorStore';
|
||||||
import { useVfsStore } from './useVfsStore';
|
import { useVfsStore } from './useVfsStore';
|
||||||
|
import { boardPinToNumber, isBoardComponent } from '../utils/boardPinMapping';
|
||||||
|
|
||||||
|
// ── Sensor pre-registration ──────────────────────────────────────────────────
|
||||||
|
// Maps component metadataId → { sensorType, dataPinName, propertyKeys }
|
||||||
|
// Used to pre-register sensors in the start_esp32 payload so the QEMU worker
|
||||||
|
// has them ready before the firmware starts executing (prevents race conditions).
|
||||||
|
const SENSOR_COMPONENT_MAP: Record<string, {
|
||||||
|
sensorType: string;
|
||||||
|
dataPinName: string;
|
||||||
|
propertyKeys: string[];
|
||||||
|
}> = {
|
||||||
|
'dht22': { sensorType: 'dht22', dataPinName: 'SDA', propertyKeys: ['temperature', 'humidity'] },
|
||||||
|
};
|
||||||
|
|
||||||
// ── Legacy type aliases (keep external consumers working) ──────────────────
|
// ── Legacy type aliases (keep external consumers working) ──────────────────
|
||||||
export type BoardType = 'arduino-uno' | 'arduino-nano' | 'arduino-mega' | 'raspberry-pi-pico';
|
export type BoardType = 'arduino-uno' | 'arduino-nano' | 'arduino-mega' | 'raspberry-pi-pico';
|
||||||
|
|
@ -50,8 +63,6 @@ class Esp32BridgeShim {
|
||||||
}
|
}
|
||||||
|
|
||||||
setPinState(pin: number, state: boolean): void { this.bridge.sendPinEvent(pin, state); }
|
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; }
|
getCurrentCycles(): number { return -1; }
|
||||||
getClockHz(): number { return 240_000_000; }
|
getClockHz(): number { return 240_000_000; }
|
||||||
isRunning(): boolean { return this.bridge.connected; }
|
isRunning(): boolean { return this.bridge.connected; }
|
||||||
|
|
@ -68,6 +79,20 @@ class Esp32BridgeShim {
|
||||||
getSpeed(): number { return 1; }
|
getSpeed(): number { return 1; }
|
||||||
loadHex(_hex: string): void { /* no-op */ }
|
loadHex(_hex: string): void { /* no-op */ }
|
||||||
loadBinary(_b64: string): void { /* no-op */ }
|
loadBinary(_b64: string): void { /* no-op */ }
|
||||||
|
|
||||||
|
// ── Generic sensor registration (board-agnostic API) ──────────────────────
|
||||||
|
// ESP32 delegates sensor protocols to the backend QEMU.
|
||||||
|
|
||||||
|
registerSensor(type: string, pin: number, properties: Record<string, unknown>): boolean {
|
||||||
|
this.bridge.sendSensorAttach(type, pin, properties);
|
||||||
|
return true; // backend handles the protocol
|
||||||
|
}
|
||||||
|
updateSensor(pin: number, properties: Record<string, unknown>): void {
|
||||||
|
this.bridge.sendSensorUpdate(pin, properties);
|
||||||
|
}
|
||||||
|
unregisterSensor(pin: number): void {
|
||||||
|
this.bridge.sendSensorDetach(pin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Runtime Maps (outside Zustand — not serialisable) ─────────────────────
|
// ── Runtime Maps (outside Zustand — not serialisable) ─────────────────────
|
||||||
|
|
@ -536,7 +561,42 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
if (board.boardKind === 'raspberry-pi-3') {
|
if (board.boardKind === 'raspberry-pi-3') {
|
||||||
getBoardBridge(boardId)?.connect();
|
getBoardBridge(boardId)?.connect();
|
||||||
} else if (isEsp32Kind(board.boardKind)) {
|
} else if (isEsp32Kind(board.boardKind)) {
|
||||||
getEsp32Bridge(boardId)?.connect();
|
// Pre-register sensors connected to this board so the QEMU worker
|
||||||
|
// has them ready before the firmware starts executing.
|
||||||
|
const esp32Bridge = getEsp32Bridge(boardId);
|
||||||
|
if (esp32Bridge) {
|
||||||
|
const { components, wires } = get();
|
||||||
|
const sensors: Array<Record<string, unknown>> = [];
|
||||||
|
for (const comp of components) {
|
||||||
|
const sensorDef = SENSOR_COMPONENT_MAP[comp.metadataId];
|
||||||
|
if (!sensorDef) continue;
|
||||||
|
// Find the wire connecting this component's data pin to the board
|
||||||
|
for (const w of wires) {
|
||||||
|
const compEndpoint = (w.start.componentId === comp.id && w.start.pinName === sensorDef.dataPinName)
|
||||||
|
? w.start : (w.end.componentId === comp.id && w.end.pinName === sensorDef.dataPinName)
|
||||||
|
? w.end : null;
|
||||||
|
if (!compEndpoint) continue;
|
||||||
|
const boardEndpoint = compEndpoint === w.start ? w.end : w.start;
|
||||||
|
if (!isBoardComponent(boardEndpoint.componentId)) continue;
|
||||||
|
// Resolve GPIO pin number
|
||||||
|
const gpioPin = boardPinToNumber(board.boardKind, boardEndpoint.pinName);
|
||||||
|
if (gpioPin === null || gpioPin < 0) continue;
|
||||||
|
// Collect sensor properties from the component
|
||||||
|
const props: Record<string, unknown> = {
|
||||||
|
sensor_type: sensorDef.sensorType,
|
||||||
|
pin: gpioPin,
|
||||||
|
};
|
||||||
|
for (const key of sensorDef.propertyKeys) {
|
||||||
|
const val = comp.properties[key];
|
||||||
|
if (val !== undefined) props[key] = typeof val === 'string' ? parseFloat(val) : val;
|
||||||
|
}
|
||||||
|
sensors.push(props);
|
||||||
|
break; // only one data pin per sensor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
esp32Bridge.setSensors(sensors);
|
||||||
|
esp32Bridge.connect();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
getBoardSimulator(boardId)?.start();
|
getBoardSimulator(boardId)?.start();
|
||||||
}
|
}
|
||||||
|
|
@ -675,7 +735,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
bridge.onLedcUpdate = (update) => {
|
bridge.onLedcUpdate = (update) => {
|
||||||
const boardPm = pinManagerMap.get(boardId);
|
const boardPm = pinManagerMap.get(boardId);
|
||||||
if (boardPm && typeof boardPm.updatePwm === 'function') {
|
if (boardPm && typeof boardPm.updatePwm === 'function') {
|
||||||
boardPm.updatePwm(update.channel, update.duty_pct);
|
const targetPin = (update.gpio !== undefined && update.gpio >= 0)
|
||||||
|
? update.gpio
|
||||||
|
: update.channel;
|
||||||
|
boardPm.updatePwm(targetPin, update.duty_pct / 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
bridge.onWs2812Update = (channel, pixels) => {
|
bridge.onWs2812Update = (channel, pixels) => {
|
||||||
|
|
@ -768,7 +831,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
bridge.onLedcUpdate = (update) => {
|
bridge.onLedcUpdate = (update) => {
|
||||||
const boardPm = pinManagerMap.get(boardId);
|
const boardPm = pinManagerMap.get(boardId);
|
||||||
if (boardPm && typeof boardPm.updatePwm === 'function') {
|
if (boardPm && typeof boardPm.updatePwm === 'function') {
|
||||||
boardPm.updatePwm(update.channel, update.duty_pct);
|
const targetPin = (update.gpio !== undefined && update.gpio >= 0)
|
||||||
|
? update.gpio
|
||||||
|
: update.channel;
|
||||||
|
boardPm.updatePwm(targetPin, update.duty_pct / 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
esp32BridgeMap.set(boardId, bridge);
|
esp32BridgeMap.set(boardId, bridge);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue