From a570d203f214f74df210a4d93cb22ccbbe5169f6 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 8 Jan 2021 12:10:23 +0100 Subject: [PATCH] Squashed 'tools/odrive/pyfibre/' changes from f9ebfdd3..b00819a0 b00819a0 add fibre.cpp 09687c5a update libfibre API for better performance 50270add update binaries 9f1fffd0 support registering external discoverers 5783e8e9 support open-ended function calls in libfibre ABI 30498039 move gitattributes for LFS settings f779a895 Merge commit 'ca1f41bc7b3acabfa2924822b87a28726e75c5d5' as 'python' 0c229c4c remove old python folder 99cec00a Merge commit 'e345a021e39fd3d6ea56297e9c6c647e8d144992' as 'cpp' e345a021 Squashed 'cpp/' content from commit d7a5c366 7a40ccf0 remove old cpp folder 920a8fb4 reduce libfibre binary size 646d5ade make odrive.find_any work with no args 53a0f76a add finishing touches to initial libfibre version 11a86c7e Merge pull request #10 from RoCkaZ/master 2e2226f6 Only set new timeout if it is different from the old one dc2bea4a introduce codecs for serializing/deserializing 677f3686 Merge commit '5a7cad54ad0bed99edcdf19704d5116a9993caa6' into sam_fibre_upstreaming 1ada8977 TCP: keep alive fd46431e improve logging b9a249a2 fix timeout error code for macOS d9f0f539 remove install-time PySerial dependency and don't fail if installed as non-root d27ff81b printing format strings a75dce81 fix shell printing 1544230c clean up fiber port 25c2bb34 fix include paths, duplicated function in merge 3661fdaf Merge branch 'devel' into sam_fibre 55d50e97 Merge commit '496a9a945a8d8bbdda556a41f784cb30ebf6eeea' 7d2df781 add timeout to UDP transport 808f3f0c split protocol.hpp into more files a8931e85 Merge remote-tracking branch 'origin/master' e87ae1dd minor bugfixes 56ccd7c4 Merge commit '2e15116dc21a4ff859b906410dec8c797955c0ec' 565cd9d4 Merge commit 'd2ca264d0a3f7eab82498880a8a1929380e0ce75' 0996d4eb replace occurrences of the term protocol with fibre 719359ea add setup.py to publish fibre on PyPI 281af8c9 dont run Fibre tup rules by default Building the Fibre test binaries is distracting when it's just used as a library in another project. Now it is only built if CONFIG_BUILD_FIBRE_TESTS=true 8f08be86 allow setting interactive shell variables, fix some warnings 64999f82 move updated ODrive python protocol library to Fibre bc1a4555 fix function calls with arguments b34163f4 apply ODrive native protocol updates to fibre and switch to fibre 1ad444c1 update fibre readme to better reflect the current status 8f0d2678 compile with -Wall ab8a2f09 use universal references in for encoder and decoder construction 2c8e7b89 Merge branch 'master' into lowlevel 45a3514d Merge pull request #5 from samuelsadok/templated_endpoints 00c4a042 Refactor the way endpoint are defined. 5de6afa3 change CRC8 template types 36d501e7 implement selected encoders b0abce47 fix decoder bugs f6b95c96 Update README.md 4fb1eba0 Add a couple of decoders 2c0a62dd switch to TCP a2fb4a15 Merge commit '2dda07b9e31fe26d60b60e5a4b2495370a63e4fc' 2dda07b9 Squashed 'fibre/' changes from 74293c7..874cb53 e49db3b0 Merge branch 'master' of github.com:samuelsadok/lightd 874cb533 note about coding style 7e76ccf1 Merge pull request #2 from mbiegert/master 9061637b make udp server stack IPv6 aware 38044773 IPv6 in python --not-working cfc87be5 don't use recursion in tcp_transport.py 6d19cc2b target comments by @samuelsadok bee3128e remove tupfile and add TCP to compat list, also mv server to test 52b149ce fix abstract python function StreamSource::get_bytes c17e2d4b add tcp discovery to python client implementation 239d0630 server opens a thread for every incoming client connection 6b7cfa30 create test server on tcp instead of udp 2eddbb9a add tupfile for compiling 4aa16811 Add small test Server 62feaafd create rudimentary tcp implementation db8d11db Squashed 'fibre/' changes from 20bd0f2..74293c7 a88d5661 Merge commit 'db8d11db47aac77859720a794cf3d0c54ed11eaf' 74293c75 Merge branch 'lightd-changes' of github.com:samuelsadok/fibre 9b148486 fix size returned by read_le/write_le 7b8f30d0 add reference to lightd 839c92a9 fix lights-off timer 06874f4f add systemd files and install script 0ee4b929 add readme 81a30238 minor python util fixes e31270be minor python util fixes 24b7f58d don't fail if python serial-module missing bb5b28b9 don't fail if python serial-module missing dbd1c799 add TOOLCHAIN var to tup 3677d800 add lightctl util 4d476b05 Squashed 'fibre/' changes from 09fbec9..20bd0f2 d206d6b9 Merge commit '4d476b05ef6400192a2edafab6449b25e53921e2' 20bd0f29 simplify addin python module inother projects 0d9b0ddb implement LED control service 163bc8e1 add gitignore 4b4af623 add rpi_ws281x library 69e92d1f Merge commit '5230fb499da4efbf8f5ab586a0398379573b5b37' as 'rpi_ws281x' 5230fb49 Squashed 'rpi_ws281x/' content from commit 3a01777 e99c592e Squashed 'fibre/' content from commit 09fbec9 419759d1 Merge commit 'e99c592e217595deee6765ff0ed7e25ca26f772e' as 'fibre' 4303df51 initial commit 09fbec97 initial commit git-subtree-dir: tools/odrive/pyfibre git-subtree-split: b00819a0098a1caa09077857c9728a11a195e108 --- .gitattributes | 3 + README.md | 17 + fibre/__init__.py | 2 +- fibre/libfibre-linux-amd64.so | 3 + fibre/libfibre-linux-armhf.so | 3 + fibre/libfibre-macos-x86.dylib | 3 + fibre/libfibre-windows-amd64.dll | 3 + fibre/libfibre.py | 704 +++++++++++++++++++++++-------- fibre/libwinpthread-1.dll | Bin 0 -> 61576 bytes fibre/shell.py | 3 +- fibre/utils.py | 6 - 11 files changed, 571 insertions(+), 176 deletions(-) create mode 100644 .gitattributes create mode 100644 README.md create mode 100755 fibre/libfibre-linux-amd64.so create mode 100755 fibre/libfibre-linux-armhf.so create mode 100644 fibre/libfibre-macos-x86.dylib create mode 100755 fibre/libfibre-windows-amd64.dll create mode 100755 fibre/libwinpthread-1.dll diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..4a382938 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +libfibre*.so filter=lfs diff=lfs merge=lfs -text +libfibre*.dll filter=lfs diff=lfs merge=lfs -text +libfibre*.dylib filter=lfs diff=lfs merge=lfs -text diff --git a/README.md b/README.md new file mode 100644 index 00000000..cfaec288 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# PyFibre + +This directory provides Python bindings for [Fibre](https://github.com/samuelsadok/fibre). Its home is located [here](https://github.com/samuelsadok/fibre/tree/master/python). There's also a standalone repository for this directory [here](https://github.com/samuelsadok/pyfibre). + +## Current Status + +Currently only client-side features are implemented, that means you can discover objects but you cannot publish objects. + +## How to use + +```python +import fibre + +with fibre.Domain("tcp-client:address=localhost,port=14220") as domain: + obj = domain.discover_one() + obj.test_function() +``` diff --git a/fibre/__init__.py b/fibre/__init__.py index 216f2e06..1ee712e9 100644 --- a/fibre/__init__.py +++ b/fibre/__init__.py @@ -1,4 +1,4 @@ from .utils import Event, Logger, TimeoutError from .shell import launch_shell -from .libfibre import find_all, find_any, ObjectLostError +from .libfibre import Domain, ObjectLostError diff --git a/fibre/libfibre-linux-amd64.so b/fibre/libfibre-linux-amd64.so new file mode 100755 index 00000000..00cbd444 --- /dev/null +++ b/fibre/libfibre-linux-amd64.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d235ce21bf52f537f17cc317a141d0f9f0f2d1683c20222aecdae4cc1c5a4295 +size 3757680 diff --git a/fibre/libfibre-linux-armhf.so b/fibre/libfibre-linux-armhf.so new file mode 100755 index 00000000..2f3e9eb2 --- /dev/null +++ b/fibre/libfibre-linux-armhf.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528c680e0417085e7187a231c05fc200ff03b9d903cb8418fbe63e9fbd66ac3d +size 750616 diff --git a/fibre/libfibre-macos-x86.dylib b/fibre/libfibre-macos-x86.dylib new file mode 100644 index 00000000..2af09dec --- /dev/null +++ b/fibre/libfibre-macos-x86.dylib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf5105038e2787b8e3e05600a8d303a24dae6052364201c6ecb3233e3a947ddb +size 657528 diff --git a/fibre/libfibre-windows-amd64.dll b/fibre/libfibre-windows-amd64.dll new file mode 100755 index 00000000..868fc1e2 --- /dev/null +++ b/fibre/libfibre-windows-amd64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:727e24428ad48184b891a57e27763e3cd9be8e98a38befc5279d3fb11f11566d +size 1239566 diff --git a/fibre/libfibre.py b/fibre/libfibre.py index 038f51d7..b623fa56 100644 --- a/fibre/libfibre.py +++ b/fibre/libfibre.py @@ -9,8 +9,13 @@ from types import MethodType import concurrent import threading import time -from fibre.utils import Logger, Event import platform +from .utils import Logger, Event +import sys + +# Enable this for better tracebacks in some cases +#import tracemalloc +#tracemalloc.start(10) lib_names = { ('Linux', 'x86_64'): 'libfibre-linux-amd64.so', @@ -21,35 +26,54 @@ lib_names = { system_desc = (platform.system(), platform.machine()) -lib_dir = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))), - 'cpp') +script_dir = os.path.dirname(os.path.realpath(__file__)) +fibre_cpp_paths = [ + os.path.join(os.path.dirname(os.path.dirname(script_dir)), "cpp"), + os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir)))), "Firmware", "fibre-cpp") +] -def test_path(path): - return path if os.path.isfile(path) else None +def get_first(lst, predicate, default): + for item in lst: + if predicate(item): + return item + return default -lib_path = (test_path(os.path.join(lib_dir, 'libfibre.so')) or - test_path(os.path.join(lib_dir, 'libfibre.dll')) or - (test_path(os.path.join(lib_dir, lib_names[system_desc])) if (system_desc in lib_names) else None)) +if not system_desc in lib_names: + fibre_cpp_path = get_first(fibre_cpp_paths, os.path.isdir, None) + + if fibre_cpp_path is None: + instructions = ("Go to https://github.com/samuelsadok/fibre-cpp for " + "instructions on how to compile libfibre. Once you have compiled it, " + "add it to this folder.") + else: + instructions = ("Go to {} and run `make`. Then edit this file (libfibre.py) " + "to include the name of the binary that was generated by `make`.".format(fibre_cpp_path)) + + raise ModuleNotFoundError("libfibre is not supported on your platform ({} {}). {}".format(*system_desc, instructions)) + +lib_name = lib_names[system_desc] +search_paths = fibre_cpp_paths + [script_dir] + +lib_path = get_first( + (os.path.join(p, lib_name) for p in search_paths), + os.path.isfile, None) if lib_path is None: - raise ModuleNotFoundError("This package has no precompiled libfibre for your platform ({} {}). " - "Go to fibre/cpp/ and run `make` to compile libfibre for your platform.".format(*system_desc)) + raise ModuleNotFoundError("{} was not found in {}".format(lib_name, search_paths)) lib = windll.LoadLibrary(lib_path) if os.name == 'nt' else cdll.LoadLibrary(lib_path) # libfibre definitions --------------------------------------------------------# -PostSignature = CFUNCTYPE(c_void_p, CFUNCTYPE(None, c_void_p), POINTER(c_int)) -RegisterEventSignature = CFUNCTYPE(c_int, c_int, c_uint32, CFUNCTYPE(None, c_void_p), POINTER(c_int)) +PostSignature = CFUNCTYPE(c_int, CFUNCTYPE(None, c_void_p), POINTER(c_int)) +RegisterEventSignature = CFUNCTYPE(c_int, c_int, c_uint32, CFUNCTYPE(None, c_void_p, c_int), POINTER(c_int)) DeregisterEventSignature = CFUNCTYPE(c_int, c_int) CallLaterSignature = CFUNCTYPE(c_void_p, c_float, CFUNCTYPE(None, c_void_p), POINTER(c_int)) CancelTimerSignature = CFUNCTYPE(c_int, c_void_p) -ConstructObjectSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_void_p, c_size_t) -DestroyObjectSignature = CFUNCTYPE(None, c_void_p, c_void_p) -OnFoundObjectSignature = CFUNCTYPE(None, c_void_p, c_void_p) +OnFoundObjectSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p) +OnLostObjectSignature = CFUNCTYPE(None, c_void_p, c_void_p) OnStoppedSignature = CFUNCTYPE(None, c_void_p, c_int) OnAttributeAddedSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_size_t, c_void_p, c_void_p, c_size_t) @@ -57,13 +81,18 @@ OnAttributeRemovedSignature = CFUNCTYPE(None, c_void_p, c_void_p) OnFunctionAddedSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_size_t, POINTER(c_char_p), POINTER(c_char_p), POINTER(c_char_p), POINTER(c_char_p)) OnFunctionRemovedSignature = CFUNCTYPE(None, c_void_p, c_void_p) -OnCallCompletedSignature = CFUNCTYPE(None, c_void_p, c_int, c_char_p) +OnCallCompletedSignature = CFUNCTYPE(c_int, c_void_p, c_int, c_void_p, c_void_p, POINTER(c_void_p), POINTER(c_size_t), POINTER(c_void_p), POINTER(c_size_t)) +OnTxCompletedSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_int, c_void_p) +OnRxCompletedSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_int, c_void_p) kFibreOk = 0 -kFibreCancelled = 1 -kFibreClosed = 2 -kFibreInvalidArgument = 3 -kFibreInternalError = 4 +kFibreBusy = 1 +kFibreCancelled = 2 +kFibreClosed = 3 +kFibreInvalidArgument = 4 +kFibreInternalError = 5 +kFibreProtocolError = 6 +kFibreHostUnreachable = 7 class LibFibreVersion(Structure): _fields_ = [ @@ -75,28 +104,45 @@ class LibFibreVersion(Structure): def __repr__(self): return "{}.{}.{}".format(self.major, self.minor, self.patch) +class LibFibreEventLoop(Structure): + _fields_ = [ + ("post", PostSignature), + ("register_event", RegisterEventSignature), + ("deregister_event", DeregisterEventSignature), + ("call_later", CallLaterSignature), + ("cancel_timer", CancelTimerSignature), + ] + libfibre_get_version = lib.libfibre_get_version libfibre_get_version.argtypes = [] libfibre_get_version.restype = POINTER(LibFibreVersion) version = libfibre_get_version().contents -if version.major != 0: +if (version.major, version.minor) != (0, 1): raise Exception("Incompatible libfibre version: {}".format(version)) libfibre_open = lib.libfibre_open -libfibre_open.argtypes = [PostSignature, RegisterEventSignature, DeregisterEventSignature, CallLaterSignature, CancelTimerSignature, ConstructObjectSignature, DestroyObjectSignature, c_void_p] +libfibre_open.argtypes = [LibFibreEventLoop] libfibre_open.restype = c_void_p libfibre_close = lib.libfibre_close libfibre_close.argtypes = [c_void_p] libfibre_close.restype = None +libfibre_open_domain = lib.libfibre_open_domain +libfibre_open_domain.argtypes = [c_void_p, c_char_p, c_size_t] +libfibre_open_domain.restype = c_void_p + +libfibre_close_domain = lib.libfibre_close_domain +libfibre_close_domain.argtypes = [c_void_p] +libfibre_close_domain.restype = None + libfibre_start_discovery = lib.libfibre_start_discovery -libfibre_start_discovery.argtypes = [c_void_p, c_char_p, c_size_t, c_void_p, OnFoundObjectSignature, OnStoppedSignature, c_void_p] -libfibre_start_discovery.restype = c_void_p +libfibre_start_discovery.argtypes = [c_void_p, c_void_p, OnFoundObjectSignature, OnLostObjectSignature, OnStoppedSignature, c_void_p] +libfibre_start_discovery.restype = None libfibre_stop_discovery = lib.libfibre_stop_discovery -libfibre_stop_discovery.argtypes = [c_void_p, c_void_p] +libfibre_stop_discovery.argtypes = [c_void_p] libfibre_stop_discovery.restype = None libfibre_subscribe_to_interface = lib.libfibre_subscribe_to_interface @@ -107,13 +153,25 @@ libfibre_get_attribute = lib.libfibre_get_attribute libfibre_get_attribute.argtypes = [c_void_p, c_void_p, POINTER(c_void_p)] libfibre_get_attribute.restype = c_int -libfibre_start_call = lib.libfibre_start_call -libfibre_start_call.argtypes = [c_void_p, c_void_p, c_char_p, c_size_t, c_char_p, c_size_t, c_void_p, OnCallCompletedSignature, c_void_p] -libfibre_start_call.restype = None +libfibre_call = lib.libfibre_call +libfibre_call.argtypes = [c_void_p, POINTER(c_void_p), c_int, c_void_p, c_size_t, c_void_p, c_size_t, POINTER(c_void_p), POINTER(c_void_p), OnCallCompletedSignature, c_void_p] +libfibre_call.restype = c_int -libfibre_cancel_call = lib.libfibre_cancel_call -libfibre_cancel_call.argtypes = [c_void_p] -libfibre_cancel_call.restype = None +libfibre_start_tx = lib.libfibre_start_tx +libfibre_start_tx.argtypes = [c_void_p, c_char_p, c_size_t, OnTxCompletedSignature, c_void_p] +libfibre_start_tx.restype = None + +libfibre_cancel_tx = lib.libfibre_cancel_tx +libfibre_cancel_tx.argtypes = [c_void_p] +libfibre_cancel_tx.restype = None + +libfibre_start_rx = lib.libfibre_start_rx +libfibre_start_rx.argtypes = [c_void_p, c_char_p, c_size_t, OnRxCompletedSignature, c_void_p] +libfibre_start_rx.restype = None + +libfibre_cancel_rx = lib.libfibre_cancel_rx +libfibre_cancel_rx.argtypes = [c_void_p] +libfibre_cancel_rx.restype = None # libfibre wrapper ------------------------------------------------------------# @@ -128,11 +186,15 @@ def _get_exception(status): elif status == kFibreCancelled: return asyncio.CancelledError() elif status == kFibreClosed: - return ObjectLostError() + return EOFError() elif status == kFibreInvalidArgument: return ArgumentError() elif status == kFibreInternalError: return Exception("internal libfibre error") + elif status == kFibreProtocolError: + return Exception("peer misbehaving") + elif status == kFibreHostUnreachable: + return ObjectLostError() else: return Exception("unknown libfibre error {}".format(status)) @@ -208,12 +270,237 @@ def run_coroutine_threadsafe(loop, func): future = concurrent.futures.Future() async def func_async(): try: - future.set_result(await func()) + result = func() + if hasattr(result, '__await__'): + result = await result + future.set_result(result) except Exception as ex: future.set_exception(ex) loop.call_soon_threadsafe(asyncio.ensure_future, func_async()) return future.result() +class TxStream(): + """Python wrapper for libfibre's LibFibreTxStream interface""" + + def __init__(self, libfibre, tx_stream_handle): + self._libfibre = libfibre + self._tx_stream_handle = tx_stream_handle + self._future = None + self._tx_buf = None + self._c_on_tx_completed = OnTxCompletedSignature(self._on_tx_completed) + self.is_closed = False + + def _on_tx_completed(self, ctx, tx_stream, status, tx_end): + tx_start = cast(self._tx_buf, c_void_p).value + + n_written = tx_end - tx_start + assert(n_written <= len(self._tx_buf)) + future = self._future + self._future = None + self._tx_buf = None + + if status == kFibreClosed: + self.is_closed = True + + if status == kFibreOk or status == kFibreClosed: + future.set_result(n_written) + else: + future.set_exception(_get_exception(status)) + + def write(self, data): + """ + Writes the provided data to the stream. Not all bytes are guaranteed to + be written. The caller should check the return value to determine the + actual number of bytes written. + + If a non-empty buffer is provided, this function will either write at + least one byte to the output, set is_closed to True or throw an + Exception (through the future). + + Currently only one write call may be active at a time (this may change + in the future). + + Returns: A future that completes with the number of bytes actually + written or an Exception. + """ + assert(self._future is None) + self._future = future = self._libfibre.loop.create_future() + self._tx_buf = data # Retain a reference to the buffer to prevent it from being garbage collected + + libfibre_start_tx(self._tx_stream_handle, + cast(self._tx_buf, c_char_p), len(self._tx_buf), + self._c_on_tx_completed, None) + + return future + + async def write_all(self, data): + """ + Writes all of the provided data to the stream or completes with an + Exception. + + If an empty buffer is provided, the underlying stream's write function + is still called at least once. + + Returns: A future that either completes with an empty result or with + an Exception. + """ + + while True: + n_written = await self.write(data) + data = data[n_written:] + if len(data) == 0: + break + elif self.is_closed: + raise EOFError("the TX stream was closed but there are still {} bytes left to send".format(len(data))) + assert(n_written > 0) # Ensure progress + +class RxStream(): + """Python wrapper for libfibre's LibFibreRxStream interface""" + + def __init__(self, libfibre, rx_stream_handle): + self._libfibre = libfibre + self._rx_stream_handle = rx_stream_handle + self._future = None + self._rx_buf = None + self._c_on_rx_completed = OnRxCompletedSignature(self._on_rx_completed) + self.is_closed = False + + def _on_rx_completed(self, ctx, rx_stream, status, rx_end): + rx_start = cast(self._rx_buf, c_void_p).value + + n_read = rx_end - rx_start + assert(n_read <= len(self._rx_buf)) + data = self._rx_buf[:n_read] + future = self._future + self._future = None + self._rx_buf = None + + if status == kFibreClosed: + self.is_closed = True + + if status == kFibreOk or status == kFibreClosed: + future.set_result(data) + else: + future.set_exception(_get_exception(status)) + + def read(self, n_read): + """ + Reads up to the specified number of bytes from the stream. + + If more than zero bytes are requested, this function will either read at + least one byte, set is_closed to True or throw an Exception (through the + future). + + Currently only one write call may be active at a time (this may change + in the future). + + Returns: A future that either completes with a buffer containing the + bytes that were read or completes with an Exception. + """ + assert(self._future is None) + self._future = future = self._libfibre.loop.create_future() + self._rx_buf = bytes(n_read) + + libfibre_start_rx(self._rx_stream_handle, + cast(self._rx_buf, c_char_p), len(self._rx_buf), + self._c_on_rx_completed, None) + + return future + + async def read_all(self, n_read): + """ + Reads the specified number of bytes from the stream or throws an + Exception. + + If zero bytes are requested, the underlying stream's read function + is still called at least once. + + Returns: A future that either completes with a buffer of size n_read or + an Exception. + """ + + data = bytes() + while True: + chunk = await self.read(n_read - len(data)) + data += chunk + if n_read == len(data): + break + elif self.is_closed: + raise EOFError() + assert(len(chunk) > 0) # Ensure progress + return data + + +class Call(object): + """ + This call behaves as you would expect an async generator to behave. This is + used to provide compatibility down to Python 3.5. + """ + def __init__(self, func): + self._func = func + self._call_handle = c_void_p(0) + self._is_started = False + self._should_close = False + self._is_closed = False + self._tx_buf = None + + def __aiter__(self): + return self + + async def asend(self, val): + assert(self._is_started == (not val is None)) + if not val is None: + self._tx_buf, self._rx_len, self._should_close = val + return await self.__anext__() + + async def __anext__(self): + if not self._is_started: + self._is_started = True + return None # This immitates the weird starting behavior of Python 3.6+ async generators iterators + + if self._is_closed: + raise StopAsyncIteration + + tx_end = c_void_p(0) + rx_end = c_void_p(0) + + rx_buf = b'\0' * self._rx_len + + call_id = insert_with_new_id(self._func._libfibre._calls, self) + + status = libfibre_call(self._func._func_handle, byref(self._call_handle), + kFibreClosed if self._should_close else kFibreOk, + cast(self._tx_buf, c_char_p), len(self._tx_buf), + cast(rx_buf, c_char_p), len(rx_buf), + byref(tx_end), byref(rx_end), self._func._libfibre.c_on_call_completed, call_id) + + if status == kFibreBusy: + self.ag_await = self._func._libfibre.loop.create_future() + status, tx_end, rx_end = await self.ag_await + self.ag_await = None + + if status != kFibreOk and status != kFibreClosed: + raise _get_exception(status) + + n_written = tx_end - cast(self._tx_buf, c_void_p).value + self._tx_buf = self._tx_buf[n_written:] + n_read = rx_end - cast(rx_buf, c_void_p).value + rx_buf = rx_buf[:n_read] + + if status != kFibreOk: + self._is_closed = True + return self._tx_buf, rx_buf, self._is_closed + + async def cancel(): + # TODO: this doesn't follow the official Python async generator protocol. Should implement aclose() instead. + status = libfibre_call(self._func._func_handle, byref(self._call_handle), kFibreOk, + 0, 0, 0, 0, 0, 0, self._func._libfibre.c_on_call_completed, call_id) + + #async def aclose(self): + # assert(self._is_started and not self._is_closed) + # return self._tx_buf, rx_buf, self._is_closed + + class RemoteFunction(object): """ Represents a callable function that maps to a function call on a remote object. @@ -224,33 +511,50 @@ class RemoteFunction(object): self._inputs = inputs self._outputs = outputs self._rx_size = sum(codec.get_length() for _, _, codec in self._outputs) - self._calls = {} - self._c_on_completed = OnCallCompletedSignature(self._on_completed) - def _on_completed(self, ctx, status, end_ptr): - call = self._calls.pop(ctx) + async def async_call(self, args, cancellation_token): + #print("making call on " + hex(args[0]._obj_handle)) + tx_buf = bytes() + for i, arg in enumerate(self._inputs): + tx_buf += arg[2].serialize(self._libfibre, args[i]) + rx_buf = bytes() - if status != kFibreOk: - call['future'].set_exception(_get_exception(status)) + agen = Call(self) + + if not cancellation_token is None: + cancellation_token.add_done_callback(agen.cancel) + + try: + assert(await agen.asend(None) is None) + + is_closed = False + while not is_closed: + tx_buf, rx_chunk, is_closed = await agen.asend((tx_buf, self._rx_size - len(rx_buf), True)) + rx_buf += rx_chunk + + finally: + if not cancellation_token is None: + cancellation_token.remove_done_callback(agen.cancel) + + assert(len(rx_buf) == self._rx_size) + + outputs = [] + for arg in self._outputs: + arg_length = arg[2].get_length() + outputs.append(arg[2].deserialize(self._libfibre, rx_buf[:arg_length])) + rx_buf = rx_buf[arg_length:] + + if len(outputs) == 0: + return + elif len(outputs) == 1: + return outputs[0] else: - pos = 0 - outputs = [] + return tuple(outputs) - for arg in self._outputs: - arg_length = arg[2].get_length() - outputs.append(arg[2].deserialize(self._libfibre, call['rx_buf'][pos:(pos + arg_length)])) - pos += arg_length - - if len(outputs) == 0: - call['future'].set_result(None) - elif len(outputs) == 1: - call['future'].set_result(outputs[0]) - else: - call['future'].set_result(tuple(outputs)) - - def __call__(self, instance, *args): + def __call__(self, *args, cancellation_token = None): """ - Starts invoking the function on the remote object. + Starts invoking the remote function. The first argument is usually a + remote object. If this function is called from the Fibre thread then it is nonblocking and returns an asyncio.Future. If it is called from another thread then it blocks until the function completes and returns the result(s) of the @@ -258,28 +562,13 @@ class RemoteFunction(object): """ if threading.current_thread() != libfibre_thread: - return run_coroutine_threadsafe(instance._libfibre.loop, lambda: self.__call__(instance, *args)) + return run_coroutine_threadsafe(self._libfibre.loop, lambda: self.__call__(*args)) if (len(self._inputs) != len(args)): raise TypeError("expected {} arguments but have {}".format(len(self._inputs), len(args))) - # All of these variables need to be protected from the garbage collector - # for the duration of the call. - call = { - 'handle': c_size_t(0), - 'tx_buf': b''.join(self._inputs[i][2].serialize(self._libfibre, args[i]) - for i in range(len(self._inputs))), # Assemble TX buffer - 'rx_buf': b'\0' * self._rx_size, # Allocate RX buffer - 'future': instance._libfibre.loop.create_future(), - } - call_id = insert_with_new_id(self._calls, call) - - libfibre_start_call(instance._obj_handle, self._func_handle, - cast(call['tx_buf'], c_char_p), len(call['tx_buf']), - cast(call['rx_buf'], c_char_p), len(call['rx_buf']), - byref(call['handle']), self._c_on_completed, call_id) - - return call['future'] + coro = self.async_call(args, cancellation_token) + return asyncio.ensure_future(coro, loop=self._libfibre.loop) def __get__(self, instance, owner): return MethodType(self, instance) if instance else self @@ -416,36 +705,41 @@ class LibFibre(): self.c_deregister_event = DeregisterEventSignature(self._deregister_event) self.c_call_later = CallLaterSignature(self._call_later) self.c_cancel_timer = CancelTimerSignature(self._cancel_timer) - self.c_construct_object = ConstructObjectSignature(self._construct_object) - self.c_destroy_object = DestroyObjectSignature(self._destroy_object) self.c_on_found_object = OnFoundObjectSignature(self._on_found_object) + self.c_on_lost_object = OnLostObjectSignature(self._on_lost_object) self.c_on_discovery_stopped = OnStoppedSignature(self._on_discovery_stopped) self.c_on_attribute_added = OnAttributeAddedSignature(self._on_attribute_added) self.c_on_attribute_removed = OnAttributeRemovedSignature(self._on_attribute_removed) self.c_on_function_added = OnFunctionAddedSignature(self._on_function_added) self.c_on_function_removed = OnFunctionRemovedSignature(self._on_function_removed) + self.c_on_call_completed = OnCallCompletedSignature(self._on_call_completed) self.timer_map = {} self.eventfd_map = {} self.interfaces = {} # key: libfibre handle, value: python class self.discovery_processes = {} # key: ID, value: python dict - self._objects = {} # key: libfibre handle, value: pyhton class + self._objects = {} # key: libfibre handle, value: python class + self._calls = {} # key: libfibre handle, value: Call object - self.ctx = c_void_p(libfibre_open( - self.c_post, - self.c_register_event, self.c_deregister_event, - self.c_call_later, self.c_cancel_timer, - self.c_construct_object, self.c_destroy_object, None)) + event_loop = LibFibreEventLoop() + event_loop.post = self.c_post + event_loop.register_event = self.c_register_event + event_loop.deregister_event = self.c_deregister_event + event_loop.call_later = self.c_call_later + event_loop.cancel_timer = self.c_cancel_timer + + self.ctx = c_void_p(libfibre_open(event_loop)) def _post(self, callback, ctx): self.loop.call_soon_threadsafe(callback, ctx) + return 0 def _register_event(self, event_fd, events, callback, ctx): self.eventfd_map[event_fd] = events if (events & 1): - self.loop.add_reader(event_fd, callback, ctx) + self.loop.add_reader(event_fd, lambda x: callback(x, 1), ctx) if (events & 4): - self.loop.add_writer(event_fd, callback, ctx) + self.loop.add_writer(event_fd, lambda x: callback(x, 4), ctx) if (events & 0xfffffffa): raise Exception("unsupported event mask " + str(events)) return 0 @@ -484,22 +778,45 @@ class LibFibre(): libfibre_subscribe_to_interface(intf_handle, self.c_on_attribute_added, self.c_on_attribute_removed, self.c_on_function_added, self.c_on_function_removed, intf_handle) return py_intf - def _construct_object(self, ctx, obj, intf, name, name_length): - #increment_libfibre_refcount() - name = None if name is None else string_at(name, name_length).decode('utf-8') - py_intf = self._load_py_intf(name, intf) - assert(not obj in self._objects) - self._objects[obj] = py_intf(self, obj) + def _load_py_obj(self, obj_handle, intf_handle): + if not obj_handle in self._objects: + name = None # TODO: load from libfibre + py_intf = self._load_py_intf(name, intf_handle) + py_obj = py_intf(self, obj_handle) + self._objects[obj_handle] = py_obj + else: + py_obj = self._objects[obj_handle] + py_obj._refcount += 1 + return py_obj - def _destroy_object(self, ctx, obj): + def _release_py_obj(self, obj_handle): + py_obj = self._objects[obj_handle] + py_obj._refcount -= 1 + if py_obj.refcount <= 0: + self._objects.pop(obj_handle) + + #def _construct_object(self, ctx, obj, intf, name, name_length): + # #increment_lib_refcount() + # name = None if name is None else string_at(name, name_length).decode('utf-8') + # py_intf = self._load_py_intf(name, intf) + # assert(not obj in self._objects) + # self._objects[obj] = py_intf(self, obj) + + def _free_py_obj(self, ctx, obj): py_obj = self._objects.pop(obj) py_obj._destroy() #decrement_lib_refcount() - def _on_found_object(self, ctx, obj): - py_obj = self._objects[obj] - # notify the subscriber - asyncio.ensure_future(self.discovery_processes[ctx]['callback'](py_obj)) + def _on_found_object(self, ctx, obj, intf): + py_obj = self._load_py_obj(obj, intf) + discovery = self.discovery_processes[ctx] + discovery._unannounced.append(py_obj) + old_future = discovery._future + discovery._future = self.loop.create_future() + old_future.set_result(None) + + def _on_lost_object(self, ctx, obj): + self._free_py_obj(obj) def _on_discovery_stopped(self, ctx, result): print("discovery stopped") @@ -517,7 +834,7 @@ class LibFibre(): setattr(intf, "_" + name + "_property", RemoteAttribute(self, attr, subintf, subintf_name, False, False)) def _on_attribute_removed(self, ctx, attr): - print("attribute removed") + print("attribute removed") # TODO def _on_function_added(self, ctx, func, name, name_length, input_names, input_codecs, output_names, output_codecs): name = string_at(name, name_length).decode('utf-8') @@ -527,25 +844,105 @@ class LibFibre(): setattr(intf, name, RemoteFunction(self, func, inputs, outputs)) def _on_function_removed(self, ctx, func): - print("function removed") + print("function removed") # TODO - def start_discovery(self, path, on_obj_discovered, cancellation_token): + def _on_call_completed(self, ctx, status, tx_end, rx_end, tx_buf, tx_len, rx_buf, rx_len): + call = self._calls.pop(ctx) + + call.ag_await.set_result((status, tx_end, rx_end)) + + return kFibreBusy + +# def start_discovery(self, path, on_obj_discovered, cancellation_token): +# buf = path.encode('ascii') +# +# discovery = { +# 'domain_handle': c_void_p(0), +# 'handle': c_void_p(0), +# 'callback': on_obj_discovered +# } +# discovery_id = insert_with_new_id(self.discovery_processes, discovery) +# +# def stop_discovery(): +# libfibre_stop_discovery(discovery['handle']) +# print("closing domain") +# +# +# print("ok ") +# cancellation_token.subscribe(lambda: stop_discovery) +# +# discovery['domain_handle'] = libfibre_open_domain(self.ctx, buf, len(buf)) +# assert(discovery['domain_handle']) +# libfibre_start_discovery(discovery['domain_handle'], byref(discovery['handle']), self.c_on_found_object, self.c_on_lost_object, self.c_on_discovery_stopped, discovery_id) +# print("disc handle ", hex(discovery['handle'].value)) +# + +class Discovery(): + def __init__(self, domain): + self._domain = domain + self._id = 0 + self._discovery_handle = c_void_p(0) + self._unannounced = [] + self._future = domain._libfibre.loop.create_future() + + async def _next(self): + if len(self._unannounced) == 0: + await self._future + return self._unannounced.pop(0) + + def _stop(self): + self._domain._libfibre.discovery_processes.pop(self._id) + libfibre_stop_discovery(self._discovery_handle) + +class _Domain(): + def __init__(self, libfibre, handle): + self._libfibre = libfibre + self._domain_handle = handle + + def _close(self): + libfibre_close_domain(self._domain_handle) + self._domain_handle = None + #decrement_lib_refcount() + + def _start_discovery(self): + discovery = Discovery(self) + discovery._id = insert_with_new_id(self._libfibre.discovery_processes, discovery) + libfibre_start_discovery(self._domain_handle, byref(discovery._discovery_handle), self._libfibre.c_on_found_object, self._libfibre.c_on_lost_object, self._libfibre.c_on_discovery_stopped, discovery._id) + return discovery + + async def _discover_one(self): + discovery = self._start_discovery() + obj = await discovery._next() + discovery._stop() + return obj + + def discover_one(self): + return run_coroutine_threadsafe(self._libfibre.loop, self._discover_one) + + + +class Domain(): + def __init__(self, path): + increment_lib_refcount() + self._opened_domain = run_coroutine_threadsafe(libfibre.loop, lambda: Domain._open(path)) + + def _open(path): + assert(libfibre_thread == threading.current_thread()) buf = path.encode('ascii') + domain_handle = libfibre_open_domain(libfibre.ctx, buf, len(buf)) + return _Domain(libfibre, domain_handle) - discovery = { - 'handle': c_void_p(0), - 'callback': on_obj_discovered - } - discovery_id = insert_with_new_id(self.discovery_processes, discovery) - - cancellation_token.subscribe(lambda: libfibre_stop_discovery(self.ctx, discovery['handle'])) - libfibre_start_discovery(self.ctx, buf, len(buf), byref(discovery['handle']), self.c_on_found_object, self.c_on_discovery_stopped, discovery_id) - + def __enter__(self): + return self._opened_domain + def __exit__(self, type, value, traceback): + run_coroutine_threadsafe(self._opened_domain._libfibre.loop, self._opened_domain._close) + self._opened_domain = None + decrement_lib_refcount() libfibre = None -def run_event_loop(): +def _run_event_loop(): global libfibre global terminate_libfibre @@ -576,7 +973,7 @@ lock = threading.Lock() libfibre_refcount = 0 libfibre_thread = None -def increment_libfibre_refcount(): +def increment_lib_refcount(): global libfibre_refcount global libfibre_thread @@ -585,7 +982,7 @@ def increment_libfibre_refcount(): #print("inc refcount to {}".format(libfibre_refcount)) if libfibre_refcount == 1: - libfibre_thread = threading.Thread(target = run_event_loop) + libfibre_thread = threading.Thread(target = _run_event_loop) libfibre_thread.start() while libfibre is None: @@ -610,64 +1007,35 @@ def decrement_lib_refcount(): libfibre_thread = None -def find_all(path, serial_number, - on_object_discovered, - search_cancellation_token, - channel_termination_token, - logger): - """ - Starts scanning for Fibre objects that match the specified path spec and calls - the callback for each Fibre object that is found. - - This function is non-blocking and thread-safe. - """ - - async def on_object_discovered_filter(obj): - increment_libfibre_refcount() - channel_termination_token.subscribe(lambda: decrement_lib_refcount()) - if serial_number is None or (await fibre.utils.get_serial_number_str(obj)) == serial_number: - result = on_object_discovered(obj) - if not result is None: - await result - - increment_libfibre_refcount() - search_cancellation_token.subscribe(lambda: decrement_lib_refcount()) - - libfibre.loop.call_soon_threadsafe(lambda: libfibre.start_discovery( - path, - on_object_discovered_filter, - search_cancellation_token)) - -def find_any(path="usb", serial_number=None, - search_cancellation_token=None, channel_termination_token=None, - timeout=None, logger=Logger(verbose=False), find_multiple=False): - """ - Blocks until the first matching Fibre object is connected and then returns that object - """ - result = [] - done_signal = Event(search_cancellation_token) - def did_discover_object(obj): - result.append(obj) - if find_multiple: - if len(result) >= int(find_multiple): - done_signal.set() - else: - done_signal.set() - - find_all(path, serial_number, did_discover_object, done_signal, channel_termination_token, logger) - - try: - done_signal.wait(timeout=timeout) - except TimeoutError: - if not find_multiple: - return None - finally: - done_signal.set() # terminate find_all - - if find_multiple: - return result - else: - return result[0] if len(result) > 0 else None +#def start_discovery(path, obj_filter, +# on_object_discovered, +# search_cancellation_token, +# channel_termination_token): +# """ +# Starts scanning for Fibre objects that match the specified path spec and calls +# the callback for each Fibre object that is found. +# +# This function is non-blocking and thread-safe. +# """ +# +# async def on_object_discovered_filter(obj): +# increment_lib_refcount() +# if not channel_termination_token is None: +# channel_termination_token.subscribe(lambda: decrement_lib_refcount()) +# if await obj_filter(obj): +# result = on_object_discovered(obj) +# if not result is None: +# await result +# elif not channel_termination_token is None: +# channel_termination_token.set() +# +# increment_lib_refcount() +# search_cancellation_token.subscribe(lambda: decrement_lib_refcount()) +# +# libfibre.loop.call_soon_threadsafe(lambda: libfibre.start_discovery( +# path, +# on_object_discovered_filter, +# search_cancellation_token)) def get_user_name(obj): diff --git a/fibre/libwinpthread-1.dll b/fibre/libwinpthread-1.dll new file mode 100755 index 0000000000000000000000000000000000000000..cc8ca20e71aed3486697afdb3f10629803016256 GIT binary patch literal 61576 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 QDZ=^RAF|E}9=;*+KPfr7UH||9 literal 0 HcmV?d00001 diff --git a/fibre/shell.py b/fibre/shell.py index 6fbfdf1f..31ba99a5 100644 --- a/fibre/shell.py +++ b/fibre/shell.py @@ -58,6 +58,7 @@ def get_user_name(interactive_variables, obj): return "anonymous_remote_object_" + str(self._obj_handle) def launch_shell(args, + object_filter, interactive_variables, print_banner, print_help, logger, app_shutdown_token, @@ -77,7 +78,7 @@ def launch_shell(args, # Connect to device logger.debug("Waiting for {}...".format(branding_long)) - fibre.find_all(args.path, args.serial_number, + fibre.start_discovery(args.path, object_filter, lambda dev: did_discover_device(dev, interactive_variables, discovered_devices, branding_short, branding_long, logger, app_shutdown_token), app_shutdown_token, app_shutdown_token, diff --git a/fibre/utils.py b/fibre/utils.py index 4a6f8965..2893bd10 100644 --- a/fibre/utils.py +++ b/fibre/utils.py @@ -23,12 +23,6 @@ if sys.version_info < (3, 3): else: TimeoutError = TimeoutError -def get_serial_number_str(device): - if hasattr(device, 'serial_number'): - return format(device.serial_number, 'x').upper() - else: - return "[unknown serial number]" - ## Threading utils ## class Event(): """