From 437f5f83bcc5df5893e6da96f7d88a34a64247f9 Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Tue, 19 May 2026 08:24:32 +0200 Subject: [PATCH] feat(esp32-worker): register picsimlab_gpio_matrix_cb callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the new synchronous GPIO Matrix callback exposed by libqemu-{xtensa,riscv32} 1.1.0 (lcgamboa/qemu commit e178ff5). Whenever the firmware writes GPIO_FUNCx_OUT_SEL_CFG_REG, the C plugin now fires picsimlab_gpio_matrix_cb(gpio, signal_id) inline. The handler: - Treats signal_id == 0x100 or 0 as "matrix routing cleared" and emits gpio_routing_clear. - For LEDC HS/LS range signals (the only ones the frontend SignalRouter currently consumes), updates the mirror and emits gpio_routing. - Drops other signals — the mirror does not need to track them yet, and emitting them would only fatten WS frames. Backwards compat: - Older libqemu (<1.1.0) doesn't expose the new field; the picsimlab_gpio_matrix_cb placeholder runs (no-op) and the 100 ms _refresh_signal_routing() poll thread continues to feed the mirror. WS event shape is identical either way. Burn-in: keeping the poll thread active in parallel with the callback for now. Once telemetry confirms parity (per phase 4 doc in velxio-prod/project/esp32-gpio-matrix-cb/), the poll thread gets retired in a follow-up commit. --- backend/app/services/esp32_worker.py | 81 +++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/backend/app/services/esp32_worker.py b/backend/app/services/esp32_worker.py index 6a7341c2..3b8368a2 100644 --- a/backend/app/services/esp32_worker.py +++ b/backend/app/services/esp32_worker.py @@ -134,17 +134,24 @@ _I2C_EVENT = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_uint8, ctypes.c_uint8, c _SPI_EVENT = ctypes.CFUNCTYPE(ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint16) _UART_TX = ctypes.CFUNCTYPE(None, ctypes.c_uint8, ctypes.c_uint8) _RMT_EVENT = ctypes.CFUNCTYPE(None, ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32) +# Synchronous GPIO Matrix routing callback. Fires on every guest write +# to GPIO_FUNCx_OUT_SEL_CFG_REG with the full 9-bit signal_id; replaces +# the 100 ms poll path in _refresh_signal_routing() once the prod +# burn-in window confirms parity. Requires libqemu-{xtensa,riscv32} +# 1.1.0+; older binaries omit the field and the placeholder runs. +_GPIO_MATRIX_CB = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int) class _CallbacksT(ctypes.Structure): _fields_ = [ - ('picsimlab_write_pin', _WRITE_PIN), - ('picsimlab_dir_pin', _DIR_PIN), - ('picsimlab_i2c_event', _I2C_EVENT), - ('picsimlab_spi_event', _SPI_EVENT), - ('picsimlab_uart_tx_event', _UART_TX), - ('pinmap', ctypes.c_void_p), - ('picsimlab_rmt_event', _RMT_EVENT), + ('picsimlab_write_pin', _WRITE_PIN), + ('picsimlab_dir_pin', _DIR_PIN), + ('picsimlab_i2c_event', _I2C_EVENT), + ('picsimlab_spi_event', _SPI_EVENT), + ('picsimlab_uart_tx_event', _UART_TX), + ('pinmap', ctypes.c_void_p), + ('picsimlab_rmt_event', _RMT_EVENT), + ('picsimlab_gpio_matrix_cb', _GPIO_MATRIX_CB), ] @@ -868,6 +875,40 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) if pixels: _emit({'type': 'ws2812_update', 'channel': channel, 'pixels': pixels}) + def _on_gpio_matrix(gpio: int, signal_id: int) -> None: + """Synchronous GPIO Matrix routing event from libqemu 1.1.0+. + Replaces the 100 ms poll path in _refresh_signal_routing(). + + signal_id 0x100 = SIG_GPIO_OUT_IDX = "matrix routing cleared, + pin reverts to plain GPIO". Below 0x100 is the routed signal + source. We filter to the LEDC HS/LS range matching what the + poll path already handled — future peripherals just need to + extend the range here, not add their own poll thread. + """ + if _stopped.is_set(): + return + try: + sid_lo = signal_id & 0xFF + prev = _signal_router.signal_for_gpio(gpio) + if signal_id == 0x100 or signal_id == 0: + # Matrix entry reset → clear routing. Only emit if we + # actually had a previous entry. + if prev is not None: + _signal_router.clear_routing(gpio) + _emit({'type': 'gpio_routing_clear', 'gpio': gpio}) + return + # Only emit for signals the frontend SignalRouter cares + # about (LEDC for now). Other writes are dropped — future + # peripherals (RMT-out, MCPWM) extend the range here. + if SIG_LEDC_HS_CH0_OUT_IDX <= sid_lo <= SIG_LEDC_LS_CH_LAST: + if prev != sid_lo: + _signal_router.update_routing(gpio, sid_lo) + _emit({'type': 'gpio_routing', + 'gpio': gpio, 'signal_id': sid_lo}) + except Exception: + # Iothread callback — never raise, just drop. + pass + # ── Per-slave I2C event counter (for logging) ───────────────────────────── _i2c_event_seq: dict = {} # addr → event count @@ -1052,15 +1093,27 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) # Keep callback struct alive (prevent GC from freeing ctypes closures) _cbs_ref = _CallbacksT( - picsimlab_write_pin = _WRITE_PIN(_on_pin_change), - picsimlab_dir_pin = _DIR_PIN(_on_dir_change), - picsimlab_i2c_event = _I2C_EVENT(_on_i2c_event), - picsimlab_spi_event = _SPI_EVENT(_on_spi_event), - picsimlab_uart_tx_event = _UART_TX(_on_uart_tx), - pinmap = ctypes.cast(_PINMAP, ctypes.c_void_p).value, - picsimlab_rmt_event = _RMT_EVENT(_on_rmt_event), + picsimlab_write_pin = _WRITE_PIN(_on_pin_change), + picsimlab_dir_pin = _DIR_PIN(_on_dir_change), + picsimlab_i2c_event = _I2C_EVENT(_on_i2c_event), + picsimlab_spi_event = _SPI_EVENT(_on_spi_event), + picsimlab_uart_tx_event = _UART_TX(_on_uart_tx), + pinmap = ctypes.cast(_PINMAP, ctypes.c_void_p).value, + picsimlab_rmt_event = _RMT_EVENT(_on_rmt_event), + picsimlab_gpio_matrix_cb = _GPIO_MATRIX_CB(_on_gpio_matrix), ) lib.qemu_picsimlab_register_callbacks(ctypes.byref(_cbs_ref)) + # Log whether the new symbol is present in this libqemu build. + # Older binaries (pre-1.1.0) silently fall back to the 100 ms + # poll path; the WS event shape is identical either way. + try: + if hasattr(lib, 'picsimlab_gpio_matrix_cb'): + _log('[gpio-matrix] libqemu 1.1.0+ detected; callback path active ' + '(poll thread runs as a safety net during burn-in)') + else: + _log('[gpio-matrix] libqemu <1.1.0; using poll path only') + except Exception: + pass # ── 6. QEMU thread ────────────────────────────────────────────────────────