From d6442180b0fb5115ba03843163a2cd168190ac30 Mon Sep 17 00:00:00 2001 From: David Montero Date: Sat, 23 May 2026 22:26:19 +0200 Subject: [PATCH] fix(esp32_worker): skip per-byte log+emit for I2CWriteSink display drivers A 1024-byte oled.show() writevto generates ~1025 calls into _on_i2c_event, one per byte. The previous code called _log (stderr write+flush) AND _emit (stdout JSON write+flush) for every event, saturating the worker subprocess's stdout pipe. The QEMU thread blocks on the synchronous write, the firmware's ESP-IDF i2c_master ISR re-enters before the previous one finished, and the Interrupt watchdog trips on CPU1 with a "Guru Meditation Error: Interrupt wdt timeout" panic on the second consecutive oled.show() call. I2CWriteSink already buffers writes internally and emits a single i2c_transaction event on FINISH, so the per-byte log+emit was pure overhead with no observability value for display drivers (SSD1306, PCF8574). Keep them for everything else. Verified end-to-end via chrome devtools MCP: - Test minimal (init + 1 explicit show): markers OK, no panic - Loop test (10x oled.show() + sleep(0.5)): DONE_LOOP, no panic --- backend/app/services/esp32_worker.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/backend/app/services/esp32_worker.py b/backend/app/services/esp32_worker.py index 0b39edc2..be13000b 100644 --- a/backend/app/services/esp32_worker.py +++ b/backend/app/services/esp32_worker.py @@ -947,15 +947,25 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) else: note = '' - if type(slave).__name__ == 'MPU6050Slave': + slave_type_name = type(slave).__name__ + if slave_type_name == 'MPU6050Slave': seq = _i2c_event_seq n = seq[addr] = seq.get(addr, 0) + 1 _log(f'I2C #{n:03d} bus={bus_id} addr=0x{addr:02x} {op_name} {note}') - else: + elif slave_type_name != 'I2CWriteSink': + # I2CWriteSink fires per-byte for display drivers (SSD1306, + # PCF8574). A 1024-byte writevto (oled.show()) generates + # ~1025 events. Logging each one + emitting a WS message + # blocks the QEMU thread long enough that the firmware's + # ESP-IDF I2C ISR re-enters and trips IWDT on the SECOND + # consecutive show() call. Skip the verbose per-event log + # and WS trace for write-only sinks — the user-visible + # OLED render is what matters, not byte-level tracing. _log(f'I2C bus={bus_id} addr=0x{addr:02x} event=0x{event:04x} ' - f'op={op_name} result=0x{result:02x} slave={type(slave).__name__}') - # Emit trace event to WebSocket so JS test can observe I2C traffic - if not _stopped.is_set(): + f'op={op_name} result=0x{result:02x} slave={slave_type_name}') + # Emit trace event to WebSocket so JS test can observe I2C traffic. + # Skip for I2CWriteSink (display data dumps) — see comment above. + if not _stopped.is_set() and slave_type_name != 'I2CWriteSink': _emit({'type': 'i2c_trace', 'bus': bus_id, 'addr': addr, 'event': event, 'op': op_name, 'result': result, 'reg_ptr': reg_ptr})