mirror of
https://github.com/odriverobotics/ODrive.git
synced 2026-09-24 17:53:39 +08:00
Squashed 'tools/odrive/pyfibre/' content from commit f9ebfdd3
git-subtree-dir: tools/odrive/pyfibre git-subtree-split: f9ebfdd3b4c9d5b907ed145f0285b67c7f0f7eb6
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
|
||||
# Python Distribution / packaging
|
||||
.Python
|
||||
/dist/
|
||||
/*.egg-info/
|
||||
/MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
from .utils import Event, Logger, TimeoutError
|
||||
from .shell import launch_shell
|
||||
from .libfibre import find_all, find_any, ObjectLostError
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,359 @@
|
||||
# See protocol.hpp for an overview of the protocol
|
||||
|
||||
import time
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
#import fibre.utils
|
||||
from fibre.utils import Event, wait_any, TimeoutError
|
||||
|
||||
import abc
|
||||
if sys.version_info >= (3, 4):
|
||||
ABC = abc.ABC
|
||||
else:
|
||||
ABC = abc.ABCMeta('ABC', (), {})
|
||||
|
||||
if sys.version_info < (3, 3):
|
||||
from monotonic import monotonic
|
||||
time.monotonic = monotonic
|
||||
|
||||
SYNC_BYTE = 0xAA
|
||||
CRC8_INIT = 0x42
|
||||
CRC16_INIT = 0x1337
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
CRC8_DEFAULT = 0x37 # this must match the polynomial in the C++ implementation
|
||||
CRC16_DEFAULT = 0x3d65 # this must match the polynomial in the C++ implementation
|
||||
|
||||
MAX_PACKET_SIZE = 128
|
||||
|
||||
# For more information on the CRC algorithm refer to protocol.md
|
||||
|
||||
def calc_crc(remainder, value, polynomial, bitwidth):
|
||||
topbit = (1 << (bitwidth - 1))
|
||||
|
||||
# Bring the next byte into the remainder.
|
||||
remainder ^= (value << (bitwidth - 8))
|
||||
for bitnumber in range(0,8):
|
||||
if (remainder & topbit):
|
||||
remainder = (remainder << 1) ^ polynomial
|
||||
else:
|
||||
remainder = (remainder << 1)
|
||||
|
||||
return remainder & ((1 << bitwidth) - 1)
|
||||
|
||||
def calc_crc8(remainder, value):
|
||||
if isinstance(value, bytearray) or isinstance(value, bytes) or isinstance(value, list):
|
||||
for byte in value:
|
||||
if not isinstance(byte,int):
|
||||
byte = ord(byte)
|
||||
remainder = calc_crc(remainder, byte, CRC8_DEFAULT, 8)
|
||||
else:
|
||||
remainder = calc_crc(remainder, byte, CRC8_DEFAULT, 8)
|
||||
return remainder
|
||||
|
||||
def calc_crc16(remainder, value):
|
||||
if isinstance(value, bytearray) or isinstance(value, bytes) or isinstance(value, list):
|
||||
for byte in value:
|
||||
if not isinstance(byte, int):
|
||||
byte = ord(byte)
|
||||
remainder = calc_crc(remainder, byte, CRC16_DEFAULT, 16)
|
||||
else:
|
||||
remainder = calc_crc(remainder, value, CRC16_DEFAULT, 16)
|
||||
return remainder
|
||||
|
||||
|
||||
class DeviceInitException(Exception):
|
||||
pass
|
||||
|
||||
class ChannelDamagedException(Exception):
|
||||
"""
|
||||
Raised when the channel is temporarily broken and a
|
||||
resend of the message might be successful
|
||||
"""
|
||||
pass
|
||||
|
||||
class ObjectLostError(Exception):
|
||||
"""
|
||||
Raised when the channel is permanently broken
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class StreamSource(ABC):
|
||||
@abc.abstractmethod
|
||||
def get_bytes(self, n_bytes, deadline):
|
||||
pass
|
||||
|
||||
class StreamSink(ABC):
|
||||
@abc.abstractmethod
|
||||
def process_bytes(self, bytes):
|
||||
pass
|
||||
|
||||
class PacketSource(ABC):
|
||||
@abc.abstractmethod
|
||||
def get_packet(self, deadline):
|
||||
pass
|
||||
|
||||
class PacketSink(ABC):
|
||||
@abc.abstractmethod
|
||||
def process_packet(self, packet):
|
||||
pass
|
||||
|
||||
|
||||
class StreamToPacketSegmenter(StreamSink):
|
||||
def __init__(self, output):
|
||||
self._header = []
|
||||
self._packet = []
|
||||
self._packet_length = 0
|
||||
self._output = output
|
||||
|
||||
def process_bytes(self, bytes):
|
||||
"""
|
||||
Processes an arbitrary number of bytes. If one or more full packets are
|
||||
are received, they are sent to this instance's output PacketSink.
|
||||
Incomplete packets are buffered between subsequent calls to this function.
|
||||
"""
|
||||
|
||||
for byte in bytes:
|
||||
if (len(self._header) < 3):
|
||||
# Process header byte
|
||||
self._header.append(byte)
|
||||
if (len(self._header) == 1) and (self._header[0] != SYNC_BYTE):
|
||||
self._header = []
|
||||
elif (len(self._header) == 2) and (self._header[1] & 0x80):
|
||||
self._header = [] # TODO: support packets larger than 128 bytes
|
||||
elif (len(self._header) == 3) and calc_crc8(CRC8_INIT, self._header):
|
||||
self._header = []
|
||||
elif (len(self._header) == 3):
|
||||
self._packet_length = self._header[1] + 2
|
||||
else:
|
||||
# Process payload byte
|
||||
self._packet.append(byte)
|
||||
|
||||
# If both header and packet are fully received, hand it on to the packet processor
|
||||
if (len(self._header) == 3) and (len(self._packet) == self._packet_length):
|
||||
if calc_crc16(CRC16_INIT, self._packet) == 0:
|
||||
self._output.process_packet(self._packet[:-2])
|
||||
self._header = []
|
||||
self._packet = []
|
||||
self._packet_length = 0
|
||||
|
||||
|
||||
class StreamBasedPacketSink(PacketSink):
|
||||
def __init__(self, output):
|
||||
self._output = output
|
||||
|
||||
def process_packet(self, packet):
|
||||
if (len(packet) >= MAX_PACKET_SIZE):
|
||||
raise NotImplementedError("packet larger than 127 currently not supported")
|
||||
|
||||
header = bytearray()
|
||||
header.append(SYNC_BYTE)
|
||||
header.append(len(packet))
|
||||
header.append(calc_crc8(CRC8_INIT, header))
|
||||
|
||||
self._output.process_bytes(header)
|
||||
self._output.process_bytes(packet)
|
||||
|
||||
# append CRC in big endian
|
||||
crc16 = calc_crc16(CRC16_INIT, packet)
|
||||
self._output.process_bytes(struct.pack('>H', crc16))
|
||||
|
||||
class PacketFromStreamConverter(PacketSource):
|
||||
def __init__(self, input):
|
||||
self._input = input
|
||||
|
||||
def get_packet(self, deadline):
|
||||
"""
|
||||
Requests bytes from the underlying input stream until a full packet is
|
||||
received or the deadline is reached, in which case None is returned. A
|
||||
deadline before the current time corresponds to non-blocking mode.
|
||||
"""
|
||||
while True:
|
||||
header = bytes()
|
||||
|
||||
# TODO: sometimes this call hangs, even though the device apparently sent something
|
||||
header = header + self._input.get_bytes_or_fail(1, deadline)
|
||||
if (header[0] != SYNC_BYTE):
|
||||
#print("sync byte mismatch")
|
||||
continue
|
||||
|
||||
header = header + self._input.get_bytes_or_fail(1, deadline)
|
||||
if (header[1] & 0x80):
|
||||
#print("packet too large")
|
||||
continue # TODO: support packets larger than 128 bytes
|
||||
|
||||
header = header + self._input.get_bytes_or_fail(1, deadline)
|
||||
if calc_crc8(CRC8_INIT, header) != 0:
|
||||
#print("crc8 mismatch")
|
||||
continue
|
||||
|
||||
packet_length = header[1] + 2
|
||||
#print("wait for {} bytes".format(packet_length))
|
||||
packet = self._input.get_bytes_or_fail(packet_length, deadline)
|
||||
if calc_crc16(CRC16_INIT, packet) != 0:
|
||||
#print("crc16 mismatch")
|
||||
continue
|
||||
return packet[:-2]
|
||||
|
||||
|
||||
class Channel(PacketSink):
|
||||
# Choose these parameters to be sensible for a specific transport layer
|
||||
_resend_timeout = 5.0 # [s]
|
||||
_send_attempts = 5
|
||||
|
||||
def __init__(self, name, input, output, cancellation_token, logger):
|
||||
"""
|
||||
Params:
|
||||
input: A PacketSource where this channel will source packets from on
|
||||
demand. Alternatively packets can be provided to this channel
|
||||
directly by calling process_packet on this instance.
|
||||
output: A PacketSink where this channel will put outgoing packets.
|
||||
"""
|
||||
self._name = name
|
||||
self._input = input
|
||||
self._output = output
|
||||
self._logger = logger
|
||||
self._outbound_seq_no = 0
|
||||
self._interface_definition_crc = 0
|
||||
self._expected_acks = {}
|
||||
self._responses = {}
|
||||
self._my_lock = threading.Lock()
|
||||
self._channel_broken = Event(cancellation_token)
|
||||
self.start_receiver_thread(Event(self._channel_broken))
|
||||
|
||||
def start_receiver_thread(self, cancellation_token):
|
||||
"""
|
||||
Starts the receiver thread that processes incoming messages.
|
||||
The thread quits as soon as the channel enters a broken state.
|
||||
"""
|
||||
def receiver_thread():
|
||||
error_ctr = 0
|
||||
try:
|
||||
while (not cancellation_token.is_set() and not self._channel_broken.is_set()
|
||||
and error_ctr < 10):
|
||||
# Set an arbitrary deadline because the get_packet function
|
||||
# currently doesn't support a cancellation_token
|
||||
deadline = time.monotonic() + 1.0
|
||||
try:
|
||||
response = self._input.get_packet(deadline)
|
||||
except TimeoutError:
|
||||
continue # try again
|
||||
except ChannelDamagedException:
|
||||
error_ctr += 1
|
||||
continue # try again
|
||||
if (error_ctr > 0):
|
||||
error_ctr -= 1
|
||||
# Process response
|
||||
# This should not throw an exception, otherwise the channel breaks
|
||||
self.process_packet(response)
|
||||
#print("receiver thread is exiting")
|
||||
except Exception:
|
||||
self._logger.debug("receiver thread is exiting: " + traceback.format_exc())
|
||||
finally:
|
||||
self._channel_broken.set()
|
||||
t = threading.Thread(target=receiver_thread)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def remote_endpoint_operation(self, endpoint_id, input, expect_ack, output_length):
|
||||
if input is None:
|
||||
input = bytearray(0)
|
||||
if (len(input) >= 128):
|
||||
raise Exception("packet larger than 127 currently not supported")
|
||||
|
||||
if (expect_ack):
|
||||
endpoint_id |= 0x8000
|
||||
|
||||
self._my_lock.acquire()
|
||||
try:
|
||||
self._outbound_seq_no = ((self._outbound_seq_no + 1) & 0x7fff)
|
||||
seq_no = self._outbound_seq_no
|
||||
finally:
|
||||
self._my_lock.release()
|
||||
seq_no |= 0x80 # FIXME: we hardwire one bit of the seq-no to 1 to avoid conflicts with the ascii protocol
|
||||
packet = struct.pack('<HHH', seq_no, endpoint_id, output_length)
|
||||
packet = packet + input
|
||||
|
||||
crc16 = calc_crc16(CRC16_INIT, packet)
|
||||
if (endpoint_id & 0x7fff == 0):
|
||||
trailer = PROTOCOL_VERSION
|
||||
else:
|
||||
trailer = self._interface_definition_crc
|
||||
#print("append trailer " + trailer)
|
||||
packet = packet + struct.pack('<H', trailer)
|
||||
|
||||
if (expect_ack):
|
||||
ack_event = Event()
|
||||
self._expected_acks[seq_no] = ack_event
|
||||
try:
|
||||
attempt = 0
|
||||
while (attempt < self._send_attempts):
|
||||
self._my_lock.acquire()
|
||||
try:
|
||||
self._output.process_packet(packet)
|
||||
except ChannelDamagedException:
|
||||
attempt += 1
|
||||
continue # resend
|
||||
except TimeoutError:
|
||||
attempt += 1
|
||||
continue # resend
|
||||
finally:
|
||||
self._my_lock.release()
|
||||
# Wait for ACK until the resend timeout is exceeded
|
||||
try:
|
||||
if wait_any(self._resend_timeout, ack_event, self._channel_broken) != 0:
|
||||
raise ObjectLostError()
|
||||
except TimeoutError:
|
||||
attempt += 1
|
||||
continue # resend
|
||||
return self._responses.pop(seq_no)
|
||||
# TODO: record channel statistics
|
||||
raise ObjectLostError() # Too many resend attempts
|
||||
finally:
|
||||
self._expected_acks.pop(seq_no)
|
||||
self._responses.pop(seq_no, None)
|
||||
else:
|
||||
# fire and forget
|
||||
self._output.process_packet(packet)
|
||||
return None
|
||||
|
||||
def remote_endpoint_read_buffer(self, endpoint_id):
|
||||
"""
|
||||
Handles reads from long endpoints
|
||||
"""
|
||||
# TODO: handle device that could (maliciously) send infinite stream
|
||||
buffer = bytes()
|
||||
while True:
|
||||
chunk_length = 512
|
||||
chunk = self.remote_endpoint_operation(endpoint_id, struct.pack("<I", len(buffer)), True, chunk_length)
|
||||
if (len(chunk) == 0):
|
||||
break
|
||||
buffer += chunk
|
||||
return buffer
|
||||
|
||||
def process_packet(self, packet):
|
||||
#print("process packet")
|
||||
packet = bytes(packet)
|
||||
if (len(packet) < 2):
|
||||
raise Exception("packet too short")
|
||||
|
||||
seq_no = struct.unpack('<H', packet[0:2])[0]
|
||||
|
||||
if (seq_no & 0x8000):
|
||||
seq_no &= 0x7fff
|
||||
ack_signal = self._expected_acks.get(seq_no, None)
|
||||
if (ack_signal):
|
||||
self._responses[seq_no] = packet[2:]
|
||||
ack_signal.set()
|
||||
#print("received ack for packet " + str(seq_no))
|
||||
else:
|
||||
print("received unexpected ACK: " + str(seq_no))
|
||||
|
||||
else:
|
||||
#if (calc_crc16(CRC16_INIT, struct.pack('<HBB', PROTOCOL_VERSION, packet[-2], packet[-1]))):
|
||||
# raise Exception("CRC16 mismatch")
|
||||
print("endpoint requested")
|
||||
# TODO: handle local endpoint operation
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
|
||||
import sys
|
||||
import platform
|
||||
import threading
|
||||
import fibre
|
||||
|
||||
async def did_discover_device(device,
|
||||
interactive_variables, discovered_devices,
|
||||
branding_short, branding_long,
|
||||
logger, app_shutdown_token):
|
||||
"""
|
||||
Handles the discovery of new devices by displaying a
|
||||
message and making the device available to the interactive
|
||||
console
|
||||
"""
|
||||
serial_number = '{:012X}'.format(await device.serial_number) if hasattr(device, 'serial_number') else "[unknown serial number]"
|
||||
if serial_number in discovered_devices:
|
||||
verb = "Reconnected"
|
||||
index = discovered_devices.index(serial_number)
|
||||
else:
|
||||
verb = "Connected"
|
||||
discovered_devices.append(serial_number)
|
||||
index = len(discovered_devices) - 1
|
||||
interactive_name = branding_short + str(index)
|
||||
|
||||
# Publish new device to interactive console
|
||||
interactive_variables[interactive_name] = device
|
||||
globals()[interactive_name] = device # Add to globals so tab complete works
|
||||
logger.notify("{} to {} {} as {}".format(verb, branding_long, serial_number, interactive_name))
|
||||
|
||||
# Subscribe to disappearance of the device
|
||||
device._on_lost.add_done_callback(lambda x: did_lose_device(interactive_name, logger, app_shutdown_token))
|
||||
|
||||
def did_lose_device(interactive_name, logger, app_shutdown_token):
|
||||
"""
|
||||
Handles the disappearance of a device by displaying
|
||||
a message.
|
||||
"""
|
||||
if not app_shutdown_token.is_set():
|
||||
logger.warn("Oh no {} disappeared".format(interactive_name))
|
||||
|
||||
|
||||
def get_user_name(interactive_variables, obj):
|
||||
queue = [(k, v) for k, v in interactive_variables.items() if isinstance(v, fibre.libfibre.RemoteObject)]
|
||||
|
||||
if not isinstance(obj, fibre.libfibre.RemoteObject):
|
||||
return None
|
||||
|
||||
while len(queue):
|
||||
k, v = queue.pop(0)
|
||||
if v == obj:
|
||||
return k
|
||||
for key in dir(v.__class__):
|
||||
class_member = getattr(v.__class__, key)
|
||||
if not key.startswith('_') and isinstance(class_member, fibre.libfibre.RemoteAttribute):
|
||||
queue.append((k + "." + (key if not class_member._magic_getter else "_" + key + "_property"), class_member._get_obj(v)))
|
||||
|
||||
return "anonymous_remote_object_" + str(self._obj_handle)
|
||||
|
||||
def launch_shell(args,
|
||||
interactive_variables,
|
||||
print_banner, print_help,
|
||||
logger, app_shutdown_token,
|
||||
branding_short="dev", branding_long="device"):
|
||||
"""
|
||||
Launches an interactive python or IPython command line
|
||||
interface.
|
||||
As devices are connected they are made available as
|
||||
"dev0", "dev1", ...
|
||||
The names of the variables can be customized by setting branding_short.
|
||||
"""
|
||||
|
||||
discovered_devices = []
|
||||
globals().update(interactive_variables)
|
||||
|
||||
fibre.libfibre.get_user_name = lambda obj: get_user_name(interactive_variables, obj)
|
||||
|
||||
# Connect to device
|
||||
logger.debug("Waiting for {}...".format(branding_long))
|
||||
fibre.find_all(args.path, args.serial_number,
|
||||
lambda dev: did_discover_device(dev, interactive_variables, discovered_devices, branding_short, branding_long, logger, app_shutdown_token),
|
||||
app_shutdown_token,
|
||||
app_shutdown_token,
|
||||
logger=logger)
|
||||
|
||||
# Check if IPython is installed
|
||||
if args.no_ipython:
|
||||
use_ipython = False
|
||||
else:
|
||||
try:
|
||||
import IPython
|
||||
use_ipython = True
|
||||
except:
|
||||
print("Warning: you don't have IPython installed.")
|
||||
print("If you want to have an improved interactive console with pretty colors,")
|
||||
print("you should install IPython\n")
|
||||
use_ipython = False
|
||||
|
||||
interactive_variables["help"] = lambda: print_help(args, len(discovered_devices) > 0)
|
||||
|
||||
# If IPython is installed, embed IPython shell, otherwise embed regular shell
|
||||
if use_ipython:
|
||||
# Override help function # pylint: disable=W0612
|
||||
help = lambda: print_help(args, len(discovered_devices) > 0)
|
||||
# to fix broken "%run -i script.py"
|
||||
locals()['__name__'] = globals()['__name__']
|
||||
console = IPython.terminal.embed.InteractiveShellEmbed(banner1='')
|
||||
|
||||
# hack to make IPython look like the regular console
|
||||
console.runcode = console.run_cell
|
||||
interact = console
|
||||
|
||||
# Catch ObjectLostError (since disconnect is not always an error)
|
||||
default_exception_hook = console._showtraceback
|
||||
def filtered_exception_hook(ex_class, ex, trace):
|
||||
if(ex_class.__module__+'.'+ex_class.__name__ != 'fibre.libfibre.ObjectLostError'):
|
||||
default_exception_hook(ex_class,ex,trace)
|
||||
|
||||
console._showtraceback = filtered_exception_hook
|
||||
else:
|
||||
# Enable tab complete if possible
|
||||
try:
|
||||
import readline # Works only on Unix
|
||||
readline.parse_and_bind("tab: complete")
|
||||
except:
|
||||
sudo_prefix = "" if platform.system() == "Windows" else "sudo "
|
||||
print("Warning: could not enable tab-complete. User experience will suffer.\n"
|
||||
"Run `{}pip install readline` and then restart this script to fix this."
|
||||
.format(sudo_prefix))
|
||||
|
||||
import code
|
||||
console = code.InteractiveConsole(locals=interactive_variables)
|
||||
interact = lambda: console.interact(banner='')
|
||||
|
||||
# Catch ObjectLostError (since disconnect is not alway an error)
|
||||
console.runcode("import sys")
|
||||
console.runcode("default_exception_hook = sys.excepthook")
|
||||
console.runcode("def filtered_exception_hook(ex_class, ex, trace):\n"
|
||||
" if ex_class.__module__ + '.' + ex_class.__name__ != 'fibre.libfibre.ObjectLostError':\n"
|
||||
" default_exception_hook(ex_class,ex,trace)")
|
||||
console.runcode("sys.excepthook=filtered_exception_hook")
|
||||
|
||||
|
||||
# Launch shell
|
||||
print_banner()
|
||||
logger._skip_bottom_line = True
|
||||
interact()
|
||||
app_shutdown_token.set()
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import platform
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
try:
|
||||
if platform.system() == 'Windows':
|
||||
import win32console
|
||||
# TODO: we should win32console anyway so we could just omit colorama
|
||||
import colorama
|
||||
colorama.init()
|
||||
except ImportError:
|
||||
print("Could not init terminal features.")
|
||||
sys.stdout.flush()
|
||||
pass
|
||||
|
||||
if sys.version_info < (3, 3):
|
||||
class TimeoutError(Exception):
|
||||
pass
|
||||
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():
|
||||
"""
|
||||
Alternative to threading.Event(), enhanced by the subscribe() function
|
||||
that the original fails to provide.
|
||||
@param Trigger: if supplied, the newly created event will be triggered
|
||||
as soon as the trigger event becomes set
|
||||
"""
|
||||
def __init__(self, trigger=None):
|
||||
self._evt = threading.Event()
|
||||
self._subscribers = []
|
||||
self._mutex = threading.Lock()
|
||||
if not trigger is None:
|
||||
trigger.subscribe(lambda: self.set())
|
||||
|
||||
def is_set(self):
|
||||
return self._evt.is_set()
|
||||
|
||||
def set(self):
|
||||
"""
|
||||
Sets the event and invokes all subscribers if the event was
|
||||
not already set
|
||||
"""
|
||||
self._mutex.acquire()
|
||||
try:
|
||||
if not self._evt.is_set():
|
||||
self._evt.set()
|
||||
for s in self._subscribers:
|
||||
s()
|
||||
finally:
|
||||
self._mutex.release()
|
||||
|
||||
def subscribe(self, handler):
|
||||
"""
|
||||
Invokes the specified handler exactly once as soon as the
|
||||
specified event is set. If the event is already set, the
|
||||
handler is invoked immediately.
|
||||
The subscribers are called in the reverse order in which they subscribed.
|
||||
Returns a function that can be invoked to unsubscribe.
|
||||
"""
|
||||
if handler is None:
|
||||
raise TypeError
|
||||
self._mutex.acquire()
|
||||
try:
|
||||
self._subscribers.insert(0, handler)
|
||||
if self._evt.is_set():
|
||||
handler()
|
||||
finally:
|
||||
self._mutex.release()
|
||||
return handler
|
||||
|
||||
def unsubscribe(self, handler):
|
||||
self._mutex.acquire()
|
||||
try:
|
||||
self._subscribers.pop(self._subscribers.index(handler))
|
||||
finally:
|
||||
self._mutex.release()
|
||||
|
||||
def wait(self, timeout=None):
|
||||
if not self._evt.wait(timeout=timeout):
|
||||
raise TimeoutError()
|
||||
|
||||
def trigger_after(self, timeout):
|
||||
"""
|
||||
Triggers the event after the specified timeout.
|
||||
This function returns immediately.
|
||||
"""
|
||||
def delayed_trigger():
|
||||
if not self.wait(timeout=timeout):
|
||||
self.set()
|
||||
threading.Thread(target=delayed_trigger)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
def wait_any(timeout=None, *events):
|
||||
"""
|
||||
Blocks until any of the specified events are triggered.
|
||||
Returns the index of the event that was triggerd or raises
|
||||
a TimeoutError
|
||||
Param timeout: A timeout in seconds
|
||||
"""
|
||||
or_event = threading.Event()
|
||||
subscriptions = []
|
||||
for event in events:
|
||||
subscriptions.append((event, event.subscribe(lambda: or_event.set())))
|
||||
or_event.wait(timeout=timeout)
|
||||
for event, sub in subscriptions:
|
||||
event.unsubscribe(sub)
|
||||
for i in range(len(events)):
|
||||
if events[i].is_set():
|
||||
return i
|
||||
raise TimeoutError()
|
||||
|
||||
|
||||
## Log utils ##
|
||||
|
||||
class Logger():
|
||||
"""
|
||||
Logs messages to stdout
|
||||
"""
|
||||
|
||||
COLOR_DEFAULT = 0
|
||||
COLOR_GREEN = 1
|
||||
COLOR_CYAN = 2
|
||||
COLOR_YELLOW = 3
|
||||
COLOR_RED = 4
|
||||
|
||||
_VT100Colors = {
|
||||
COLOR_GREEN: '\x1b[92;1m',
|
||||
COLOR_CYAN: '\x1b[96;1m',
|
||||
COLOR_YELLOW: '\x1b[93;1m',
|
||||
COLOR_RED: '\x1b[91;1m',
|
||||
COLOR_DEFAULT: '\x1b[0m'
|
||||
}
|
||||
|
||||
_Win32Colors = {
|
||||
COLOR_GREEN: 0x0A,
|
||||
COLOR_CYAN: 0x0B,
|
||||
COLOR_YELLOW: 0x0E,
|
||||
COLOR_RED: 0x0C,
|
||||
COLOR_DEFAULT: 0x07
|
||||
}
|
||||
|
||||
def __init__(self, verbose=True):
|
||||
self._prefix = ''
|
||||
self._skip_bottom_line = False # If true, messages are printed one line above the cursor
|
||||
self._verbose = verbose
|
||||
self._print_lock = threading.Lock()
|
||||
if platform.system() == 'Windows':
|
||||
self._stdout_buf = win32console.GetStdHandle(win32console.STD_OUTPUT_HANDLE)
|
||||
|
||||
def indent(self, prefix=' '):
|
||||
indented_logger = Logger()
|
||||
indented_logger._prefix = self._prefix + prefix
|
||||
return indented_logger
|
||||
|
||||
def print_on_second_last_line(self, text, color):
|
||||
"""
|
||||
Prints a text on the second last line.
|
||||
This can be used to print a message above the command
|
||||
prompt. If the command prompt spans multiple lines
|
||||
there will be glitches.
|
||||
If the printed text spans multiple lines there will also
|
||||
be glitches (though this could be fixed).
|
||||
"""
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
# Windows <10 doesn't understand VT100 escape codes and the colorama
|
||||
# also doesn't support the specific escape codes we need so we use the
|
||||
# native Win32 API.
|
||||
info = self._stdout_buf.GetConsoleScreenBufferInfo()
|
||||
cursor_pos = info['CursorPosition']
|
||||
scroll_rect=win32console.PySMALL_RECTType(
|
||||
Left=0, Top=1,
|
||||
Right=info['Window'].Right,
|
||||
Bottom=cursor_pos.Y-1)
|
||||
scroll_dest = win32console.PyCOORDType(scroll_rect.Left, scroll_rect.Top-1)
|
||||
self._stdout_buf.ScrollConsoleScreenBuffer(
|
||||
scroll_rect, scroll_rect, scroll_dest, # clipping rect is same as scroll rect
|
||||
u' ', Logger._Win32Colors[color]) # fill with empty cells with the desired color attributes
|
||||
line_start = win32console.PyCOORDType(0, cursor_pos.Y-1)
|
||||
self._stdout_buf.WriteConsoleOutputCharacter(text, line_start)
|
||||
|
||||
else:
|
||||
# Assume we're in a terminal that interprets VT100 escape codes.
|
||||
# TODO: test on macOS
|
||||
|
||||
# Escape character sequence:
|
||||
# ESC 7: store cursor position
|
||||
# ESC 1A: move cursor up by one
|
||||
# ESC 1S: scroll entire viewport by one
|
||||
# ESC 1L: insert 1 line at cursor position
|
||||
# (print text)
|
||||
# ESC 8: restore old cursor position
|
||||
|
||||
self._print_lock.acquire()
|
||||
sys.stdout.write('\x1b7\x1b[1A\x1b[1S\x1b[1L')
|
||||
sys.stdout.write(Logger._VT100Colors[color] + text + Logger._VT100Colors[Logger.COLOR_DEFAULT])
|
||||
sys.stdout.write('\x1b8')
|
||||
sys.stdout.flush()
|
||||
self._print_lock.release()
|
||||
|
||||
def print_colored(self, text, color):
|
||||
if self._skip_bottom_line:
|
||||
self.print_on_second_last_line(text, color)
|
||||
else:
|
||||
# On Windows, colorama does the job of interpreting the VT100 escape sequences
|
||||
self._print_lock.acquire()
|
||||
sys.stdout.write(Logger._VT100Colors[color] + text + Logger._VT100Colors[Logger.COLOR_DEFAULT] + '\n')
|
||||
sys.stdout.flush()
|
||||
self._print_lock.release()
|
||||
|
||||
def debug(self, text):
|
||||
if self._verbose:
|
||||
self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT)
|
||||
def success(self, text):
|
||||
self.print_colored(self._prefix + text, Logger.COLOR_GREEN)
|
||||
def info(self, text):
|
||||
self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT)
|
||||
def notify(self, text):
|
||||
self.print_colored(self._prefix + text, Logger.COLOR_CYAN)
|
||||
def warn(self, text):
|
||||
self.print_colored(self._prefix + text, Logger.COLOR_YELLOW)
|
||||
def error(self, text):
|
||||
# TODO: write to stderr
|
||||
self.print_colored(self._prefix + text, Logger.COLOR_RED)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
This script is used to deploy the Fibre python library to PyPi
|
||||
so that users can install them easily with
|
||||
"pip install fibre"
|
||||
|
||||
To install the package and its dependencies locally, run:
|
||||
sudo pip install -r requirements.txt
|
||||
|
||||
To build and package the python tools into a tar archive:
|
||||
python setup.py sdist
|
||||
|
||||
Warning: Before you proceed, be aware that you can upload a
|
||||
specific version only once ever. After that you need to increment
|
||||
the hotfix number. Deleting the release manually on the PyPi
|
||||
website does not help.
|
||||
|
||||
Use TestPyPi while developing.
|
||||
|
||||
To build, package and upload the python tools to TestPyPi, run:
|
||||
python setup.py sdist upload -r pypitest
|
||||
To make a real release ensure you're at the release commit
|
||||
and then run the above command without the "test" (so just "pypi").
|
||||
|
||||
To install a prerelease version from test index:
|
||||
sudo pip install --pre --index-url https://test.pypi.org/simple/ --no-cache-dir fibre
|
||||
|
||||
|
||||
PyPi access requires that you have set up ~/.pypirc with your
|
||||
PyPi credentials and that your account has the rights
|
||||
to publish packages with the name fibre.
|
||||
"""
|
||||
|
||||
# TODO: add additional y/n prompt to prevent from erroneous upload
|
||||
|
||||
from setuptools import setup
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Change this if you already uploaded the current
|
||||
# version but need to release a hotfix
|
||||
hotfix = 0
|
||||
|
||||
#creating_package = "sdist" in sys.argv
|
||||
#
|
||||
## Load version from Git tag
|
||||
#import odrive.version
|
||||
#version = odrive.version.get_version_str(git_only=creating_package)
|
||||
#
|
||||
#if creating_package and (hotfix > 0 or not version[-1].isdigit()):
|
||||
# # Add this for hotfixes
|
||||
# version += "-" + str(hotfix)
|
||||
#
|
||||
#
|
||||
## If we're currently creating the package we need to autogenerate
|
||||
## a file that contains the version string
|
||||
#if creating_package:
|
||||
# version_file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'odrive', 'version.txt')
|
||||
# with open(version_file_path, mode='w') as version_file:
|
||||
# version_file.write(version)
|
||||
#
|
||||
## TODO: find a better place for this
|
||||
#if not creating_package:
|
||||
# import platform
|
||||
# if platform.system() == 'Linux':
|
||||
# import odrive.utils
|
||||
# odrive.utils.setup_udev_rules(odrive.utils.Logger())
|
||||
|
||||
setup(
|
||||
name = 'fibre',
|
||||
packages = ['fibre'],
|
||||
#scripts = ['..fibre', 'odrivetool.bat', 'odrive_demo.py'],
|
||||
version = '0.0.1dev0',
|
||||
description = 'Abstraction layer for painlessly building object oriented distributed systems that just work',
|
||||
author = 'Samuel Sadok',
|
||||
author_email = 'samuel.sadok@bluewin.ch',
|
||||
license='MIT',
|
||||
url = 'https://github.com/samuelsadok/fibre',
|
||||
keywords = ['communication', 'transport-layer', 'rpc'],
|
||||
install_requires = [],
|
||||
#package_data={'': ['version.txt']},
|
||||
classifiers = [],
|
||||
)
|
||||
|
||||
# TODO: include README
|
||||
|
||||
## clean up
|
||||
#if creating_package:
|
||||
# os.remove(version_file_path)
|
||||
Reference in New Issue
Block a user