Skip to content

Commit 21af65c

Browse files
authored
0.9.0 Release.
2 parents 7df8062 + 82bb1d8 commit 21af65c

File tree

7 files changed

+364
-25
lines changed

7 files changed

+364
-25
lines changed

bellows/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
MAJOR_VERSION = 0
2-
MINOR_VERSION = 8
3-
PATCH_VERSION = '2'
2+
MINOR_VERSION = 9
3+
PATCH_VERSION = '0'
44
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
55
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)

bellows/thread.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import asyncio
2+
import logging
3+
4+
import sys
5+
6+
import functools
7+
from concurrent.futures import ThreadPoolExecutor
8+
9+
LOGGER = logging.getLogger(__name__)
10+
11+
12+
class EventLoopThread:
13+
''' Run a parallel event loop in a separate thread '''
14+
def __init__(self):
15+
self.loop = None
16+
self.thread_complete = None
17+
18+
def run_coroutine_threadsafe(self, coroutine):
19+
current_loop = asyncio.get_event_loop()
20+
future = asyncio.run_coroutine_threadsafe(coroutine, self.loop)
21+
return asyncio.wrap_future(future, loop=current_loop)
22+
23+
def _thread_main(self, init_task):
24+
self.loop = asyncio.new_event_loop()
25+
asyncio.set_event_loop(self.loop)
26+
27+
try:
28+
self.loop.run_until_complete(init_task)
29+
self.loop.run_forever()
30+
finally:
31+
self.loop.close()
32+
self.loop = None
33+
34+
async def start(self):
35+
current_loop = asyncio.get_event_loop()
36+
if self.loop is not None and not self.loop.is_closed():
37+
return
38+
39+
executor_opts = {'max_workers': 1}
40+
if sys.version_info[:2] >= (3, 6):
41+
executor_opts['thread_name_prefix'] = __name__
42+
executor = ThreadPoolExecutor(**executor_opts)
43+
44+
thread_started_future = current_loop.create_future()
45+
46+
async def init_task():
47+
current_loop.call_soon_threadsafe(thread_started_future.set_result, None)
48+
49+
# Use current loop so current loop has a reference to the long-running thread as one of its tasks
50+
thread_complete = current_loop.run_in_executor(executor, self._thread_main, init_task())
51+
self.thread_complete = thread_complete
52+
current_loop.call_soon(executor.shutdown, False)
53+
await thread_started_future
54+
return thread_complete
55+
56+
def force_stop(self):
57+
if self.loop is not None:
58+
self.loop.call_soon_threadsafe(self.loop.stop)
59+
60+
61+
class ThreadsafeProxy:
62+
''' Proxy class which enforces threadsafe non-blocking calls
63+
This class can be used to wrap an object to ensure any calls
64+
using that object's methods are done on a particular event loop
65+
'''
66+
def __init__(self, obj, obj_loop):
67+
self._obj = obj
68+
self._obj_loop = obj_loop
69+
70+
def __getattr__(self, name):
71+
func = getattr(self._obj, name)
72+
if not callable(func):
73+
raise TypeError("Can only use ThreadsafeProxy with callable attributes: {}.{}".format(
74+
self._obj.__class__.__name__, name))
75+
76+
def func_wrapper(*args, **kwargs):
77+
loop = self._obj_loop
78+
curr_loop = asyncio.get_event_loop()
79+
call = functools.partial(func, *args, **kwargs)
80+
if loop == curr_loop:
81+
return call()
82+
if loop.is_closed():
83+
# Disconnected
84+
LOGGER.warning("Attempted to use a closed event loop")
85+
return
86+
if asyncio.iscoroutinefunction(func):
87+
future = asyncio.run_coroutine_threadsafe(call(), loop)
88+
return asyncio.wrap_future(future, loop=curr_loop)
89+
else:
90+
def check_result_wrapper():
91+
result = call()
92+
if result is not None:
93+
raise TypeError("ThreadsafeProxy can only wrap functions with no return value \
94+
\nUse an async method to return values: {}.{}".format(
95+
self._obj.__class__.__name__, name))
96+
97+
loop.call_soon_threadsafe(check_result_wrapper)
98+
return func_wrapper

bellows/uart.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import serial
66
import serial_asyncio
77

8+
from bellows.thread import EventLoopThread, ThreadsafeProxy
9+
810
import bellows.types as t
911

1012
LOGGER = logging.getLogger(__name__)
@@ -27,7 +29,7 @@ class Gateway(asyncio.Protocol):
2729
class Terminator:
2830
pass
2931

30-
def __init__(self, application, connected_future=None):
32+
def __init__(self, application, connected_future=None, connection_done_future=None):
3133
self._send_seq = 0
3234
self._rec_seq = 0
3335
self._buffer = b''
@@ -36,6 +38,7 @@ def __init__(self, application, connected_future=None):
3638
self._connected_future = connected_future
3739
self._sendq = asyncio.Queue()
3840
self._pending = (-1, None)
41+
self._connection_done_future = connection_done_future
3942

4043
def connection_made(self, transport):
4144
"""Callback when the uart is connected"""
@@ -173,20 +176,22 @@ def _reset_cleanup(self, future):
173176

174177
def connection_lost(self, exc):
175178
"""Port was closed unexpectedly."""
179+
if self._connection_done_future:
180+
self._connection_done_future.set_result(exc)
176181
if exc is None:
177182
LOGGER.debug("Closed serial connection")
178183
return
179184

180185
LOGGER.error("Lost serial connection: %s", exc)
181186
self._application.connection_lost(exc)
182187

183-
def reset(self):
188+
async def reset(self):
184189
"""Send a reset frame and init internal state."""
185190
LOGGER.debug("Resetting ASH")
186191
if self._reset_future is not None:
187192
LOGGER.error(("received new reset request while an existing "
188193
"one is in progress"))
189-
return self._reset_future
194+
return await self._reset_future
190195

191196
self._send_seq = 0
192197
self._rec_seq = 0
@@ -197,10 +202,10 @@ def reset(self):
197202
self._pending[1].set_result(True)
198203
self._pending = (-1, None)
199204

200-
self._reset_future = asyncio.Future()
205+
self._reset_future = asyncio.get_event_loop().create_future()
201206
self._reset_future.add_done_callback(self._reset_cleanup)
202207
self.write(self._rst_frame())
203-
return asyncio.wait_for(self._reset_future, timeout=RESET_TIMEOUT)
208+
return await asyncio.wait_for(self._reset_future, timeout=RESET_TIMEOUT)
204209

205210
async def _send_task(self):
206211
"""Send queue handler"""
@@ -212,7 +217,7 @@ async def _send_task(self):
212217
success = False
213218
rxmit = 0
214219
while not success:
215-
self._pending = (seq, asyncio.Future())
220+
self._pending = (seq, asyncio.get_event_loop().create_future())
216221
self.write(self._data_frame(data, seq, rxmit))
217222
rxmit = 1
218223
success = await self._pending[1]
@@ -305,12 +310,12 @@ def _unstuff(self, s):
305310
return out
306311

307312

308-
async def connect(port, baudrate, application, loop=None):
309-
if loop is None:
310-
loop = asyncio.get_event_loop()
313+
async def _connect(port, baudrate, application):
314+
loop = asyncio.get_event_loop()
311315

312-
connection_future = asyncio.Future()
313-
protocol = Gateway(application, connection_future)
316+
connection_future = loop.create_future()
317+
connection_done_future = loop.create_future()
318+
protocol = Gateway(application, connection_future, connection_done_future)
314319

315320
transport, protocol = await serial_asyncio.create_serial_connection(
316321
loop,
@@ -324,4 +329,17 @@ async def connect(port, baudrate, application, loop=None):
324329

325330
await connection_future
326331

332+
thread_safe_protocol = ThreadsafeProxy(protocol, loop)
333+
return thread_safe_protocol, connection_done_future
334+
335+
336+
async def connect(port, baudrate, application, use_thread=True):
337+
if use_thread:
338+
application = ThreadsafeProxy(application, asyncio.get_event_loop())
339+
thread = EventLoopThread()
340+
await thread.start()
341+
protocol, connection_done = await thread.run_coroutine_threadsafe(_connect(port, baudrate, application))
342+
connection_done.add_done_callback(lambda _: thread.force_stop())
343+
else:
344+
protocol, _ = await _connect(port, baudrate, application)
327345
return protocol

tests/test_application.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ async def mocksend(method, nwk, aps_frame, seq, data):
343343
return [returnvals.pop(0)]
344344

345345
def mock_get_device(*args, **kwargs):
346-
dev = Device(app, mock.sentinel.ieee, mock.sentinel.nwk)
346+
dev = Device(app, mock.sentinel.ieee, 0xaa55)
347347
dev.node_desc = mock.MagicMock()
348348
dev.node_desc.is_end_device = is_an_end_dev
349349
return dev

0 commit comments

Comments
 (0)