feat(esp32-worker): register picsimlab_gpio_matrix_cb callback

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.
This commit is contained in:
davidmonterocrespo24 2026-05-19 08:24:32 +02:00
parent bd23a05d23
commit 437f5f83bc
1 changed files with 67 additions and 14 deletions

View File

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