161 lines
6.1 KiB
Python
161 lines
6.1 KiB
Python
"""
|
|
EspLibManager — ESP32 emulation via lcgamboa libqemu-xtensa.dll.
|
|
|
|
Exposes the same public API as EspQemuManager so simulation.py can
|
|
transparently switch between the two backends:
|
|
- DLL available → full GPIO + ADC + UART + WiFi (this module)
|
|
- DLL missing → serial-only via subprocess (esp_qemu_manager.py)
|
|
|
|
Activation: set environment variable QEMU_ESP32_LIB to the DLL path.
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from typing import Callable, Awaitable
|
|
|
|
from .esp32_lib_bridge import Esp32LibBridge, _DEFAULT_LIB
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Path to libqemu-xtensa.dll — env var takes priority, then auto-detect beside this module
|
|
LIB_PATH: str = os.environ.get('QEMU_ESP32_LIB', '') or (
|
|
_DEFAULT_LIB if os.path.isfile(_DEFAULT_LIB) else ''
|
|
)
|
|
|
|
EventCallback = Callable[[str, dict], Awaitable[None]]
|
|
|
|
# lcgamboa machine names (esp32-picsimlab has the GPIO callback bridge)
|
|
_MACHINE: dict[str, str] = {
|
|
'esp32': 'esp32-picsimlab',
|
|
'esp32-s3': 'esp32s3-picsimlab',
|
|
'esp32-c3': 'esp32c3-picsimlab',
|
|
}
|
|
|
|
|
|
class _InstanceState:
|
|
"""Tracks one running ESP32 instance."""
|
|
def __init__(self, bridge: Esp32LibBridge, callback: EventCallback, board_type: str):
|
|
self.bridge = bridge
|
|
self.callback = callback
|
|
self.board_type = board_type
|
|
|
|
|
|
class EspLibManager:
|
|
"""
|
|
Manager for ESP32 emulation via libqemu-xtensa.dll.
|
|
|
|
Uses Esp32LibBridge to load the lcgamboa QEMU shared library.
|
|
GPIO, ADC, UART, and WiFi events are delivered via the callback
|
|
function registered at start_instance().
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._instances: dict[str, _InstanceState] = {}
|
|
|
|
# ── Availability check ───────────────────────────────────────────────────
|
|
|
|
@staticmethod
|
|
def is_available() -> bool:
|
|
"""Return True if the DLL path is configured and the file exists."""
|
|
return bool(LIB_PATH) and os.path.isfile(LIB_PATH)
|
|
|
|
# ── Public API (mirrors EspQemuManager) ─────────────────────────────────
|
|
|
|
def start_instance(
|
|
self,
|
|
client_id: str,
|
|
board_type: str,
|
|
callback: EventCallback,
|
|
firmware_b64: str | None = None,
|
|
) -> None:
|
|
if client_id in self._instances:
|
|
logger.warning('start_instance: %s already running', client_id)
|
|
return
|
|
|
|
loop = asyncio.get_event_loop()
|
|
bridge = Esp32LibBridge(LIB_PATH, loop)
|
|
|
|
# ── GPIO listener → emit gpio_change events ──────────────────────────
|
|
async def _on_gpio(pin: int, state: int) -> None:
|
|
await callback('gpio_change', {'pin': pin, 'state': state})
|
|
|
|
# ── UART listener → accumulate bytes, emit serial_output ─────────────
|
|
uart_buf: bytearray = bytearray()
|
|
|
|
async def _on_uart(uart_id: int, byte_val: int) -> None:
|
|
if uart_id == 0:
|
|
uart_buf.append(byte_val)
|
|
# Flush on newline or if buffer gets large
|
|
if byte_val == ord('\n') or len(uart_buf) >= 256:
|
|
text = uart_buf.decode('utf-8', errors='replace')
|
|
uart_buf.clear()
|
|
await callback('serial_output', {'data': text})
|
|
|
|
bridge.register_gpio_listener(
|
|
lambda p, s: loop.call_soon_threadsafe(
|
|
lambda: asyncio.ensure_future(_on_gpio(p, s), loop=loop)
|
|
)
|
|
)
|
|
bridge.register_uart_listener(
|
|
lambda i, b: loop.call_soon_threadsafe(
|
|
lambda: asyncio.ensure_future(_on_uart(i, b), loop=loop)
|
|
)
|
|
)
|
|
|
|
machine = _MACHINE.get(board_type, 'esp32-picsimlab')
|
|
state = _InstanceState(bridge, callback, board_type)
|
|
self._instances[client_id] = state
|
|
|
|
asyncio.ensure_future(callback('system', {'event': 'booting'}))
|
|
|
|
if firmware_b64:
|
|
try:
|
|
bridge.start(firmware_b64, machine)
|
|
asyncio.ensure_future(callback('system', {'event': 'booted'}))
|
|
except Exception as e:
|
|
logger.error('start_instance %s: bridge.start failed: %s', client_id, e)
|
|
self._instances.pop(client_id, None)
|
|
asyncio.ensure_future(callback('error', {'message': str(e)}))
|
|
else:
|
|
# No firmware yet — instance registered, waiting for load_firmware()
|
|
logger.info('start_instance %s: no firmware, waiting for load_firmware()', client_id)
|
|
|
|
def stop_instance(self, client_id: str) -> None:
|
|
state = self._instances.pop(client_id, None)
|
|
if state:
|
|
try:
|
|
state.bridge.stop()
|
|
except Exception as e:
|
|
logger.warning('stop_instance %s: %s', client_id, e)
|
|
|
|
def load_firmware(self, client_id: str, firmware_b64: str) -> None:
|
|
"""Hot-reload firmware: stop current bridge, start fresh with new firmware."""
|
|
state = self._instances.get(client_id)
|
|
if not state:
|
|
logger.warning('load_firmware: no instance %s', client_id)
|
|
return
|
|
board_type = state.board_type
|
|
callback = state.callback
|
|
self.stop_instance(client_id)
|
|
|
|
async def _restart() -> None:
|
|
await asyncio.sleep(0.3)
|
|
self.start_instance(client_id, board_type, callback, firmware_b64)
|
|
|
|
asyncio.create_task(_restart())
|
|
|
|
def set_pin_state(self, client_id: str, pin: int | str, state: int) -> None:
|
|
"""Drive a GPIO pin from an external board (e.g. Arduino output → ESP32 input)."""
|
|
inst = self._instances.get(client_id)
|
|
if inst:
|
|
inst.bridge.set_pin(int(pin), state)
|
|
|
|
async def send_serial_bytes(self, client_id: str, data: bytes) -> None:
|
|
"""Send bytes to the ESP32 UART0 RX (user serial input)."""
|
|
inst = self._instances.get(client_id)
|
|
if inst:
|
|
inst.bridge.uart_send(0, data)
|
|
|
|
|
|
esp_lib_manager = EspLibManager()
|