Skip to content

Commit cc325df

Browse files
committed
feat: add synchronous client wrapper and example usage for Wokwi simulation
1 parent 538191a commit cc325df

File tree

12 files changed

+401
-48
lines changed

12 files changed

+401
-48
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Wokwi Python Client 🚀
22

3-
Typed, asyncio-friendly Python SDK for the **Wokwi Simulation API**
3+
Typed Python SDK for the **Wokwi Simulation API**, with async and sync APIs
44

55
[![PyPI version](https://img.shields.io/pypi/v/wokwi-client?logo=pypi)](https://pypi.org/project/wokwi-client/)
66
[![Python versions](https://img.shields.io/pypi/pyversions/wokwi-client)](https://pypi.org/project/wokwi-client/)

docs/index.md

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Wokwi Python Client Library
22

3-
Typed, asyncio-friendly Python SDK for the **Wokwi Simulation API**.
3+
Typed Python SDK for the **Wokwi Simulation API**, with async and sync APIs.
44

55
## Features
66

@@ -10,6 +10,7 @@ Typed, asyncio-friendly Python SDK for the **Wokwi Simulation API**.
1010
- Monitor serial output asynchronously and write to them
1111
- Control peripherals and read GPIO pins
1212
- Fully type-annotated and easy to use with asyncio
13+
- Async and sync APIs
1314

1415
## Installation
1516

@@ -25,37 +26,74 @@ Get your API token from [https://wokwi.com/dashboard/ci](https://wokwi.com/dashb
2526

2627
## Quickstart Example
2728

28-
```python
29-
import asyncio
30-
import os
31-
from wokwi_client import WokwiClient, GET_TOKEN_URL
32-
33-
34-
async def main():
35-
token = os.getenv("WOKWI_CLI_TOKEN")
36-
if not token:
37-
raise SystemExit(
38-
f"Set WOKWI_CLI_TOKEN in your environment. You can get it from {GET_TOKEN_URL}."
39-
)
40-
41-
client = WokwiClient(token)
42-
await client.connect()
43-
await client.upload_file("diagram.json")
44-
await client.upload_file("firmware.bin")
45-
await client.start_simulation(firmware="firmware.bin")
46-
serial_task = asyncio.create_task(
47-
client.serial_monitor_cat()
48-
) # Stream serial output
49-
await client.wait_until_simulation_time(10) # Run simulation for 10 seconds
50-
serial_task.cancel()
51-
await client.disconnect()
52-
53-
54-
if __name__ == "__main__":
55-
asyncio.run(main())
56-
```
29+
=== "Async (recommended)"
30+
31+
```python
32+
import asyncio
33+
import os
34+
from wokwi_client import WokwiClient, GET_TOKEN_URL
35+
36+
37+
async def main():
38+
token = os.getenv("WOKWI_CLI_TOKEN")
39+
if not token:
40+
raise SystemExit(
41+
f"Set WOKWI_CLI_TOKEN in your environment. You can get it from {GET_TOKEN_URL}."
42+
)
43+
44+
client = WokwiClient(token)
45+
await client.connect()
46+
await client.upload_file("diagram.json")
47+
await client.upload_file("firmware.bin")
48+
await client.start_simulation(firmware="firmware.bin")
49+
50+
serial_task = asyncio.create_task(
51+
client.serial_monitor_cat()
52+
) # Stream serial output
53+
await client.wait_until_simulation_time(10) # Run simulation for 10 seconds
54+
serial_task.cancel()
55+
await client.disconnect()
56+
57+
58+
if __name__ == "__main__":
59+
asyncio.run(main())
60+
```
61+
62+
=== "Sync"
63+
64+
```python
65+
import os
66+
from wokwi_client import WokwiClientSync, GET_TOKEN_URL
67+
68+
69+
def main():
70+
token = os.getenv("WOKWI_CLI_TOKEN")
71+
if not token:
72+
raise SystemExit(
73+
f"Set WOKWI_CLI_TOKEN in your environment. You can get it from {GET_TOKEN_URL}."
74+
)
75+
76+
client = WokwiClientSync(token)
77+
client.connect()
78+
client.upload_file("diagram.json")
79+
client.upload_file("firmware.bin")
80+
client.start_simulation(firmware="firmware.bin")
81+
82+
# Stream serial output concurrently for 10 seconds
83+
client.monitor_serial(lambda line: print(line.decode("utf-8"), end="", flush=True))
84+
client.wait_until_simulation_time(10)
85+
client.disconnect()
86+
87+
88+
if __name__ == "__main__":
89+
main()
90+
```
91+
92+
## Examples
5793

58-
See the [examples/hello_esp32/main.py](https://github.com/wokwi/wokwi-python-client/blob/main/examples/hello_esp32/main.py) for a full example including serial monitoring, and [examples/micropython_esp32/main.py](https://github.com/wokwi/wokwi-python-client/blob/main/examples/micropython_esp32/main.py) for an example of running MicroPython on a simulated ESP32 board.
94+
- [examples/hello_esp32/main.py](https://github.com/wokwi/wokwi-python-client/blob/main/examples/hello_esp32/main.py) full async example including serial monitoring
95+
- [examples/hello_esp32_sync/main.py](https://github.com/wokwi/wokwi-python-client/blob/main/examples/hello_esp32_sync/main.py) sync wrapper usage
96+
- [examples/micropython_esp32/main.py](https://github.com/wokwi/wokwi-python-client/blob/main/examples/micropython_esp32/main.py) example of running MicroPython on a simulated ESP32 board.
5997

6098
## API Reference
6199

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore the firmware files, as they are downloaded from the internet
2+
hello_world.bin
3+
hello_world.elf

examples/hello_esp32_sync/__init__.py

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"version": 1,
3+
"author": "Uri Shaked",
4+
"editor": "wokwi",
5+
"parts": [
6+
{
7+
"type": "wokwi-esp32-devkit-v1",
8+
"id": "esp",
9+
"top": 0,
10+
"left": 0,
11+
"attrs": { "fullBoot": "1" }
12+
}
13+
],
14+
"connections": [
15+
["esp:TX0", "$serialMonitor:RX", "", []],
16+
["esp:RX0", "$serialMonitor:TX", "", []]
17+
],
18+
"serialMonitor": {
19+
"display": "terminal"
20+
},
21+
"dependencies": {}
22+
}

examples/hello_esp32_sync/main.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (C) 2025, CodeMagic LTD
3+
4+
import os
5+
from pathlib import Path
6+
7+
import requests
8+
9+
from wokwi_client import GET_TOKEN_URL, WokwiClientSync
10+
11+
EXAMPLE_DIR = Path(__file__).parent
12+
HELLO_WORLD_URL = "https://github.com/wokwi/esp-idf-hello-world/raw/refs/heads/main/bin"
13+
FIRMWARE_FILES = {
14+
"hello_world.bin": f"{HELLO_WORLD_URL}/hello_world.bin",
15+
"hello_world.elf": f"{HELLO_WORLD_URL}/hello_world.elf",
16+
}
17+
SLEEP_TIME = int(os.getenv("WOKWI_SLEEP_TIME", "10"))
18+
19+
20+
def main() -> None:
21+
token = os.getenv("WOKWI_CLI_TOKEN")
22+
if not token:
23+
raise SystemExit(
24+
f"Set WOKWI_CLI_TOKEN in your environment. You can get it from {GET_TOKEN_URL}."
25+
)
26+
27+
for filename, url in FIRMWARE_FILES.items():
28+
if (EXAMPLE_DIR / filename).exists():
29+
continue
30+
print(f"Downloading {filename} from {url}")
31+
response = requests.get(url)
32+
response.raise_for_status()
33+
with open(EXAMPLE_DIR / filename, "wb") as f:
34+
f.write(response.content)
35+
36+
client = WokwiClientSync(token)
37+
print(f"Wokwi client library version: {client.version}")
38+
39+
hello = client.connect()
40+
print("Connected to Wokwi Simulator, server version:", hello["version"])
41+
42+
# Upload the diagram and firmware files
43+
client.upload_file("diagram.json", EXAMPLE_DIR / "diagram.json")
44+
client.upload_file("hello_world.bin", EXAMPLE_DIR / "hello_world.bin")
45+
client.upload_file("hello_world.elf", EXAMPLE_DIR / "hello_world.elf")
46+
47+
# Start the simulation
48+
client.start_simulation(
49+
firmware="hello_world.bin",
50+
elf="hello_world.elf",
51+
)
52+
53+
# Stream serial output for a few seconds (non-blocking)
54+
client.monitor_serial(lambda line: print(line.decode("utf-8"), end="", flush=True))
55+
print(f"Simulation started, waiting for {SLEEP_TIME} seconds…")
56+
client.wait_until_simulation_time(SLEEP_TIME)
57+
58+
# Disconnect from the simulator
59+
client.disconnect()
60+
61+
62+
if __name__ == "__main__":
63+
main()

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ markdown_extensions:
4242
- pymdownx.inlinehilite
4343
- pymdownx.superfences
4444
- pymdownx.tabbed
45+
- pymdownx.tabbed:
46+
alternate_style: true
4547
- toc:
4648
permalink: ""
4749

src/wokwi_client/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
from .__version__ import get_version
1414
from .client import WokwiClient
15+
from .client_sync import WokwiClientSync
1516
from .constants import GET_TOKEN_URL
1617

1718
__version__ = get_version()
18-
__all__ = ["WokwiClient", "__version__", "GET_TOKEN_URL"]
19+
__all__ = ["WokwiClient", "WokwiClientSync", "__version__", "GET_TOKEN_URL"]

0 commit comments

Comments
 (0)