22#
33# SPDX-License-Identifier: MIT
44
5+ import asyncio
56import base64
7+ import inspect
68from pathlib import Path
7- from typing import Any , Optional , Union , cast
9+ from typing import Any , Callable , Optional , Union , cast
810
911from .__version__ import get_version
1012from .constants import DEFAULT_WS_URL
@@ -48,6 +50,7 @@ def __init__(self, token: str, server: Optional[str] = None):
4850 self .last_pause_nanos = 0
4951 self ._transport .add_event_listener ("sim:pause" , self ._on_pause )
5052 self ._pause_queue = EventQueue (self ._transport , "sim:pause" )
53+ self ._serial_monitor_tasks : set [asyncio .Task [None ]] = set ()
5154
5255 async def connect (self ) -> dict [str , Any ]:
5356 """
@@ -61,7 +64,10 @@ async def connect(self) -> dict[str, Any]:
6164 async def disconnect (self ) -> None :
6265 """
6366 Disconnect from the Wokwi simulator server.
67+
68+ This also stops all active serial monitors.
6469 """
70+ self .stop_serial_monitors ()
6571 await self ._transport .close ()
6672
6773 async def upload (self , name : str , content : bytes ) -> None :
@@ -188,6 +194,49 @@ async def restart_simulation(self, pause: bool = False) -> None:
188194 """
189195 await restart (self ._transport , pause )
190196
197+ def serial_monitor (self , callback : Callable [[bytes ], Any ]) -> asyncio .Task [None ]:
198+ """
199+ Start monitoring the serial output in the background and invoke `callback` for each line.
200+
201+ This method **does not block**: it creates and returns an asyncio.Task that runs until the
202+ transport is closed or the task is cancelled. The callback may be synchronous or async.
203+
204+ Example:
205+ task = client.serial_monitor(lambda line: print(line.decode(), end=""))
206+ ... do other async work ...
207+ task.cancel()
208+ """
209+
210+ async def _runner () -> None :
211+ try :
212+ async for line in monitor_lines (self ._transport ):
213+ try :
214+ result = callback (line )
215+ if inspect .isawaitable (result ):
216+ await result
217+ except Exception :
218+ # Swallow callback exceptions to keep the monitor alive.
219+ # Users can add their own error handling inside the callback.
220+ pass
221+ finally :
222+ # Clean up task from the set when it completes
223+ self ._serial_monitor_tasks .discard (task )
224+
225+ task = asyncio .create_task (_runner (), name = "wokwi-serial-monitor" )
226+ self ._serial_monitor_tasks .add (task )
227+ return task
228+
229+ def stop_serial_monitors (self ) -> None :
230+ """
231+ Stop all active serial monitor tasks.
232+
233+ This method cancels all tasks created by the serial_monitor method.
234+ After calling this method, all active serial monitors will stop receiving data.
235+ """
236+ for task in self ._serial_monitor_tasks .copy ():
237+ task .cancel ()
238+ self ._serial_monitor_tasks .clear ()
239+
191240 async def serial_monitor_cat (self , decode_utf8 : bool = True , errors : str = "replace" ) -> None :
192241 """
193242 Print serial monitor output to stdout as it is received from the simulation.
0 commit comments