Skip to content

Commit 34850fc

Browse files
committed
📝 add async examples #59
1 parent 1eb2c96 commit 34850fc

2 files changed

Lines changed: 238 additions & 0 deletions

File tree

examples/async_client.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import asyncio
2+
import c104
3+
import concurrent.futures
4+
import functools
5+
import logging
6+
7+
8+
def async_exception_handler(task: asyncio.Future):
9+
"""
10+
Callback to log unhandled exceptions in scheduled async tasks.
11+
12+
Required when starting asyncio coroutines from non-async (C++) callbacks,
13+
using `run_coroutine_threadsafe`.
14+
"""
15+
try:
16+
task.result()
17+
except (asyncio.CancelledError, concurrent.futures.CancelledError):
18+
# Silently suppress cancellation
19+
return
20+
except Exception:
21+
logging.error(f"Unhandled exception in coroutine:", exc_info=True)
22+
23+
24+
async def async_measurement(point: c104.Point, message: c104.IncomingMessage) -> None:
25+
"""
26+
Coroutine to handle incoming measurement updates.
27+
28+
Simulates processing delay or background I/O after receiving a value from the C104 server.
29+
"""
30+
print(f"{point.type} MEASUREMENT received on IOA: {point.io_address}, details: {point.info}")
31+
32+
# Optional debug:
33+
# print(f"Raw APDU: {message.raw.hex()}")
34+
# print("Explained:", c104.explain_bytes(apdu=message.raw))
35+
36+
await asyncio.sleep(3) # Simulate processing delay
37+
print("Measurement handling completed after 3 seconds")
38+
39+
40+
def on_receive_point(point: c104.Point, previous_info: c104.Information,
41+
message: c104.IncomingMessage, loop: asyncio.AbstractEventLoop) -> c104.ResponseState:
42+
"""
43+
Synchronous C++-side callback triggered on point update.
44+
45+
Uses `run_coroutine_threadsafe` to schedule the async measurement handler in the asyncio loop.
46+
"""
47+
future = asyncio.run_coroutine_threadsafe(async_measurement(point, message), loop)
48+
future.add_done_callback(async_exception_handler)
49+
50+
# Response must be returned immediately, so the coroutine does not block this call.
51+
return c104.ResponseState.SUCCESS
52+
53+
54+
async def main():
55+
"""
56+
Main async entrypoint for the client.
57+
58+
Initializes a C104 connection and binds monitoring and command points.
59+
Demonstrates how a synchronous C++ module (`c104`) can interoperate with `asyncio`.
60+
"""
61+
loop = asyncio.get_event_loop()
62+
63+
# --- Client Setup ---
64+
client = c104.Client()
65+
connection = client.add_connection(ip="127.0.0.1", port=2404, init=c104.Init.ALL)
66+
station = connection.add_station(common_address=47)
67+
68+
# --- Monitoring Point Setup ---
69+
point = station.add_point(io_address=11, type=c104.Type.M_ME_NC_1)
70+
71+
# The callback uses `functools.partial` to bind the event loop into a synchronous function
72+
point.on_receive(callable=functools.partial(on_receive_point, loop=loop))
73+
74+
# --- Command Point Setup ---
75+
command = station.add_point(io_address=13, type=c104.Type.C_DC_TA_1)
76+
77+
# --- Start the Client ---
78+
client.start()
79+
80+
while connection.state != c104.ConnectionState.OPEN:
81+
print(f"Waiting for connection to {connection.ip}:{connection.port}...")
82+
await asyncio.sleep(1)
83+
84+
print(f"Client connected. Initial point value: {point.value}")
85+
86+
# --- Trigger Point Read ---
87+
print("Triggering point read...")
88+
if point.read():
89+
print(f"-> Read SUCCESS: Value = {point.value}")
90+
else:
91+
print("-> Read FAILURE")
92+
93+
await asyncio.sleep(3)
94+
95+
# --- Transmit a Double Command ---
96+
print("Triggering double command...")
97+
command.info = c104.DoubleCmd(state=c104.Double.ON, qualifier=c104.Qoc.LONG_PULSE)
98+
if command.transmit(cause=c104.Cot.ACTIVATION):
99+
print("-> Command SUCCESS")
100+
else:
101+
print("-> Command FAILURE")
102+
103+
# --- Keep Alive to Process Incoming Data ---
104+
for second in range(60):
105+
if connection.state != c104.ConnectionState.OPEN:
106+
print("Connection closed.")
107+
break
108+
print(f"Client active... ({second + 1}s)")
109+
await asyncio.sleep(1)
110+
111+
112+
if __name__ == "__main__":
113+
# Optional: enable C104 debug output for troubleshooting
114+
# c104.set_debug_mode(c104.Debug.Client|c104.Debug.Connection|c104.Debug.Point|c104.Debug.Callback)
115+
116+
logging.basicConfig(level=logging.INFO)
117+
asyncio.run(main())

examples/async_server.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import asyncio
2+
import c104
3+
import concurrent.futures
4+
import functools
5+
import logging
6+
import random
7+
8+
9+
def async_exception_handler(task: asyncio.Future):
10+
"""
11+
Callback to log unhandled exceptions in scheduled async tasks.
12+
13+
Required when starting asyncio coroutines from non-async (C++) callbacks,
14+
using `run_coroutine_threadsafe`.
15+
"""
16+
try:
17+
task.result()
18+
except (asyncio.CancelledError, concurrent.futures.CancelledError):
19+
# Silently suppress cancellation
20+
pass
21+
except Exception:
22+
logging.error(f"Unhandled exception in coroutine:", exc_info=True)
23+
24+
25+
async def async_command(point: c104.Point):
26+
"""
27+
Coroutine to process a received double command from the C104 stack.
28+
29+
This simulates a long-running operation in response to a command.
30+
"""
31+
print(f"{point.type} DOUBLE COMMAND received on IOA: {point.io_address}, details: {point.info}")
32+
if isinstance(point.info, c104.DoubleCmd) and point.info.qualifier == c104.Qoc.LONG_PULSE:
33+
print("------> Received LONG PULSE")
34+
35+
# Simulated step-wise processing (can be replaced with I/O-bound operations)
36+
await asyncio.sleep(1)
37+
print("async_cmd: after sleep 1s")
38+
await asyncio.sleep(5)
39+
print("async_cmd: after sleep 5s")
40+
await asyncio.sleep(1)
41+
print("async_cmd: after sleep final 1s")
42+
43+
44+
def on_double_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage,
45+
loop: asyncio.AbstractEventLoop) -> c104.ResponseState:
46+
"""
47+
Synchronous callback for C104 command point, triggered by the server.
48+
49+
Schedules an async coroutine (`async_cmd`) using the event loop.
50+
Because this is a synchronous context (C++-backed), we use `run_coroutine_threadsafe`.
51+
"""
52+
future = asyncio.run_coroutine_threadsafe(async_command(point), loop)
53+
future.add_done_callback(async_exception_handler)
54+
55+
# Response must be returned immediately, so the coroutine does not block this call.
56+
return c104.ResponseState.SUCCESS
57+
58+
59+
def random_value(point: c104.Point) -> None:
60+
"""
61+
Synchronous callback to update a monitoring point before it is transmitted.
62+
63+
C104 expects the updated value immediately, so this must remain non-async.
64+
"""
65+
point.value = random.random() * 100 # Simulate a measurement
66+
67+
68+
async def main():
69+
"""
70+
Main async entrypoint for the server.
71+
72+
Initializes a C104 server and binds monitoring and command points.
73+
Demonstrates how a synchronous C++ module (`c104`) can interoperate with `asyncio`.
74+
"""
75+
loop = asyncio.get_event_loop()
76+
77+
# --- Server Setup ---
78+
server = c104.Server()
79+
station = server.add_station(common_address=47)
80+
81+
# --- Monitoring Point Setup ---
82+
point = station.add_point(io_address=11, type=c104.Type.M_ME_NC_1, report_ms=10000)
83+
point.on_before_auto_transmit(callable=random_value)
84+
point.on_before_read(callable=random_value)
85+
86+
# --- Command Point Setup ---
87+
command = station.add_point(io_address=13, type=c104.Type.C_DC_TA_1)
88+
command.value = c104.Double.OFF
89+
90+
# The callback uses `functools.partial` to bind the event loop into a synchronous function
91+
command.on_receive(callable=functools.partial(on_double_command, loop=loop))
92+
93+
# --- Start the Server ---
94+
server.start()
95+
96+
# Wait until a client connects
97+
while not server.has_active_connections:
98+
print("Waiting for client connection...")
99+
await asyncio.sleep(1)
100+
101+
print("Client connected.")
102+
await asyncio.sleep(1)
103+
104+
# if you just want to wait forever uncomment the following two lines and remove the rest of this function
105+
# forever = asyncio.Event()
106+
# await forever.wait()
107+
108+
for second in range(30):
109+
if not server.has_open_connections:
110+
print("Connection closed.")
111+
break
112+
print(f"Server active... ({second + 1}s)")
113+
await asyncio.sleep(1)
114+
115+
116+
if __name__ == "__main__":
117+
# Optional: enable C104 debug output for troubleshooting
118+
# c104.set_debug_mode(c104.Debug.Server|c104.Debug.Point|c104.Debug.Callback)
119+
120+
logging.basicConfig(level=logging.INFO)
121+
asyncio.run(main())

0 commit comments

Comments
 (0)