2
2
#
3
3
# SPDX-License-Identifier: MIT
4
4
5
+ import asyncio
5
6
import base64
7
+ import inspect
6
8
from pathlib import Path
7
- from typing import Any , Optional , Union , cast
9
+ from typing import Any , Callable , Optional , Union , cast
8
10
9
11
from .__version__ import get_version
10
12
from .constants import DEFAULT_WS_URL
@@ -48,6 +50,7 @@ def __init__(self, token: str, server: Optional[str] = None):
48
50
self .last_pause_nanos = 0
49
51
self ._transport .add_event_listener ("sim:pause" , self ._on_pause )
50
52
self ._pause_queue = EventQueue (self ._transport , "sim:pause" )
53
+ self ._serial_monitor_tasks : set [asyncio .Task [None ]] = set ()
51
54
52
55
async def connect (self ) -> dict [str , Any ]:
53
56
"""
@@ -61,7 +64,10 @@ async def connect(self) -> dict[str, Any]:
61
64
async def disconnect (self ) -> None :
62
65
"""
63
66
Disconnect from the Wokwi simulator server.
67
+
68
+ This also stops all active serial monitors.
64
69
"""
70
+ self .stop_serial_monitors ()
65
71
await self ._transport .close ()
66
72
67
73
async def upload (self , name : str , content : bytes ) -> None :
@@ -188,6 +194,49 @@ async def restart_simulation(self, pause: bool = False) -> None:
188
194
"""
189
195
await restart (self ._transport , pause )
190
196
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
+
191
240
async def serial_monitor_cat (self , decode_utf8 : bool = True , errors : str = "replace" ) -> None :
192
241
"""
193
242
Print serial monitor output to stdout as it is received from the simulation.
0 commit comments