odrive 0.6.9.dev0__py37-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. odrive/__init__.py +142 -0
  2. odrive/api_client.py +54 -0
  3. odrive/config.py +1296 -0
  4. odrive/crypto.py +369 -0
  5. odrive/data/brakeRs.json +18 -0
  6. odrive/data/drvs.json +82 -0
  7. odrive/data/encoders.json +86 -0
  8. odrive/data/motors.json +128 -0
  9. odrive/data/odrive-micro-x1.json +48 -0
  10. odrive/data/odrive-micro-x3.json +40 -0
  11. odrive/data/odrive-micro-x4.json +40 -0
  12. odrive/data/odrive-s1-x4.json +52 -0
  13. odrive/data/odrive-v4.4.json +58 -0
  14. odrive/data/schema-brakeR.json +6 -0
  15. odrive/data/schema-drv.json +6 -0
  16. odrive/data/schema-encoders.json +6 -0
  17. odrive/data/schema-motors.json +6 -0
  18. odrive/data/schema-odrive.json +5 -0
  19. odrive/data/schema.json +244 -0
  20. odrive/database.py +184 -0
  21. odrive/dfu.py +195 -0
  22. odrive/dfuse/DfuDevice.py +297 -0
  23. odrive/dfuse/__init__.py +1 -0
  24. odrive/enums.py +415 -0
  25. odrive/firmware.py +102 -0
  26. odrive/hw_version.py +67 -0
  27. odrive/legacy.py +73 -0
  28. odrive/legacy_config.py +123 -0
  29. odrive/legacy_dfu.py +442 -0
  30. odrive/lib/libodrive-windows-x64.dll +0 -0
  31. odrive/libodrive.py +325 -0
  32. odrive/pyfibre/fibre/__init__.py +4 -0
  33. odrive/pyfibre/fibre/libfibre-windows-amd64.dll +0 -0
  34. odrive/pyfibre/fibre/libfibre.py +1078 -0
  35. odrive/pyfibre/fibre/libwinpthread-1.dll +0 -0
  36. odrive/pyfibre/fibre/shell.py +162 -0
  37. odrive/pyfibre/fibre/utils.py +133 -0
  38. odrive/release_api.py +184 -0
  39. odrive/rich_text.py +134 -0
  40. odrive/shell.py +144 -0
  41. odrive/utils.py +881 -0
  42. odrive/version.py +1 -0
  43. odrive-0.6.9.dev0.data/scripts/odrive_demo.py +50 -0
  44. odrive-0.6.9.dev0.data/scripts/odrivetool +263 -0
  45. odrive-0.6.9.dev0.data/scripts/odrivetool.bat +2 -0
  46. odrive-0.6.9.dev0.dist-info/METADATA +22 -0
  47. odrive-0.6.9.dev0.dist-info/RECORD +49 -0
  48. odrive-0.6.9.dev0.dist-info/WHEEL +5 -0
  49. odrive-0.6.9.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1078 @@
1
+ #!/bin/python
2
+
3
+ from ctypes import *
4
+ import asyncio
5
+ import os
6
+ from itertools import count, takewhile
7
+ import struct
8
+ from types import MethodType
9
+ import concurrent
10
+ import enum
11
+ import threading
12
+ import time
13
+ import platform
14
+ from .utils import Logger
15
+ import sys
16
+
17
+ # Enable this for better tracebacks in some cases
18
+ #import tracemalloc
19
+ #tracemalloc.start(10)
20
+
21
+ lib_names = {
22
+ ('Linux', 'x86_64'): 'libfibre-linux-amd64.so',
23
+ ('Linux', 'armv7l'): 'libfibre-linux-armhf.so',
24
+ ('Linux', 'aarch64'): 'libfibre-linux-aarch64.so',
25
+ ('Windows', 'AMD64'): 'libfibre-windows-amd64.dll',
26
+ ('Darwin', 'x86_64'): 'libfibre-macos-x86.dylib',
27
+ ('Darwin', 'arm64'): 'libfibre-macos-arm.dylib',
28
+ }
29
+
30
+ system_desc = (platform.system(), platform.machine())
31
+
32
+ if system_desc == ('Linux', 'aarch64') and sys.maxsize <= 2**32:
33
+ system_desc = ('Linux', 'armv7l') # Python running in 32-bit mode on 64-bit OS
34
+
35
+ script_dir = os.path.dirname(os.path.realpath(__file__))
36
+ fibre_cpp_paths = [
37
+ os.path.join(os.path.dirname(os.path.dirname(script_dir)), "cpp"),
38
+ os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir)))), "Firmware", "fibre-cpp")
39
+ ]
40
+
41
+ def get_first(lst, predicate, default):
42
+ for item in lst:
43
+ if predicate(item):
44
+ return item
45
+ return default
46
+
47
+ if not system_desc in lib_names:
48
+ raise ModuleNotFoundError(("libfibre is not supported on your platform ({} {}). "
49
+ "Go to https://github.com/samuelsadok/fibre-cpp/tree/devel for "
50
+ "instructions on how to compile libfibre. Once you have compiled it, "
51
+ "add it to this folder.").format(*system_desc))
52
+
53
+ lib_name = lib_names[system_desc]
54
+ search_paths = fibre_cpp_paths + [script_dir]
55
+
56
+ lib_path = get_first(
57
+ (os.path.join(p, lib_name) for p in search_paths),
58
+ os.path.isfile, None)
59
+
60
+ if lib_path is None:
61
+ raise ModuleNotFoundError("{} was not found in {}".format(lib_name, search_paths))
62
+
63
+ if os.path.getsize(lib_path) < 1000:
64
+ raise ModuleNotFoundError("{} is too small. Did you forget to init git lfs? Try this:\n"
65
+ " 1. Install git lfs (https://git-lfs.github.com/)\n"
66
+ " 2. Run `cd {}`\n"
67
+ " 3. Run `git lfs install`\n"
68
+ " 4. Run `git lfs pull`".format(lib_path, os.path.dirname(lib_path)))
69
+
70
+ if os.name == 'nt':
71
+ dll_dir = os.path.dirname(lib_path)
72
+ try:
73
+ # New way in python 3.8+
74
+ os.add_dll_directory(dll_dir)
75
+ except:
76
+ os.environ['PATH'] = dll_dir + os.pathsep + os.environ['PATH']
77
+ lib = windll.LoadLibrary(lib_path)
78
+ else:
79
+ lib = cdll.LoadLibrary(lib_path)
80
+
81
+ # libfibre definitions --------------------------------------------------------#
82
+
83
+ PostSignature = CFUNCTYPE(c_int, CFUNCTYPE(None, c_void_p), POINTER(c_int))
84
+ RegisterEventSignature = CFUNCTYPE(c_int, c_int, c_uint32, CFUNCTYPE(None, c_void_p, c_int), POINTER(c_int))
85
+ DeregisterEventSignature = CFUNCTYPE(c_int, c_int)
86
+ CallLaterSignature = CFUNCTYPE(c_void_p, c_float, CFUNCTYPE(None, c_void_p), POINTER(c_int))
87
+ CancelTimerSignature = CFUNCTYPE(c_int, c_void_p)
88
+
89
+ OnFoundObjectSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p)
90
+ OnLostObjectSignature = CFUNCTYPE(None, c_void_p, c_void_p)
91
+ OnStoppedSignature = CFUNCTYPE(None, c_void_p, c_int)
92
+
93
+ OnAttributeAddedSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_size_t, c_void_p, c_void_p, c_size_t)
94
+ OnAttributeRemovedSignature = CFUNCTYPE(None, c_void_p, c_void_p)
95
+ 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))
96
+ OnFunctionRemovedSignature = CFUNCTYPE(None, c_void_p, c_void_p)
97
+
98
+ 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))
99
+ OnTxCompletedSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_int, c_void_p)
100
+ OnRxCompletedSignature = CFUNCTYPE(None, c_void_p, c_void_p, c_int, c_void_p)
101
+
102
+ kFibreOk = 0
103
+ kFibreBusy = 1
104
+ kFibreCancelled = 2
105
+ kFibreClosed = 3
106
+ kFibreInvalidArgument = 4
107
+ kFibreInternalError = 5
108
+ kFibreProtocolError = 6
109
+ kFibreHostUnreachable = 7
110
+
111
+ class LibFibreVersion(Structure):
112
+ _fields_ = [
113
+ ("major", c_uint16),
114
+ ("minor", c_uint16),
115
+ ("patch", c_uint16),
116
+ ]
117
+
118
+ def __repr__(self):
119
+ return "{}.{}.{}".format(self.major, self.minor, self.patch)
120
+
121
+ class LibFibreEventLoop(Structure):
122
+ _fields_ = [
123
+ ("post", PostSignature),
124
+ ("register_event", RegisterEventSignature),
125
+ ("deregister_event", DeregisterEventSignature),
126
+ ("call_later", CallLaterSignature),
127
+ ("cancel_timer", CancelTimerSignature),
128
+ ]
129
+
130
+ libfibre_get_version = lib.libfibre_get_version
131
+ libfibre_get_version.argtypes = []
132
+ libfibre_get_version.restype = POINTER(LibFibreVersion)
133
+
134
+ version = libfibre_get_version().contents
135
+ if (version.major, version.minor) != (0, 1):
136
+ raise Exception("Incompatible libfibre version: {}".format(version))
137
+
138
+ libfibre_open = lib.libfibre_open
139
+ libfibre_open.argtypes = [LibFibreEventLoop]
140
+ libfibre_open.restype = c_void_p
141
+
142
+ libfibre_close = lib.libfibre_close
143
+ libfibre_close.argtypes = [c_void_p]
144
+ libfibre_close.restype = None
145
+
146
+ libfibre_open_domain = lib.libfibre_open_domain
147
+ libfibre_open_domain.argtypes = [c_void_p, c_char_p, c_size_t]
148
+ libfibre_open_domain.restype = c_void_p
149
+
150
+ libfibre_close_domain = lib.libfibre_close_domain
151
+ libfibre_close_domain.argtypes = [c_void_p]
152
+ libfibre_close_domain.restype = None
153
+
154
+ libfibre_start_discovery = lib.libfibre_start_discovery
155
+ libfibre_start_discovery.argtypes = [c_void_p, c_void_p, OnFoundObjectSignature, OnLostObjectSignature, OnStoppedSignature, c_void_p]
156
+ libfibre_start_discovery.restype = None
157
+
158
+ libfibre_stop_discovery = lib.libfibre_stop_discovery
159
+ libfibre_stop_discovery.argtypes = [c_void_p]
160
+ libfibre_stop_discovery.restype = None
161
+
162
+ libfibre_subscribe_to_interface = lib.libfibre_subscribe_to_interface
163
+ libfibre_subscribe_to_interface.argtypes = [c_void_p, OnAttributeAddedSignature, OnAttributeRemovedSignature, OnFunctionAddedSignature, OnFunctionRemovedSignature, c_void_p]
164
+ libfibre_subscribe_to_interface.restype = None
165
+
166
+ libfibre_get_attribute = lib.libfibre_get_attribute
167
+ libfibre_get_attribute.argtypes = [c_void_p, c_void_p, POINTER(c_void_p)]
168
+ libfibre_get_attribute.restype = c_int
169
+
170
+ libfibre_call = lib.libfibre_call
171
+ 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]
172
+ libfibre_call.restype = c_int
173
+
174
+ libfibre_start_tx = lib.libfibre_start_tx
175
+ libfibre_start_tx.argtypes = [c_void_p, c_char_p, c_size_t, OnTxCompletedSignature, c_void_p]
176
+ libfibre_start_tx.restype = None
177
+
178
+ libfibre_cancel_tx = lib.libfibre_cancel_tx
179
+ libfibre_cancel_tx.argtypes = [c_void_p]
180
+ libfibre_cancel_tx.restype = None
181
+
182
+ libfibre_start_rx = lib.libfibre_start_rx
183
+ libfibre_start_rx.argtypes = [c_void_p, c_char_p, c_size_t, OnRxCompletedSignature, c_void_p]
184
+ libfibre_start_rx.restype = None
185
+
186
+ libfibre_cancel_rx = lib.libfibre_cancel_rx
187
+ libfibre_cancel_rx.argtypes = [c_void_p]
188
+ libfibre_cancel_rx.restype = None
189
+
190
+
191
+ # libfibre wrapper ------------------------------------------------------------#
192
+
193
+ class ObjectLostError(Exception):
194
+ def __init__(self):
195
+ super(Exception, self).__init__("the object disappeared")
196
+
197
+ def _get_exception(status):
198
+ if status == kFibreOk:
199
+ return None
200
+ elif status == kFibreCancelled:
201
+ return asyncio.CancelledError()
202
+ elif status == kFibreClosed:
203
+ return EOFError()
204
+ elif status == kFibreInvalidArgument:
205
+ return ArgumentError()
206
+ elif status == kFibreInternalError:
207
+ return Exception("internal libfibre error")
208
+ elif status == kFibreProtocolError:
209
+ return Exception("peer misbehaving")
210
+ elif status == kFibreHostUnreachable:
211
+ return ObjectLostError()
212
+ else:
213
+ return Exception("unknown libfibre error {}".format(status))
214
+
215
+ class StructCodec():
216
+ """
217
+ Generic serializer/deserializer based on struct pack
218
+ """
219
+ def __init__(self, struct_format, target_type):
220
+ self._struct_format = struct_format
221
+ self._target_type = target_type
222
+ def get_length(self):
223
+ return struct.calcsize(self._struct_format)
224
+ def serialize(self, libfibre, value):
225
+ if self._target_type == int and isinstance(value, enum.Enum):
226
+ value = value.value
227
+ value = self._target_type(value)
228
+ return struct.pack(self._struct_format, value)
229
+ def deserialize(self, libfibre, buffer):
230
+ value = struct.unpack(self._struct_format, buffer)
231
+ value = value[0] if len(value) == 1 else value
232
+ return self._target_type(value)
233
+
234
+ class ObjectPtrCodec():
235
+ """
236
+ Serializer/deserializer for an object reference
237
+
238
+ libfibre transcodes object references internally from/to something that can
239
+ be sent over the wire and understood by the remote instance.
240
+ """
241
+ def get_length(self):
242
+ return struct.calcsize("P")
243
+ def serialize(self, libfibre, value):
244
+ if value is None:
245
+ return struct.pack("P", 0)
246
+ elif isinstance(value, RemoteObject):
247
+ assert(value._obj_handle) # Cannot serialize reference to a lost object
248
+ return struct.pack("P", value._obj_handle)
249
+ else:
250
+ raise TypeError("Expected value of type RemoteObject or None but got '{}'. An example for a RemoteObject is this expression: odrv0.axis0.controller._input_pos_property".format(type(value).__name__))
251
+ def deserialize(self, libfibre, buffer):
252
+ handle = struct.unpack("P", buffer)[0]
253
+
254
+ # TODO: this is broken: A function can return an object ref before it is known to PyFibre.
255
+ return None if handle == 0 else libfibre._objects[handle]
256
+
257
+
258
+ codecs = {
259
+ 'int8': StructCodec("<b", int),
260
+ 'uint8': StructCodec("<B", int),
261
+ 'int16': StructCodec("<h", int),
262
+ 'uint16': StructCodec("<H", int),
263
+ 'int32': StructCodec("<i", int),
264
+ 'uint32': StructCodec("<I", int),
265
+ 'int64': StructCodec("<q", int),
266
+ 'uint64': StructCodec("<Q", int),
267
+ 'bool': StructCodec("<?", bool),
268
+ 'float': StructCodec("<f", float),
269
+ 'object_ref': ObjectPtrCodec()
270
+ }
271
+
272
+ def decode_arg_list(arg_names, codec_names):
273
+ for i in count(0):
274
+ if arg_names[i] is None or codec_names[i] is None:
275
+ break
276
+ arg_name = arg_names[i].decode('utf-8')
277
+ codec_name = codec_names[i].decode('utf-8')
278
+ if not codec_name in codecs:
279
+ raise Exception("unsupported codec {}".format(codec_name))
280
+ yield arg_name, codec_name, codecs[codec_name]
281
+
282
+ def insert_with_new_id(dictionary, val):
283
+ key = next(x for x in count(1) if x not in set(dictionary.keys()))
284
+ dictionary[key] = val
285
+ return key
286
+
287
+ # Runs a function on a foreign event loop and blocks until the function is done.
288
+ def run_coroutine_threadsafe(loop, func):
289
+ future = concurrent.futures.Future()
290
+ async def func_async():
291
+ try:
292
+ result = func()
293
+ if hasattr(result, '__await__'):
294
+ result = await result
295
+ future.set_result(result)
296
+ except Exception as ex:
297
+ future.set_exception(ex)
298
+ loop.call_soon_threadsafe(asyncio.ensure_future, func_async())
299
+ return future.result()
300
+
301
+ class TxStream():
302
+ """Python wrapper for libfibre's LibFibreTxStream interface"""
303
+
304
+ def __init__(self, libfibre, tx_stream_handle):
305
+ self._libfibre = libfibre
306
+ self._tx_stream_handle = tx_stream_handle
307
+ self._future = None
308
+ self._tx_buf = None
309
+ self._c_on_tx_completed = OnTxCompletedSignature(self._on_tx_completed)
310
+ self.is_closed = False
311
+
312
+ def _on_tx_completed(self, ctx, tx_stream, status, tx_end):
313
+ tx_start = cast(self._tx_buf, c_void_p).value
314
+
315
+ n_written = tx_end - tx_start
316
+ assert(n_written <= len(self._tx_buf))
317
+ future = self._future
318
+ self._future = None
319
+ self._tx_buf = None
320
+
321
+ if status == kFibreClosed:
322
+ self.is_closed = True
323
+
324
+ if status == kFibreOk or status == kFibreClosed:
325
+ future.set_result(n_written)
326
+ else:
327
+ future.set_exception(_get_exception(status))
328
+
329
+ def write(self, data):
330
+ """
331
+ Writes the provided data to the stream. Not all bytes are guaranteed to
332
+ be written. The caller should check the return value to determine the
333
+ actual number of bytes written.
334
+
335
+ If a non-empty buffer is provided, this function will either write at
336
+ least one byte to the output, set is_closed to True or throw an
337
+ Exception (through the future).
338
+
339
+ Currently only one write call may be active at a time (this may change
340
+ in the future).
341
+
342
+ Returns: A future that completes with the number of bytes actually
343
+ written or an Exception.
344
+ """
345
+ assert(self._future is None)
346
+ self._future = future = self._libfibre.loop.create_future()
347
+ self._tx_buf = data # Retain a reference to the buffer to prevent it from being garbage collected
348
+
349
+ libfibre_start_tx(self._tx_stream_handle,
350
+ cast(self._tx_buf, c_char_p), len(self._tx_buf),
351
+ self._c_on_tx_completed, None)
352
+
353
+ return future
354
+
355
+ async def write_all(self, data):
356
+ """
357
+ Writes all of the provided data to the stream or completes with an
358
+ Exception.
359
+
360
+ If an empty buffer is provided, the underlying stream's write function
361
+ is still called at least once.
362
+
363
+ Returns: A future that either completes with an empty result or with
364
+ an Exception.
365
+ """
366
+
367
+ while True:
368
+ n_written = await self.write(data)
369
+ data = data[n_written:]
370
+ if len(data) == 0:
371
+ break
372
+ elif self.is_closed:
373
+ raise EOFError("the TX stream was closed but there are still {} bytes left to send".format(len(data)))
374
+ assert(n_written > 0) # Ensure progress
375
+
376
+ class RxStream():
377
+ """Python wrapper for libfibre's LibFibreRxStream interface"""
378
+
379
+ def __init__(self, libfibre, rx_stream_handle):
380
+ self._libfibre = libfibre
381
+ self._rx_stream_handle = rx_stream_handle
382
+ self._future = None
383
+ self._rx_buf = None
384
+ self._c_on_rx_completed = OnRxCompletedSignature(self._on_rx_completed)
385
+ self.is_closed = False
386
+
387
+ def _on_rx_completed(self, ctx, rx_stream, status, rx_end):
388
+ rx_start = cast(self._rx_buf, c_void_p).value
389
+
390
+ n_read = rx_end - rx_start
391
+ assert(n_read <= len(self._rx_buf))
392
+ data = self._rx_buf[:n_read]
393
+ future = self._future
394
+ self._future = None
395
+ self._rx_buf = None
396
+
397
+ if status == kFibreClosed:
398
+ self.is_closed = True
399
+
400
+ if status == kFibreOk or status == kFibreClosed:
401
+ future.set_result(data)
402
+ else:
403
+ future.set_exception(_get_exception(status))
404
+
405
+ def read(self, n_read):
406
+ """
407
+ Reads up to the specified number of bytes from the stream.
408
+
409
+ If more than zero bytes are requested, this function will either read at
410
+ least one byte, set is_closed to True or throw an Exception (through the
411
+ future).
412
+
413
+ Currently only one write call may be active at a time (this may change
414
+ in the future).
415
+
416
+ Returns: A future that either completes with a buffer containing the
417
+ bytes that were read or completes with an Exception.
418
+ """
419
+ assert(self._future is None)
420
+ self._future = future = self._libfibre.loop.create_future()
421
+ self._rx_buf = bytes(n_read)
422
+
423
+ libfibre_start_rx(self._rx_stream_handle,
424
+ cast(self._rx_buf, c_char_p), len(self._rx_buf),
425
+ self._c_on_rx_completed, None)
426
+
427
+ return future
428
+
429
+ async def read_all(self, n_read):
430
+ """
431
+ Reads the specified number of bytes from the stream or throws an
432
+ Exception.
433
+
434
+ If zero bytes are requested, the underlying stream's read function
435
+ is still called at least once.
436
+
437
+ Returns: A future that either completes with a buffer of size n_read or
438
+ an Exception.
439
+ """
440
+
441
+ data = bytes()
442
+ while True:
443
+ chunk = await self.read(n_read - len(data))
444
+ data += chunk
445
+ if n_read == len(data):
446
+ break
447
+ elif self.is_closed:
448
+ raise EOFError()
449
+ assert(len(chunk) > 0) # Ensure progress
450
+ return data
451
+
452
+
453
+ class Call(object):
454
+ """
455
+ This call behaves as you would expect an async generator to behave. This is
456
+ used to provide compatibility down to Python 3.5.
457
+ """
458
+ def __init__(self, func):
459
+ self._func = func
460
+ self._call_handle = c_void_p(0)
461
+ self._is_started = False
462
+ self._should_close = False
463
+ self._is_closed = False
464
+ self._tx_buf = None
465
+
466
+ def __aiter__(self):
467
+ return self
468
+
469
+ async def asend(self, val):
470
+ assert(self._is_started == (not val is None))
471
+ if not val is None:
472
+ self._tx_buf, self._rx_len, self._should_close = val
473
+ return await self.__anext__()
474
+
475
+ async def __anext__(self):
476
+ if not self._is_started:
477
+ self._is_started = True
478
+ return None # This immitates the weird starting behavior of Python 3.6+ async generators iterators
479
+
480
+ if self._is_closed:
481
+ raise StopAsyncIteration
482
+
483
+ tx_end = c_void_p(0)
484
+ rx_end = c_void_p(0)
485
+
486
+ rx_buf = bytes(self._rx_len)
487
+
488
+ call_id = insert_with_new_id(self._func._libfibre._calls, self)
489
+
490
+ status = libfibre_call(self._func._func_handle, byref(self._call_handle),
491
+ kFibreClosed if self._should_close else kFibreOk,
492
+ cast(self._tx_buf, c_char_p), len(self._tx_buf),
493
+ cast(rx_buf, c_char_p), len(rx_buf),
494
+ byref(tx_end), byref(rx_end), self._func._libfibre.c_on_call_completed, call_id)
495
+
496
+ if status == kFibreBusy:
497
+ self.ag_await = self._func._libfibre.loop.create_future()
498
+ status, tx_end, rx_end = await self.ag_await
499
+ self.ag_await = None
500
+
501
+ if status != kFibreOk and status != kFibreClosed:
502
+ raise _get_exception(status)
503
+
504
+ n_written = tx_end - cast(self._tx_buf, c_void_p).value
505
+ self._tx_buf = self._tx_buf[n_written:]
506
+ n_read = rx_end - cast(rx_buf, c_void_p).value
507
+ rx_buf = rx_buf[:n_read]
508
+
509
+ if status != kFibreOk:
510
+ self._is_closed = True
511
+ return self._tx_buf, rx_buf, self._is_closed
512
+
513
+ async def cancel():
514
+ # TODO: this doesn't follow the official Python async generator protocol. Should implement aclose() instead.
515
+ status = libfibre_call(self._func._func_handle, byref(self._call_handle), kFibreOk,
516
+ 0, 0, 0, 0, 0, 0, self._func._libfibre.c_on_call_completed, call_id)
517
+
518
+ #async def aclose(self):
519
+ # assert(self._is_started and not self._is_closed)
520
+ # return self._tx_buf, rx_buf, self._is_closed
521
+
522
+
523
+ class RemoteFunction(object):
524
+ """
525
+ Represents a callable function that maps to a function call on a remote object.
526
+ """
527
+ def __init__(self, libfibre, func_handle, inputs, outputs):
528
+ self._libfibre = libfibre
529
+ self._func_handle = func_handle
530
+ self._inputs = inputs
531
+ self._outputs = outputs
532
+ self._rx_size = sum(codec.get_length() for _, _, codec in self._outputs)
533
+
534
+ async def async_call(self, args, cancellation_token):
535
+ #print("making call on " + hex(args[0]._obj_handle))
536
+ tx_buf = bytes()
537
+ for i, arg in enumerate(self._inputs):
538
+ tx_buf += arg[2].serialize(self._libfibre, args[i])
539
+ rx_buf = bytes()
540
+
541
+ agen = Call(self)
542
+
543
+ if not cancellation_token is None:
544
+ cancellation_token.add_done_callback(agen.cancel)
545
+
546
+ try:
547
+ assert(await agen.asend(None) is None)
548
+
549
+ is_closed = False
550
+ while not is_closed:
551
+ tx_buf, rx_chunk, is_closed = await agen.asend((tx_buf, self._rx_size - len(rx_buf), True))
552
+ rx_buf += rx_chunk
553
+
554
+ finally:
555
+ if not cancellation_token is None:
556
+ cancellation_token.remove_done_callback(agen.cancel)
557
+
558
+ assert(len(rx_buf) == self._rx_size)
559
+
560
+ outputs = []
561
+ for arg in self._outputs:
562
+ arg_length = arg[2].get_length()
563
+ outputs.append(arg[2].deserialize(self._libfibre, rx_buf[:arg_length]))
564
+ rx_buf = rx_buf[arg_length:]
565
+
566
+ if len(outputs) == 0:
567
+ return
568
+ elif len(outputs) == 1:
569
+ return outputs[0]
570
+ else:
571
+ return tuple(outputs)
572
+
573
+ def __call__(self, *args, cancellation_token = None):
574
+ """
575
+ Starts invoking the remote function. The first argument is usually a
576
+ remote object.
577
+ If this function is called from the Fibre thread then it is nonblocking
578
+ and returns an asyncio.Future. If it is called from another thread then
579
+ it blocks until the function completes and returns the result(s) of the
580
+ invokation.
581
+ """
582
+
583
+ if threading.current_thread() != libfibre_thread:
584
+ return run_coroutine_threadsafe(self._libfibre.loop, lambda: self.__call__(*args))
585
+
586
+ if (len(self._inputs) != len(args)):
587
+ raise TypeError("expected {} arguments but have {}".format(len(self._inputs), len(args)))
588
+
589
+ coro = self.async_call(args, cancellation_token)
590
+ return asyncio.ensure_future(coro, loop=self._libfibre.loop)
591
+
592
+ def __get__(self, instance, owner):
593
+ return MethodType(self, instance) if instance else self
594
+
595
+ def _dump(self, name):
596
+ print_arglist = lambda arglist: ", ".join("{}: {}".format(arg_name, codec_name) for arg_name, codec_name, codec in arglist)
597
+ return "{}({}){}".format(name,
598
+ print_arglist(self._inputs),
599
+ "" if len(self._outputs) == 0 else
600
+ " -> " + print_arglist(self._outputs) if len(self._outputs) == 1 else
601
+ " -> (" + print_arglist(self._outputs) + ")")
602
+
603
+ class RemoteAttribute(object):
604
+ def __init__(self, libfibre, attr_handle, intf_handle, intf_name, magic_getter, magic_setter):
605
+ self._libfibre = libfibre
606
+ self._attr_handle = attr_handle
607
+ self._intf_handle = intf_handle
608
+ self._intf_name = intf_name
609
+ self._magic_getter = magic_getter
610
+ self._magic_setter = magic_setter
611
+
612
+ def _get_obj(self, instance):
613
+ assert(not instance._obj_handle is None)
614
+
615
+ obj_handle = c_void_p(0)
616
+ status = libfibre_get_attribute(instance._obj_handle, self._attr_handle, byref(obj_handle))
617
+ if status != kFibreOk:
618
+ raise _get_exception(status)
619
+
620
+ obj = self._libfibre._load_py_obj(obj_handle.value, self._intf_handle)
621
+ if obj in instance._children:
622
+ self._libfibre._release_py_obj(obj_handle.value)
623
+ else:
624
+ # the object will be released when the parent is released
625
+ instance._children.add(obj)
626
+
627
+ return obj
628
+
629
+ def __get__(self, instance, owner):
630
+ if not instance:
631
+ return self
632
+
633
+ if self._magic_getter:
634
+ if threading.current_thread() == libfibre_thread:
635
+ # read() behaves asynchronously when run on the fibre thread
636
+ # which means it returns an awaitable which _must_ be awaited
637
+ # (otherwise it's a bug). However hasattr(...) internally calls
638
+ # __get__ and does not await the result. Thus the safest thing
639
+ # is to just disallow __get__ from run as an async method.
640
+ raise Exception("Cannot use magic getter on Fibre thread. Use _[prop_name]_propery.read() instead.")
641
+ return self._get_obj(instance).read()
642
+ else:
643
+ return self._get_obj(instance)
644
+
645
+ def __set__(self, instance, val):
646
+ if self._magic_setter:
647
+ return self._get_obj(instance).exchange(val)
648
+ else:
649
+ raise Exception("this attribute cannot be written to")
650
+
651
+ class EmptyInterface():
652
+ def __str__(self):
653
+ return "[lost object]"
654
+ def __repr__(self):
655
+ return self.__str__()
656
+
657
+ class RemoteObject(object):
658
+ """
659
+ Base class for interfaces of remote objects.
660
+ """
661
+ __sealed__ = False
662
+
663
+ def __init__(self, libfibre, obj_handle):
664
+ self.__class__._refcount += 1
665
+ self._refcount = 0
666
+ self._children = set()
667
+
668
+ self._libfibre = libfibre
669
+ self._obj_handle = obj_handle
670
+ self._on_lost = concurrent.futures.Future() # TODO: maybe we can do this with conc
671
+
672
+ # Ensure that assignments to undefined attributes raise an exception
673
+ self.__sealed__ = True
674
+
675
+ def __setattr__(self, key, value):
676
+ if self.__sealed__ and not key in dir(self) and not hasattr(self, key):
677
+ raise AttributeError("Attribute {} not found".format(key))
678
+ object.__setattr__(self, key, value)
679
+
680
+ #def __del__(self):
681
+ # print("unref")
682
+ # libfibre_unref_obj(self._obj_handle)
683
+
684
+ def _dump(self, indent, depth):
685
+ if self._obj_handle is None:
686
+ return "[object lost]"
687
+
688
+ try:
689
+ if depth <= 0:
690
+ return "..."
691
+ lines = []
692
+ for key in dir(self.__class__):
693
+ if key.startswith('_'):
694
+ continue
695
+ class_member = getattr(self.__class__, key)
696
+ if isinstance(class_member, RemoteFunction):
697
+ lines.append(indent + class_member._dump(key))
698
+ elif isinstance(class_member, RemoteAttribute):
699
+ val = getattr(self, key)
700
+ if isinstance(val, RemoteObject) and not class_member._magic_getter:
701
+ lines.append(indent + key + (": " if depth == 1 else ":\n") + val._dump(indent + " ", depth - 1))
702
+ else:
703
+ if isinstance(val, RemoteObject) and class_member._magic_getter:
704
+ val_str = get_user_name(val)
705
+ else:
706
+ val_str = str(val)
707
+ property_type = str(class_member._get_obj(self).__class__.read._outputs[0][1])
708
+ lines.append(indent + key + ": " + val_str + " (" + property_type + ")")
709
+ else:
710
+ lines.append(indent + key + ": " + str(type(val)))
711
+ except:
712
+ return "[failed to dump object]"
713
+
714
+ return "\n".join(lines)
715
+
716
+ def __str__(self):
717
+ return self._dump("", depth=2)
718
+
719
+ def __repr__(self):
720
+ return self.__str__()
721
+
722
+ def _destroy(self):
723
+ libfibre = self._libfibre
724
+ on_lost = self._on_lost
725
+ children = self._children
726
+
727
+ self._libfibre = None
728
+ self._obj_handle = None
729
+ self._on_lost = None
730
+ self._children = set()
731
+
732
+ for child in children:
733
+ libfibre._release_py_obj(child._obj_handle)
734
+
735
+ self.__class__._refcount -= 1
736
+ if self.__class__._refcount == 0:
737
+ libfibre.interfaces.pop(self.__class__._handle)
738
+
739
+ self.__class__ = EmptyInterface # ensure that this object has no more attributes
740
+ on_lost.set_result(True)
741
+
742
+
743
+ class LibFibre():
744
+ def __init__(self, loop):
745
+ self.loop = loop
746
+
747
+ # We must keep a reference to these function objects so they don't get
748
+ # garbage collected.
749
+ self.c_post = PostSignature(self._post)
750
+ self.c_register_event = RegisterEventSignature(self._register_event)
751
+ self.c_deregister_event = DeregisterEventSignature(self._deregister_event)
752
+ self.c_call_later = CallLaterSignature(self._call_later)
753
+ self.c_cancel_timer = CancelTimerSignature(self._cancel_timer)
754
+ self.c_on_found_object = OnFoundObjectSignature(self._on_found_object)
755
+ self.c_on_lost_object = OnLostObjectSignature(self._on_lost_object)
756
+ self.c_on_discovery_stopped = OnStoppedSignature(self._on_discovery_stopped)
757
+ self.c_on_attribute_added = OnAttributeAddedSignature(self._on_attribute_added)
758
+ self.c_on_attribute_removed = OnAttributeRemovedSignature(self._on_attribute_removed)
759
+ self.c_on_function_added = OnFunctionAddedSignature(self._on_function_added)
760
+ self.c_on_function_removed = OnFunctionRemovedSignature(self._on_function_removed)
761
+ self.c_on_call_completed = OnCallCompletedSignature(self._on_call_completed)
762
+
763
+ self.timer_map = {}
764
+ self.eventfd_map = {}
765
+ self.interfaces = {} # key: libfibre handle, value: python class
766
+ self.discovery_processes = {} # key: ID, value: python dict
767
+ self._objects = {} # key: libfibre handle, value: python class
768
+ self._calls = {} # key: libfibre handle, value: Call object
769
+
770
+ event_loop = LibFibreEventLoop()
771
+ event_loop.post = self.c_post
772
+ event_loop.register_event = self.c_register_event
773
+ event_loop.deregister_event = self.c_deregister_event
774
+ event_loop.call_later = self.c_call_later
775
+ event_loop.cancel_timer = self.c_cancel_timer
776
+
777
+ self.ctx = c_void_p(libfibre_open(event_loop))
778
+ assert(self.ctx)
779
+
780
+ def _post(self, callback, ctx):
781
+ self.loop.call_soon_threadsafe(callback, ctx)
782
+ return 0
783
+
784
+ def _register_event(self, event_fd, events, callback, ctx):
785
+ self.eventfd_map[event_fd] = events
786
+ if (events & 1):
787
+ self.loop.add_reader(event_fd, lambda x: callback(x, 1), ctx)
788
+ if (events & 4):
789
+ self.loop.add_writer(event_fd, lambda x: callback(x, 4), ctx)
790
+ if (events & 0xfffffffa):
791
+ raise Exception("unsupported event mask " + str(events))
792
+ return 0
793
+
794
+ def _deregister_event(self, event_fd):
795
+ events = self.eventfd_map.pop(event_fd)
796
+ if (events & 1):
797
+ self.loop.remove_reader(event_fd)
798
+ if (events & 4):
799
+ self.loop.remove_writer(event_fd)
800
+ return 0
801
+
802
+ def _call_later(self, delay, callback, ctx):
803
+ def cb():
804
+ self.timer_map.pop(timer_id)
805
+ callback(ctx)
806
+ timer_id = insert_with_new_id(self.timer_map, self.loop.call_later(delay, cb))
807
+ return timer_id
808
+
809
+ def _cancel_timer(self, timer_id):
810
+ self.timer_map.pop(timer_id).cancel()
811
+ return 0
812
+
813
+ def _load_py_intf(self, name, intf_handle):
814
+ """
815
+ Creates a new python type for the specified libfibre interface handle or
816
+ returns the existing python type if one was already create before.
817
+
818
+ Behind the scenes the python type will react to future events coming
819
+ from libfibre, such as functions/attributes being added/removed.
820
+ """
821
+ if intf_handle in self.interfaces:
822
+ return self.interfaces[intf_handle]
823
+ else:
824
+ if name is None:
825
+ name = "anonymous_interface_" + str(intf_handle)
826
+ py_intf = self.interfaces[intf_handle] = type(name, (RemoteObject,), {'_handle': intf_handle, '_refcount': 0})
827
+ #exit(1)
828
+ 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)
829
+ return py_intf
830
+
831
+ def _load_py_obj(self, obj_handle, intf_handle):
832
+ if not obj_handle in self._objects:
833
+ name = None # TODO: load from libfibre
834
+ py_intf = self._load_py_intf(name, intf_handle)
835
+ py_obj = py_intf(self, obj_handle)
836
+ self._objects[obj_handle] = py_obj
837
+ else:
838
+ py_obj = self._objects[obj_handle]
839
+
840
+ # Note: this refcount does not count the python references to the object
841
+ # but rather mirrors the libfibre-internal refcount of the object. This
842
+ # is so that we can destroy the Python object when libfibre releases it.
843
+ py_obj._refcount += 1
844
+ return py_obj
845
+
846
+ def _release_py_obj(self, obj_handle):
847
+ py_obj = self._objects[obj_handle]
848
+ py_obj._refcount -= 1
849
+ if py_obj._refcount <= 0:
850
+ self._objects.pop(obj_handle)
851
+ py_obj._destroy()
852
+
853
+ def _on_found_object(self, ctx, obj, intf):
854
+ py_obj = self._load_py_obj(obj, intf)
855
+ discovery = self.discovery_processes[ctx]
856
+
857
+
858
+ # TODO: this is a hack because ObjectPtrCodec is broken
859
+ def load(subobj):
860
+ for key in dir(subobj):
861
+ if not key.startswith('_'):
862
+ attr = getattr(subobj.__class__, key)
863
+ if isinstance(attr, RemoteAttribute):
864
+ load(attr._get_obj(subobj))
865
+
866
+ load(py_obj)
867
+
868
+ discovery._unannounced.append(py_obj)
869
+ old_future = discovery._future
870
+ discovery._future = self.loop.create_future()
871
+ old_future.set_result(None)
872
+
873
+ def _on_lost_object(self, ctx, obj):
874
+ assert(obj)
875
+ self._release_py_obj(obj)
876
+
877
+ def _on_discovery_stopped(self, ctx, result):
878
+ print("discovery stopped")
879
+
880
+ def _on_attribute_added(self, ctx, attr, name, name_length, subintf, subintf_name, subintf_name_length):
881
+ name = string_at(name, name_length).decode('utf-8')
882
+ subintf_name = None if subintf_name is None else string_at(subintf_name, subintf_name_length).decode('utf-8')
883
+ intf = self.interfaces[ctx]
884
+
885
+ magic_getter = not subintf_name is None and subintf_name.startswith("fibre.Property<") and subintf_name.endswith(">")
886
+ magic_setter = not subintf_name is None and subintf_name.startswith("fibre.Property<readwrite ") and subintf_name.endswith(">")
887
+
888
+ setattr(intf, name, RemoteAttribute(self, attr, subintf, subintf_name, magic_getter, magic_setter))
889
+ if magic_getter or magic_setter:
890
+ setattr(intf, "_" + name + "_property", RemoteAttribute(self, attr, subintf, subintf_name, False, False))
891
+
892
+ def _on_attribute_removed(self, ctx, attr):
893
+ print("attribute removed") # TODO
894
+
895
+ def _on_function_added(self, ctx, func, name, name_length, input_names, input_codecs, output_names, output_codecs):
896
+ name = string_at(name, name_length).decode('utf-8')
897
+ inputs = list(decode_arg_list(input_names, input_codecs))
898
+ outputs = list(decode_arg_list(output_names, output_codecs))
899
+ intf = self.interfaces[ctx]
900
+ setattr(intf, name, RemoteFunction(self, func, inputs, outputs))
901
+
902
+ def _on_function_removed(self, ctx, func):
903
+ print("function removed") # TODO
904
+
905
+ def _on_call_completed(self, ctx, status, tx_end, rx_end, tx_buf, tx_len, rx_buf, rx_len):
906
+ call = self._calls.pop(ctx)
907
+
908
+ call.ag_await.set_result((status, tx_end, rx_end))
909
+
910
+ return kFibreBusy
911
+
912
+ class Discovery():
913
+ """
914
+ All public members of this class are thread-safe.
915
+ """
916
+
917
+ def __init__(self, domain):
918
+ self._domain = domain
919
+ self._id = 0
920
+ self._discovery_handle = c_void_p(0)
921
+ self._unannounced = []
922
+ self._future = domain._libfibre.loop.create_future()
923
+
924
+ async def _next(self):
925
+ if len(self._unannounced) == 0:
926
+ await self._future
927
+ return self._unannounced.pop(0)
928
+
929
+ def _stop(self):
930
+ self._domain._libfibre.discovery_processes.pop(self._id)
931
+ libfibre_stop_discovery(self._discovery_handle)
932
+ self._future.set_exception(asyncio.CancelledError())
933
+
934
+ def stop(self):
935
+ if threading.current_thread() == libfibre_thread:
936
+ self._stop()
937
+ else:
938
+ run_coroutine_threadsafe(self._domain._libfibre.loop, self._stop)
939
+
940
+ class _Domain():
941
+ """
942
+ All public members of this class are thread-safe.
943
+ """
944
+
945
+ def __init__(self, libfibre, handle):
946
+ self._libfibre = libfibre
947
+ self._domain_handle = handle
948
+
949
+ def _close(self):
950
+ libfibre_close_domain(self._domain_handle)
951
+ self._domain_handle = None
952
+ #decrement_lib_refcount()
953
+
954
+ def _start_discovery(self):
955
+ discovery = Discovery(self)
956
+ discovery._id = insert_with_new_id(self._libfibre.discovery_processes, discovery)
957
+ 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)
958
+ return discovery
959
+
960
+ async def _discover_one(self):
961
+ discovery = self._start_discovery()
962
+ obj = await discovery._next()
963
+ discovery._stop()
964
+ return obj
965
+
966
+ def discover_one(self):
967
+ """
968
+ Blocks until exactly one object is discovered.
969
+ """
970
+ return run_coroutine_threadsafe(self._libfibre.loop, self._discover_one)
971
+
972
+ def run_discovery(self, callback):
973
+ """
974
+ Invokes `callback` for every object that is discovered. The callback is
975
+ invoked on the libfibre thread and can be an asynchronous function.
976
+ Returns a `Discovery` object on which `stop()` can be called to
977
+ terminate the discovery.
978
+ """
979
+ discovery = run_coroutine_threadsafe(self._libfibre.loop, self._start_discovery)
980
+ async def loop():
981
+ while True:
982
+ obj = await discovery._next()
983
+ await callback(obj)
984
+ self._libfibre.loop.call_soon_threadsafe(lambda: asyncio.ensure_future(loop()))
985
+ return discovery
986
+
987
+
988
+ class Domain():
989
+ def __init__(self, path):
990
+ increment_lib_refcount()
991
+ self._opened_domain = run_coroutine_threadsafe(libfibre.loop, lambda: Domain._open(path))
992
+
993
+ def _open(path):
994
+ assert(libfibre_thread == threading.current_thread())
995
+ buf = path.encode('ascii')
996
+ domain_handle = libfibre_open_domain(libfibre.ctx, buf, len(buf))
997
+ return _Domain(libfibre, domain_handle)
998
+
999
+ def __enter__(self):
1000
+ return self._opened_domain
1001
+
1002
+ def __exit__(self, type, value, traceback):
1003
+ run_coroutine_threadsafe(self._opened_domain._libfibre.loop, self._opened_domain._close)
1004
+ self._opened_domain = None
1005
+ decrement_lib_refcount()
1006
+
1007
+ libfibre = None
1008
+
1009
+ def _run_event_loop():
1010
+ global libfibre
1011
+ global terminate_libfibre
1012
+
1013
+ loop = asyncio.new_event_loop()
1014
+ asyncio.set_event_loop(loop)
1015
+
1016
+ terminate_libfibre = loop.create_future()
1017
+ libfibre = LibFibre(loop)
1018
+
1019
+ libfibre.loop.run_until_complete(terminate_libfibre)
1020
+
1021
+ libfibre_close(libfibre.ctx)
1022
+
1023
+ # Detach all objects that still exist
1024
+ # TODO: the proper way would be either of these
1025
+ # - provide a libfibre function to destroy an object on-demand which we'd
1026
+ # call before libfibre_close().
1027
+ # - have libfibre_close() report the destruction of all objects
1028
+
1029
+ while len(libfibre._objects):
1030
+ libfibre._objects.pop(list(libfibre._objects.keys())[0])._destroy()
1031
+ assert(len(libfibre.interfaces) == 0)
1032
+
1033
+ libfibre = None
1034
+
1035
+
1036
+ lock = threading.Lock()
1037
+ libfibre_refcount = 0
1038
+ libfibre_thread = None
1039
+
1040
+ def increment_lib_refcount():
1041
+ global libfibre_refcount
1042
+ global libfibre_thread
1043
+
1044
+ with lock:
1045
+ libfibre_refcount += 1
1046
+ #print("inc refcount to {}".format(libfibre_refcount))
1047
+
1048
+ if libfibre_refcount == 1:
1049
+ libfibre_thread = threading.Thread(target = _run_event_loop)
1050
+ libfibre_thread.start()
1051
+
1052
+ while libfibre is None:
1053
+ time.sleep(0.1)
1054
+
1055
+ def decrement_lib_refcount():
1056
+ global libfibre_refcount
1057
+ global libfibre_thread
1058
+
1059
+ with lock:
1060
+ #print("dec refcount from {}".format(libfibre_refcount))
1061
+ libfibre_refcount -= 1
1062
+
1063
+ if libfibre_refcount == 0:
1064
+ libfibre.loop.call_soon_threadsafe(lambda: terminate_libfibre.set_result(True))
1065
+
1066
+ # It's unlikely that releasing fibre from a fibre callback is ok. If
1067
+ # there is a valid scenario for this then we can remove the assert.
1068
+ assert(libfibre_thread != threading.current_thread())
1069
+
1070
+ libfibre_thread.join()
1071
+ libfibre_thread = None
1072
+
1073
+ def get_user_name(obj):
1074
+ """
1075
+ Can be overridden by the application to return the user-facing name of an
1076
+ object.
1077
+ """
1078
+ return "[anonymous object]"