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
This commit is contained in:
David Montero 2026-05-23 22:26:19 +02:00
parent b09c8339ac
commit d6442180b0
1 changed files with 15 additions and 5 deletions

View File

@ -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})