This commit is contained in:
2025-05-13 01:34:53 +03:00
parent 427735e23d
commit 83f3f1c7d4
945 changed files with 633484 additions and 0 deletions
@@ -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,5 @@
from .discovery import find_any, find_all
from .utils import Event, Logger, TimeoutError
from .protocol import ChannelBrokenException, ChannelDamagedException
from .shell import launch_shell
@@ -0,0 +1,179 @@
"""
Provides functions for the discovery of Fibre nodes
"""
import sys
import json
import time
import threading
import traceback
import struct
import fibre.protocol
import fibre.utils
import fibre.remote_object
from fibre.utils import Event, Logger
from fibre.protocol import ChannelBrokenException, TimeoutError
import appdirs
import os
# Load all installed transport layers
channel_types = {}
try:
import fibre.usbbulk_transport
channel_types['usb'] = fibre.usbbulk_transport.discover_channels
except ImportError:
pass
try:
import fibre.serial_transport
channel_types['serial'] = fibre.serial_transport.discover_channels
except ImportError:
pass
try:
import fibre.tcp_transport
channel_types['tcp'] = fibre.tcp_transport.discover_channels
except ImportError:
pass
try:
import fibre.udp_transport
channel_types['udp'] = fibre.udp_transport.discover_channels
except ImportError:
pass
def noprint(text):
pass
def find_all(path, serial_number,
did_discover_object_callback,
search_cancellation_token,
channel_termination_token,
logger):
"""
Starts scanning for Fibre nodes that match the specified path spec and calls
the callback for each Fibre node that is found.
This function is non-blocking.
"""
def did_discover_channel(channel):
"""
Inits an object from a given channel and then calls did_discover_object_callback
with the created object
This queries the endpoint 0 on that channel to gain information
about the interface, which is then used to init the corresponding object.
"""
try:
logger.debug("Connecting to device on " + channel._name)
cache_dir = appdirs.user_cache_dir("odrivetool")
cache_path = None
# Fetch the json version tag to check cache (only supported on firmware v0.5 or later)
try:
json_version_tag = channel.remote_endpoint_operation(0, struct.pack("<I", 0xffffffff), True, 4)
json_version_tag = struct.unpack("<I", json_version_tag)[0]
logger.debug("Device reported JSON version ID: {:08d}".format(json_version_tag))
cache_path = os.path.join(cache_dir, 'fibre_schema_cache_{:08d}'.format(json_version_tag))
except:
logger.debug("Failed to get JSON checksum")
# Check cache
json_data = None
try:
if not cache_path is None:
with open(cache_path, 'rb') as fp:
json_crc16 = fibre.protocol.calc_crc16(fibre.protocol.PROTOCOL_VERSION, fp.read())
fp.seek(0)
json_data = json.load(fp)
except:
logger.debug("Failed load JSON cache file {}".format(cache_path))
# Fallback to loading JSON from device
if json_data is None:
# Downloading json data
logger.info("Downloading json data from ODrive... (this might take a while)")
json_bytes = channel.remote_endpoint_read_buffer(0)
try:
json_string = json_bytes.decode("ascii")
except UnicodeDecodeError:
logger.debug("Device responded on endpoint 0 with something that is not ASCII")
raise UnicodeDecodeError
json_crc16 = fibre.protocol.calc_crc16(fibre.protocol.PROTOCOL_VERSION, json_bytes)
json_data = json.loads(json_string)
# Save JSON to cache
if not cache_path is None:
logger.debug("Creating new JSON cache file {}".format(cache_path))
os.makedirs(cache_dir, exist_ok=True)
with open(cache_path, 'w+') as json_cache:
json_cache.write(json_string)
logger.debug("Saved JSON to cache file {}".format(cache_path))
channel._interface_definition_crc = json_crc16
logger.debug("JSON: " + str(json_data).replace("{'name'", "\n{'name'"))
json_data = {"name": "fibre_node", "members": json_data}
obj = fibre.remote_object.RemoteObject(json_data, None, channel, logger)
obj.__dict__['_json_data'] = json_data['members']
obj.__dict__['_json_crc'] = json_crc16
device_serial_number = fibre.utils.get_serial_number_str(obj)
if serial_number != None and device_serial_number != serial_number:
logger.debug("Ignoring device with serial number {}".format(device_serial_number))
return
did_discover_object_callback(obj)
except Exception:
logger.debug("Unexpected exception after discovering channel: " + traceback.format_exc())
# For each connection type, kick off an appropriate discovery loop
for search_spec in path.split(','):
prefix = search_spec.split(':')[0]
the_rest = ':'.join(search_spec.split(':')[1:])
if prefix in channel_types:
t = threading.Thread(target=channel_types[prefix],
args=(the_rest, serial_number, did_discover_channel, search_cancellation_token, channel_termination_token, logger))
t.daemon = True
t.start()
else:
raise Exception("Invalid path spec \"{}\"".format(search_spec))
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 node is connected and then returns that node
"""
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]
@@ -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 ChannelBrokenException(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 ChannelBrokenException()
except TimeoutError:
attempt += 1
continue # resend
return self._responses.pop(seq_no)
# TODO: record channel statistics
raise ChannelBrokenException() # 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
@@ -0,0 +1,264 @@
"""
Provides functions for the discovery of Fibre nodes
"""
import sys
import json
import struct
import threading
import fibre.protocol
class ObjectDefinitionError(Exception):
pass
codecs = {}
class StructCodec():
"""
Generic serializer/deserializer based on struct pack
"""
def __init__(self, struct_format, target_type):
self._struct_format = struct_format
self._target_type = target_type
def get_length(self):
return struct.calcsize(self._struct_format)
def serialize(self, value):
value = self._target_type(value)
return struct.pack(self._struct_format, value)
def deserialize(self, buffer):
value = struct.unpack(self._struct_format, buffer)
value = value[0] if len(value) == 1 else value
return self._target_type(value)
class RemoteProperty():
"""
Used internally by dynamically created objects to translate
property assignments and fetches into endpoint operations on the
object's associated channel
"""
def __init__(self, json_data, parent):
self._parent = parent
self.__channel__ = parent.__channel__
id_str = json_data.get("id", None)
if id_str is None:
raise ObjectDefinitionError("unspecified endpoint ID")
self._id = int(id_str)
self._name = json_data.get("name", None)
if self._name is None:
self._name = "[anonymous]"
type_str = json_data.get("type", None)
if type_str is None:
raise ObjectDefinitionError("unspecified type")
# Find all codecs that match the type_str and build a dictionary
# of the form {type1: codec1, type2: codec2}
eligible_types = {k: v[type_str] for (k,v) in codecs.items() if type_str in v}
if not eligible_types:
raise ObjectDefinitionError("unsupported codec {}".format(type_str))
# TODO: better heuristics to select a matching type (i.e. prefer non lossless)
eligible_types = list(eligible_types.items())
self._property_type = eligible_types[0][0]
self._codec = eligible_types[0][1]
access_mode = json_data.get("access", "r")
self._can_read = 'r' in access_mode
self._can_write = 'w' in access_mode
def get_value(self):
buffer = self._parent.__channel__.remote_endpoint_operation(self._id, None, True, self._codec.get_length())
return self._codec.deserialize(buffer)
def set_value(self, value):
buffer = self._codec.serialize(value)
# TODO: Currenly we wait for an ack here. Settle on the default guarantee.
self._parent.__channel__.remote_endpoint_operation(self._id, buffer, True, 0)
def _dump(self):
if self._name == "serial_number":
# special case: serial number should be displayed in hex (TODO: generalize)
val_str = "{:012X}".format(self.get_value())
elif self._name == "error":
# special case: errors should be displayed in hex (TODO: generalize)
val_str = "0x{:04X}".format(self.get_value())
else:
val_str = str(self.get_value())
return "{} = {} ({})".format(self._name, val_str, self._property_type.__name__)
class EndpointRefCodec():
"""
Serializer/deserializer for an endpoint reference
"""
def get_length(self):
return struct.calcsize("<HH")
def serialize(self, value):
if value is None:
(ep_id, ep_crc) = (0, 0)
elif isinstance(value, RemoteProperty):
(ep_id, ep_crc) = (value._id, value.__channel__._interface_definition_crc)
else:
raise TypeError("Expected value of type RemoteProperty or None but got '{}'. En example for a RemoteProperty is this expression: odrv0.axis0.controller._remote_attributes['pos_setpoint']".format(type(value).__name__))
return struct.pack("<HH", ep_id, ep_crc)
def deserialize(self, buffer):
return struct.unpack("<HH", buffer)
codecs[int] = {
'int8': StructCodec("<b", int),
'uint8': StructCodec("<B", int),
'int16': StructCodec("<h", int),
'uint16': StructCodec("<H", int),
'int32': StructCodec("<i", int),
'uint32': StructCodec("<I", int),
'int64': StructCodec("<q", int),
'uint64': StructCodec("<Q", int)
}
codecs[bool] = {
'bool': StructCodec("<?", bool)
}
codecs[float] = {
'float': StructCodec("<f", float)
}
codecs[RemoteProperty] = {
'endpoint_ref': EndpointRefCodec()
}
class RemoteFunction(object):
"""
Represents a callable function that maps to a function call on a remote object
"""
def __init__(self, json_data, parent):
self._parent = parent
id_str = json_data.get("id", None)
if id_str is None:
raise ObjectDefinitionError("unspecified endpoint ID")
self._trigger_id = int(id_str)
self._name = json_data.get("name", None)
if self._name is None:
self._name = "[anonymous]"
self._inputs = []
for param_json in json_data.get("arguments", []) + json_data.get("inputs", []): # TODO: deprecate "arguments" keyword
param_json["mode"] = "r"
self._inputs.append(RemoteProperty(param_json, parent))
self._outputs = []
for param_json in json_data.get("outputs", []): # TODO: deprecate "arguments" keyword
param_json["mode"] = "r"
self._outputs.append(RemoteProperty(param_json, parent))
def __call__(self, *args):
if (len(self._inputs) != len(args)):
raise TypeError("expected {} arguments but have {}".format(len(self._inputs), len(args)))
for i in range(len(args)):
self._inputs[i].set_value(args[i])
self._parent.__channel__.remote_endpoint_operation(self._trigger_id, None, True, 0)
if len(self._outputs) > 0:
return self._outputs[0].get_value()
def _dump(self):
return "{}({})".format(self._name, ", ".join("{}: {}".format(x._name, x._property_type.__name__) for x in self._inputs))
class RemoteObject(object):
"""
Object with functions and properties that map to remote endpoints
"""
def __init__(self, json_data, parent, channel, logger):
"""
Creates an object that implements the specified JSON type description by
communicating over the provided channel
"""
# Directly write to __dict__ to avoid calling __setattr__ too early
object.__getattribute__(self, "__dict__")["_remote_attributes"] = {}
object.__getattribute__(self, "__dict__")["__sealed__"] = False
# Assign once more to make linter happy
self._remote_attributes = {}
self.__sealed__ = False
self.__channel__ = channel
self.__parent__ = parent
# Build attribute list from JSON
for member_json in json_data.get("members", []):
member_name = member_json.get("name", None)
if member_name is None:
logger.debug("ignoring unnamed attribute")
continue
try:
type_str = member_json.get("type", None)
if type_str == "object":
attribute = RemoteObject(member_json, self, channel, logger)
elif type_str == "function":
attribute = RemoteFunction(member_json, self)
elif type_str != None:
attribute = RemoteProperty(member_json, self)
else:
raise ObjectDefinitionError("no type information")
except ObjectDefinitionError as ex:
logger.debug("malformed member {}: {}".format(member_name, str(ex)))
continue
self._remote_attributes[member_name] = attribute
self.__dict__[member_name] = attribute
# Ensure that from here on out assignments to undefined attributes
# raise an exception
self.__sealed__ = True
channel._channel_broken.subscribe(self._tear_down)
def _dump(self, indent, depth):
if depth <= 0:
return "..."
lines = []
for key, val in self._remote_attributes.items():
if isinstance(val, RemoteObject):
val_str = indent + key + (": " if depth == 1 else ":\n") + val._dump(indent + " ", depth - 1)
else:
val_str = indent + val._dump()
lines.append(val_str)
return "\n".join(lines)
def __str__(self):
return self._dump("", depth=2)
def __repr__(self):
return self.__str__()
def __getattribute__(self, name):
attr = object.__getattribute__(self, "_remote_attributes").get(name, None)
if isinstance(attr, RemoteProperty):
if attr._can_read:
return attr.get_value()
else:
raise Exception("Cannot read from property {}".format(name))
elif attr != None:
return attr
else:
return object.__getattribute__(self, name)
#raise AttributeError("Attribute {} not found".format(name))
def __setattr__(self, name, value):
attr = object.__getattribute__(self, "_remote_attributes").get(name, None)
if isinstance(attr, RemoteProperty):
if attr._can_write:
attr.set_value(value)
else:
raise Exception("Cannot write to property {}".format(name))
elif not object.__getattribute__(self, "__sealed__") or name in object.__getattribute__(self, "__dict__"):
object.__getattribute__(self, "__dict__")[name] = value
else:
raise AttributeError("Attribute {} not found".format(name))
def _tear_down(self):
# Clear all remote members
for k in self._remote_attributes.keys():
self.__dict__.pop(k)
self._remote_attributes = {}
@@ -0,0 +1,108 @@
"""
Provides classes that implement the StreamSource/StreamSink and
PacketSource/PacketSink interfaces for serial ports.
"""
import os
import re
import time
import traceback
import serial
import serial.tools.list_ports
import fibre
from fibre.utils import TimeoutError
# TODO: make this customizable
DEFAULT_BAUDRATE = 115200
class SerialStreamTransport(fibre.protocol.StreamSource, fibre.protocol.StreamSink):
def __init__(self, port, baud):
self._timeout = 1
self._dev = serial.Serial(port, baud, timeout=self._timeout)
def process_bytes(self, bytes):
self._dev.write(bytes)
def get_bytes(self, n_bytes, deadline):
"""
Returns n bytes unless the deadline is reached, in which case the bytes
that were read up to that point are returned. If deadline is None the
function blocks forever. A deadline before the current time corresponds
to non-blocking mode.
"""
# Only set new timeout value if it is reasonably different from the old one (e.g. 20% as below)
# Otherwise it adds significant overhead (at least under Win10) as the port is reset with every reconfiguration
if deadline is None and self._timeout is not None:
self._timeout = None
self._dev.timeout = None
elif deadline is not None:
new_timeout = max(deadline - time.monotonic(), 0)
if abs(new_timeout - self._timeout) > self._timeout * 0.2:
self._timeout = new_timeout
self._dev.timeout = new_timeout
return self._dev.read(n_bytes)
def get_bytes_or_fail(self, n_bytes, deadline):
result = self.get_bytes(n_bytes, deadline)
if len(result) < n_bytes:
raise TimeoutError("expected {} bytes but got only {}", n_bytes, len(result))
return result
def close(self):
self._dev.close()
def find_dev_serial_ports():
try:
return ['/dev/' + x for x in os.listdir('/dev')]
except FileNotFoundError:
return []
def find_pyserial_ports():
return [x.device for x in serial.tools.list_ports.comports()]
def discover_channels(path, serial_number, callback, cancellation_token, channel_termination_token, logger):
"""
Scans for serial ports that match the path spec.
This function blocks until cancellation_token is set.
Channels spawned by this function run until channel_termination_token is set.
"""
if path == None:
# This regex should match all desired port names on macOS,
# Linux and Windows but might match some incorrect port names.
regex = r'^(/dev/tty\.usbmodem.*|/dev/ttyACM.*|COM[0-9]+)$'
else:
regex = "^" + path + "$"
known_devices = []
def device_matcher(port_name):
if port_name in known_devices:
return False
return bool(re.match(regex, port_name))
def did_disconnect(port_name, device):
device.close()
# TODO: yes there is a race condition here in case you wonder.
known_devices.pop(known_devices.index(port_name))
while not cancellation_token.is_set():
all_ports = find_pyserial_ports() + find_dev_serial_ports()
new_ports = filter(device_matcher, all_ports)
for port_name in new_ports:
try:
serial_device = SerialStreamTransport(port_name, DEFAULT_BAUDRATE)
input_stream = fibre.protocol.PacketFromStreamConverter(serial_device)
output_stream = fibre.protocol.StreamBasedPacketSink(serial_device)
channel = fibre.protocol.Channel(
"serial port {}@{}".format(port_name, DEFAULT_BAUDRATE),
input_stream, output_stream, channel_termination_token, logger)
channel.serial_device = serial_device
except serial.serialutil.SerialException:
logger.debug("Serial device init failed. Ignoring this port. More info: " + traceback.format_exc())
known_devices.append(port_name)
else:
known_devices.append(port_name)
channel._channel_broken.subscribe(lambda: did_disconnect(port_name, serial_device))
callback(channel)
time.sleep(1)
@@ -0,0 +1,128 @@
import sys
import platform
import threading
import fibre
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(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.__channel__._channel_broken.subscribe(lambda: 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 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)
# 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 ChannelBrokenException (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.protocol.ChannelBrokenException'):
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 ChannelBrokenException (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.protocol.ChannelBrokenException':\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()
@@ -0,0 +1,85 @@
import sys
import socket
import time
import traceback
import fibre.protocol
from fibre.utils import wait_any, TimeoutError
def noprint(x):
pass
class TCPTransport(fibre.protocol.StreamSource, fibre.protocol.StreamSink):
def __init__(self, dest_addr, dest_port, logger):
# TODO: FIXME: use IPv6
# Problem: getaddrinfo fails if the resolver returns an
# IPv4 address, but we are using AF_INET6
#family = socket.AF_INET6 if socket.has_ipv6 else socket.AF_INET
family = socket.AF_INET
self.sock = socket.socket(family, socket.SOCK_STREAM)
# TODO: Determine the right address to use from the list
self.target = socket.getaddrinfo(dest_addr, dest_port, family)[0][4]
# TODO: this blocks until a connection is established, or the system cancels it
self.sock.connect(self.target)
def process_bytes(self, buffer):
self.sock.send(buffer)
def get_bytes(self, n_bytes, deadline):
"""
Returns n bytes unless the deadline is reached, in which case the bytes
that were read up to that point are returned. If deadline is None the
function blocks forever. A deadline before the current time corresponds
to non-blocking mode.
"""
# convert deadline to seconds (floating point)
deadline = None if deadline is None else max(deadline - time.monotonic(), 0)
self.sock.settimeout(deadline)
try:
data = self.sock.recv(n_bytes, socket.MSG_WAITALL) # receive n_bytes
return data
except socket.timeout:
# if we got a timeout data will still be none, so we call recv again
# this time in non blocking state and see if we can get some data
try:
return self.sock.recv(n_bytes, socket.MSG_DONTWAIT)
except socket.timeout:
raise TimeoutError
def get_bytes_or_fail(self, n_bytes, deadline):
result = self.get_bytes(n_bytes, deadline)
if len(result) < n_bytes:
raise TimeoutError("expected {} bytes but got only {}".format(n_bytes, len(result)))
return result
def discover_channels(path, serial_number, callback, cancellation_token, channel_termination_token, logger):
"""
Tries to connect to a TCP server based on the path spec.
This function blocks until cancellation_token is set.
Channels spawned by this function run until channel_termination_token is set.
"""
try:
dest_addr = ':'.join(path.split(":")[:-1])
dest_port = int(path.split(":")[-1])
except (ValueError, IndexError):
raise Exception('"{}" is not a valid TCP destination. The format should be something like "localhost:1234".'
.format(path))
while not cancellation_token.is_set():
try:
tcp_transport = fibre.tcp_transport.TCPTransport(dest_addr, dest_port, logger)
stream2packet_input = fibre.protocol.PacketFromStreamConverter(tcp_transport)
packet2stream_output = fibre.protocol.StreamBasedPacketSink(tcp_transport)
channel = fibre.protocol.Channel(
"TCP device {}:{}".format(dest_addr, dest_port),
stream2packet_input, packet2stream_output,
channel_termination_token, logger)
except:
#logger.debug("TCP channel init failed. More info: " + traceback.format_exc())
pass
else:
callback(channel)
wait_any(None, cancellation_token, channel._channel_broken)
time.sleep(1)
@@ -0,0 +1,57 @@
import sys
import socket
import time
import traceback
import fibre.protocol
from fibre.utils import wait_any
def noprint(x):
pass
class UDPTransport(fibre.protocol.PacketSource, fibre.protocol.PacketSink):
def __init__(self, dest_addr, dest_port, logger):
# TODO: FIXME: use IPv6
# Problem: getaddrinfo fails if the resolver returns an
# IPv4 address, but we are using AF_INET6
#family = socket.AF_INET6 if socket.has_ipv6 else socket.AF_INET
family = socket.AF_INET
self.sock = socket.socket(family, socket.SOCK_DGRAM)
# TODO: Determine the right address to use from the list
self.target = socket.getaddrinfo(dest_addr,dest_port, family)[0][4]
def process_packet(self, buffer):
self.sock.sendto(buffer, self.target)
def get_packet(self, deadline):
# TODO: implement deadline
data, _ = self.sock.recvfrom(1024)
return data
def discover_channels(path, serial_number, callback, cancellation_token, channel_termination_token, logger):
"""
Tries to connect to a UDP server based on the path spec.
This function blocks until cancellation_token is set.
Channels spawned by this function run until channel_termination_token is set.
"""
try:
dest_addr = ':'.join(path.split(":")[:-1])
dest_port = int(path.split(":")[-1])
except (ValueError, IndexError):
raise Exception('"{}" is not a valid UDP destination. The format should be something like "localhost:1234".'
.format(path))
while not cancellation_token.is_set():
try:
udp_transport = fibre.udp_transport.UDPTransport(dest_addr, dest_port, logger)
channel = fibre.protocol.Channel(
"UDP device {}:{}".format(dest_addr, dest_port),
udp_transport, udp_transport,
channel_termination_token, logger)
except:
logger.debug("UDP channel init failed. More info: " + traceback.format_exc())
pass
else:
callback(channel)
wait_any(None, cancellation_token, channel._channel_broken)
time.sleep(1)
@@ -0,0 +1,216 @@
# requires pyusb
# pip install --pre pyusb
import usb.core
import usb.util
import sys
import time
import fibre.protocol
import traceback
import platform
from fibre.utils import TimeoutError
# Currently we identify fibre-enabled devices by VID,PID
# TODO: identify by USB descriptors
WELL_KNOWN_VID_PID_PAIRS = [
(0x1209, 0x0D31),
(0x1209, 0x0D32),
(0x1209, 0x0D33)
]
class USBBulkTransport(fibre.protocol.PacketSource, fibre.protocol.PacketSink):
def __init__(self, dev, logger):
self._logger = logger
self.dev = dev
self.intf = None
self._name = "USB device {}:{}".format(dev.idVendor, dev.idProduct)
self._was_damaged = False
##
# information about the connected device
##
def info(self):
# loop through configurations
string = ""
for cfg in self.dev:
string += "ConfigurationValue {0}\n".format(cfg.bConfigurationValue)
for intf in cfg:
string += "\tInterfaceNumber {0},{1}\n".format(intf.bInterfaceNumber, intf.bAlternateSetting)
for ep in intf:
string += "\t\tEndpointAddress {0}\n".format(ep.bEndpointAddress)
return string
def init(self):
# Under some conditions, the Linux USB/libusb stack ends up in a corrupt
# state where there are a few packets in a receive queue but a call
# to epr.read() does not return these packet until a new packet arrives.
# This undesirable queue can be cleared by resetting the device.
# On windows this would cause file-not-found errors in subsequent dev calls
if platform.system() != 'Windows':
self.dev.reset()
#self.dev.set_configuration() # no args: set first configuration
# Find the best interface
self.cfg = self.dev.get_active_configuration()
custom_interfaces = [i for i in self.cfg.interfaces() if i.bInterfaceClass == 0x00 and i.bInterfaceSubClass == 0x01]
cdc_interfaces = [i for i in self.cfg.interfaces() if i.bInterfaceClass == 0x0a and i.bInterfaceSubClass == 0x00]
all_compatible_interfaces = custom_interfaces + cdc_interfaces
if len(all_compatible_interfaces) == 0:
raise Exception("the device has no compatible interfaces")
self.intf = all_compatible_interfaces[0]
# Try to detach kernel driver from interface
try:
if self.dev.is_kernel_driver_active(self.intf.bInterfaceNumber):
self.dev.detach_kernel_driver(self.intf.bInterfaceNumber)
self._logger.debug("Detached Kernel Driver")
else:
self._logger.debug("Kernel Driver was not attached")
except NotImplementedError:
pass #is_kernel_driver_active not implemented on Windows
# find write endpoint (first OUT endpoint)
self.epw = usb.util.find_descriptor(self.intf,
custom_match = \
lambda e: \
usb.util.endpoint_direction(e.bEndpointAddress) == \
usb.util.ENDPOINT_OUT
)
assert self.epw is not None
self._logger.debug("EndpointAddress for writing {}".format(self.epw.bEndpointAddress))
# find read endpoint (first IN endpoint)
self.epr = usb.util.find_descriptor(self.intf,
custom_match = \
lambda e: \
usb.util.endpoint_direction(e.bEndpointAddress) == \
usb.util.ENDPOINT_IN
)
assert self.epr is not None
self._logger.debug("EndpointAddress for reading {}".format(self.epr.bEndpointAddress))
def deinit(self):
if not self.intf is None:
usb.util.release_interface(self.dev, self.intf)
def process_packet(self, usbBuffer):
try:
ret = self.epw.write(usbBuffer, 0)
if self._was_damaged:
self._logger.debug("Recovered from USB halt/stall condition")
self._was_damaged = False
return ret
except usb.core.USBError as ex:
if ex.errno == 19 or ex.errno == 32: # "no such device", "pipe error"
raise fibre.protocol.ChannelBrokenException()
elif ex.errno is None or ex.errno == 60 or ex.errno == 110: # timeout
raise TimeoutError()
else:
self._logger.debug("error in usbbulk_transport.py, process_packet")
self._logger.debug(traceback.format_exc())
self._logger.debug("halt condition: {}".format(ex.errno))
self._logger.debug(str(ex))
# Try resetting halt/stall condition
try:
self.deinit()
self.init()
except usb.core.USBError:
raise fibre.protocol.ChannelBrokenException()
# Retry transfer
self._was_damaged = True
raise fibre.protocol.ChannelDamagedException()
def get_packet(self, deadline):
try:
bufferLen = self.epr.wMaxPacketSize
timeout = max(int((deadline - time.monotonic()) * 1000), 0)
ret = self.epr.read(bufferLen, timeout)
if self._was_damaged:
self._logger.debug("Recovered from USB halt/stall condition")
self._was_damaged = False
return bytearray(ret)
except usb.core.USBError as ex:
if ex.errno == 19 or ex.errno == 32: # "no such device", "pipe error"
raise fibre.protocol.ChannelBrokenException()
elif ex.errno is None or ex.errno == 60 or ex.errno == 110: # timeout
raise TimeoutError()
else:
self._logger.debug("error in usbbulk_transport.py, process_packet")
self._logger.debug(traceback.format_exc())
self._logger.debug("halt condition: {}".format(ex.errno))
self._logger.debug(str(ex))
# Try resetting halt/stall condition
try:
self.deinit()
self.init()
except usb.core.USBError:
raise fibre.protocol.ChannelBrokenException()
# Retry transfer
self._was_damaged = True
raise fibre.protocol.ChannelDamagedException()
def discover_channels(path, serial_number, callback, cancellation_token, channel_termination_token, logger):
"""
Scans for USB devices that match the path spec.
This function blocks until cancellation_token is set.
Channels spawned by this function run until channel_termination_token is set.
"""
if path == None or path == "":
bus = None
address = None
else:
try:
bus = int(path.split(":")[0])
address = int(path.split(":")[1])
except (ValueError, IndexError):
raise Exception("{} is not a valid USB path specification. "
"Expected a string of the format BUS:DEVICE where BUS "
"and DEVICE are integers.".format(path))
known_devices = []
def device_matcher(device):
#print(" test {:04X}:{:04X}".format(device.idVendor, device.idProduct))
try:
if (device.bus, device.address) in known_devices:
return False
if bus != None and device.bus != bus:
return False
if address != None and device.address != address:
return False
if serial_number != None and device.serial_number != serial_number:
return False
if (device.idVendor, device.idProduct) not in WELL_KNOWN_VID_PID_PAIRS:
return False
except:
return False
return True
while not cancellation_token.is_set():
# logger.debug("USB discover loop")
devices = usb.core.find(find_all=True, custom_match=device_matcher)
for usb_device in devices:
try:
bulk_device = USBBulkTransport(usb_device, logger)
logger.debug(bulk_device.info())
bulk_device.init()
channel = fibre.protocol.Channel(
"USB device bus {} device {}".format(usb_device.bus, usb_device.address),
bulk_device, bulk_device, channel_termination_token, logger)
channel.usb_device = usb_device # for debugging only
except usb.core.USBError as ex:
if ex.errno == 13:
# TODO: this is an ODrive specific message and should live outside of the fibre library
logger.warn("I found a USB device that looks like an ODrive (bus {}, device {}) but I can't access it. Try running `sudo odrivetool udev-setup`, then unplug and replug the device.".format(usb_device.bus, usb_device.address))
known_devices.append((usb_device.bus, usb_device.address))
elif ex.errno == 16:
logger.debug("USB device busy. I'll reset it and try again.")
usb_device.reset()
continue
else:
logger.warn("USB device init failed (bus {}, device {}). Ignoring this device. More info: ".format(usb_device.bus, usb_device.address) + traceback.format_exc())
known_devices.append((usb_device.bus, usb_device.address))
else:
known_devices.append((usb_device.bus, usb_device.address))
callback(channel)
time.sleep(1)
@@ -0,0 +1,237 @@
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.
Returns a function that can be invoked to unsubscribe.
"""
if handler is None:
raise TypeError
self._mutex.acquire()
try:
self._subscribers.append(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,90 @@
"""
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 = [
'appdirs', # Used to find caching directory
],
#package_data={'': ['version.txt']},
classifiers = [],
)
# TODO: include README
## clean up
#if creating_package:
# os.remove(version_file_path)