Skip to content

Commit

Permalink
Legacy v0.5 (#201)
Browse files Browse the repository at this point in the history
* Starting PyUAVCAN v0.5 Branch

* CAN-FD Support in SocketCAN

* Adding support to SocketCAN for CAN-FD 2.0B frames
and to upper layers as needed.
* Adding TAO support as well.
* Adding unit tests for CANFrameFd
  • Loading branch information
emrainey committed Jan 4, 2022
1 parent acf1e15 commit aa42988
Show file tree
Hide file tree
Showing 34 changed files with 518 additions and 179 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Main Workflow

on:
push:
branches: [legacy-v0]
branches: [legacy-v0_5]

jobs:
lint:
Expand Down Expand Up @@ -69,5 +69,5 @@ jobs:
continue-on-error: true # this step will fail if the file has already been published
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN_PYUAVCAN_V0 }}
password: ${{ secrets.PYPI_API_TOKEN_PYUAVCAN_V0_5 }}
verbose: true
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "pyuavcan_v0/dsdl_files"]
path = pyuavcan_v0/dsdl_files
[submodule "pyuavcan_v0_5/dsdl_files"]
path = pyuavcan_v0_5/dsdl_files
url = https://github.com/UAVCAN/public_regulated_data_types
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
Legacy UAVCAN/CAN v0 in Python
Legacy UAVCAN/CAN v0.5 in Python
==============================

This is the (legacy) implementation of UAVCAN/CAN v0 in Python. It is maintained under the `pyuavcan_v0` package name
This is the (legacy) implementation of UAVCAN/CAN v0.5 in Python. It is maintained under the `pyuavcan_v0_5` package name
to prevent conflict with the stable version of the protocol, UAVCAN v1.

This package is currently maintained for migration and backwards compatibility purposes. v0 is not recommended for
This package is currently maintained for migration and backwards compatibility purposes. v0.5 is not recommended for
new designs; new users should adopt pyuavcan v1. Please consult with [uavcan.org](https://uavcan.org) for details.

## Installation

```bash
pip install pyuavcan_v0
pip install pyuavcan_v0_5
```

## Development
Expand All @@ -21,7 +21,7 @@ In order to deploy to PyPI via CI, do this:

1. Update the version number in `version.py`, e.g. `1.0.0`, and commit before proceeding.
2. Create a new tag with the same version number, e.g. `git tag -a 1.0.0 -m "My release 1.0.0"`
3. Push to master.
3. Push to legacy-v0.5.

### Code style

Expand Down
34 changes: 26 additions & 8 deletions pyuavcan_v0/__init__.py → pyuavcan_v0_5/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
import pkg_resources
import time
from logging import getLogger
from abc import ABCMeta

CAN_2_DATAFIELD_LEN = 8
CAN_FD_DATAFIELD_LEN = 64

try:
# noinspection PyStatementEffect
Expand All @@ -45,18 +49,32 @@ def monotonic_wrapper(*args, **kwargs):
class UAVCANException(Exception):
pass

class CanBus(object):
__metaclass__ = ABCMeta

class CanBusFD(CanBus):
def __init__(self):
super(CanBusFD,self).__init__()
self.max_payload_len = CAN_FD_DATAFIELD_LEN
self.supports_tao = False

class CanBus2(CanBus):
def __init__(self):
super(CanBus2,self).__init__()
self.max_payload_len = CAN_2_DATAFIELD_LEN
self.supports_tao = True

from .version import __version__
import pyuavcan_v0.node as node
from pyuavcan_v0.node import make_node
from pyuavcan_v0.driver import make_driver
import pyuavcan_v0.dsdl as dsdl
import pyuavcan_v0.transport as transport
from pyuavcan_v0.transport import get_uavcan_data_type, \
import pyuavcan_v0_5.node as node
from pyuavcan_v0_5.node import make_node
from pyuavcan_v0_5.driver import make_driver
import pyuavcan_v0_5.dsdl as dsdl
import pyuavcan_v0_5.transport as transport
from pyuavcan_v0_5.transport import get_uavcan_data_type, \
get_active_union_field, switch_union_field, is_union, \
get_constants, get_fields, \
is_request, is_response
from pyuavcan_v0.introspect import value_to_constant_name, to_yaml
from pyuavcan_v0_5.introspect import value_to_constant_name, to_yaml


TRANSFER_PRIORITY_LOWEST = 31
Expand Down Expand Up @@ -189,4 +207,4 @@ def create_instance(*args, **kwargs):


# Importing modules that may be dependent on the standard DSDL types
import pyuavcan_v0.app as app
import pyuavcan_v0_5.app as app
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import time
import sqlite3
from logging import getLogger
import pyuavcan_v0
from pyuavcan_v0 import UAVCANException
import pyuavcan_v0_5
from pyuavcan_v0_5 import UAVCANException


logger = getLogger(__name__)
Expand All @@ -23,7 +23,7 @@ def _unique_id_to_string(uid):


class CentralizedServer(object):
QUERY_TIMEOUT = pyuavcan_v0.protocol.dynamic_node_id.Allocation().FOLLOWUP_TIMEOUT_MS / 1000 # @UndefinedVariable
QUERY_TIMEOUT = pyuavcan_v0_5.protocol.dynamic_node_id.Allocation().FOLLOWUP_TIMEOUT_MS / 1000 # @UndefinedVariable
DEFAULT_NODE_ID_RANGE = 1, 125
DATABASE_STORAGE_MEMORY = ':memory:'

Expand Down Expand Up @@ -103,7 +103,7 @@ def __init__(self, node, node_monitor, database_storage=None, dynamic_node_id_ra
self._node_monitor_event_handle = node_monitor.add_update_handler(self._handle_monitor_event)

self._dynamic_node_id_range = dynamic_node_id_range or CentralizedServer.DEFAULT_NODE_ID_RANGE
self._handle = node.add_handler(pyuavcan_v0.protocol.dynamic_node_id.Allocation, # @UndefinedVariable
self._handle = node.add_handler(pyuavcan_v0_5.protocol.dynamic_node_id.Allocation, # @UndefinedVariable
self._on_allocation_message)

self._allocation_table.set(node.node_info.hardware_version.unique_id.to_bytes(), node.node_id)
Expand Down Expand Up @@ -151,7 +151,7 @@ def _on_allocation_message(self, e):
self._query = e.message.unique_id.to_bytes()
self._query_timestamp = e.transfer.ts_monotonic

response = pyuavcan_v0.protocol.dynamic_node_id.Allocation() # @UndefinedVariable
response = pyuavcan_v0_5.protocol.dynamic_node_id.Allocation() # @UndefinedVariable
response.first_part_of_unique_id = 0
response.node_id = 0
response.unique_id.from_bytes(self._query)
Expand All @@ -165,7 +165,7 @@ def _on_allocation_message(self, e):
self._query += e.message.unique_id.to_bytes()
self._query_timestamp = e.transfer.ts_monotonic

response = pyuavcan_v0.protocol.dynamic_node_id.Allocation() # @UndefinedVariable
response = pyuavcan_v0_5.protocol.dynamic_node_id.Allocation() # @UndefinedVariable
response.first_part_of_unique_id = 0
response.node_id = 0
response.unique_id.from_bytes(self._query)
Expand Down Expand Up @@ -203,7 +203,7 @@ def _on_allocation_message(self, e):
if node_allocated_id:
self._allocation_table.set(self._query, node_allocated_id)

response = pyuavcan_v0.protocol.dynamic_node_id.Allocation() # @UndefinedVariable
response = pyuavcan_v0_5.protocol.dynamic_node_id.Allocation() # @UndefinedVariable
response.first_part_of_unique_id = 0
response.node_id = node_allocated_id
response.unique_id.from_bytes(self._query)
Expand Down
37 changes: 23 additions & 14 deletions pyuavcan_v0/app/file_server.py → pyuavcan_v0_5/app/file_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import os
from collections import defaultdict
from logging import getLogger
import pyuavcan_v0
import pyuavcan_v0_5
import errno


Expand All @@ -33,18 +33,19 @@ def _try_resolve_relative_path(search_in, rel_path):
class FileServer(object):
def __init__(self, node, lookup_paths=None):
if node.is_anonymous:
raise pyuavcan_v0.UAVCANException('File server cannot be launched on an anonymous node')
raise pyuavcan_v0_5.UAVCANException('File server cannot be launched on an anonymous node')

self.lookup_paths = lookup_paths or []

self._path_hit_counters = defaultdict(int)
self._handles = []
self._node_offsets = {}

def add_handler(datatype, callback):
self._handles.append(node.add_handler(datatype, callback))

add_handler(pyuavcan_v0.protocol.file.GetInfo, self._get_info)
add_handler(pyuavcan_v0.protocol.file.Read, self._read)
add_handler(pyuavcan_v0_5.protocol.file.GetInfo, self._get_info)
add_handler(pyuavcan_v0_5.protocol.file.Read, self._read)
# TODO: support all file services

def close(self):
Expand All @@ -55,6 +56,10 @@ def close(self):
def path_hit_counters(self):
return dict(self._path_hit_counters)

@property
def node_offsets(self):
return dict(self._node_offsets)

def _resolve_path(self, relative):
rel = relative.path.decode().replace(chr(relative.SEPARATOR), os.path.sep)
out = _try_resolve_relative_path(self.lookup_paths, rel)
Expand All @@ -65,36 +70,40 @@ def _resolve_path(self, relative):
return out

def _get_info(self, e):
logger.debug("[#{0:03d}:pyuavcan_v0.protocol.file.GetInfo] {1!r}"
logger.debug("[#{0:03d}:pyuavcan_v0_5.protocol.file.GetInfo] {1!r}"
.format(e.transfer.source_node_id, e.request.path.path.decode()))
try:
with open(self._resolve_path(e.request.path), "rb") as f:
data = f.read()
resp = pyuavcan_v0.protocol.file.GetInfo.Response()
resp = pyuavcan_v0_5.protocol.file.GetInfo.Response()
resp.error.value = resp.error.OK
resp.size = len(data)
resp.entry_type.flags = resp.entry_type.FLAG_FILE | resp.entry_type.FLAG_READABLE
except Exception:
# TODO: Convert OSError codes to the error codes defined in DSDL
logger.exception("[#{0:03d}:pyuavcan_v0.protocol.file.GetInfo] error", exc_info=True)
resp = pyuavcan_v0.protocol.file.GetInfo.Response()
logger.exception("[#{0:03d}:pyuavcan_v0_5.protocol.file.GetInfo] error", exc_info=True)
resp = pyuavcan_v0_5.protocol.file.GetInfo.Response()
resp.error.value = resp.error.UNKNOWN_ERROR

return resp

def _read(self, e):
logger.debug("[#{0:03d}:pyuavcan_v0.protocol.file.Read] {1!r} @ offset {2:d}"
logger.debug("[#{0:03d}:pyuavcan_v0_5.protocol.file.Read] {1!r} @ offset {2:d}"
.format(e.transfer.source_node_id, e.request.path.path.decode(), e.request.offset))
try:
with open(self._resolve_path(e.request.path), "rb") as f:
path = self._resolve_path(e.request.path)
with open(path, "rb") as f:
f.seek(e.request.offset)
resp = pyuavcan_v0.protocol.file.Read.Response()
read_size = pyuavcan_v0.get_uavcan_data_type(pyuavcan_v0.get_fields(resp)['data']).max_size
resp = pyuavcan_v0_5.protocol.file.Read.Response()
read_size = pyuavcan_v0_5.get_uavcan_data_type(pyuavcan_v0_5.get_fields(resp)['data']).max_size
resp.data = bytearray(f.read(read_size))
resp.error.value = resp.error.OK

file_size = os.path.getsize(path)
self._node_offsets[int(e.transfer.source_node_id)] = [int(e.request.offset), file_size]
except Exception:
logger.exception("[#{0:03d}:pyuavcan_v0.protocol.file.Read] error")
resp = pyuavcan_v0.protocol.file.Read.Response()
logger.exception("[#{0:03d}:pyuavcan_v0_5.protocol.file.Read] error")
resp = pyuavcan_v0_5.protocol.file.Read.Response()
resp.error.value = resp.error.UNKNOWN_ERROR

return resp
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

from __future__ import division, absolute_import, print_function, unicode_literals
from logging import getLogger
import pyuavcan_v0
import pyuavcan_v0_5


logger = getLogger(__name__)


class LogMessageMonitor(object):
def __init__(self, node):
self._handle = node.add_handler(pyuavcan_v0.protocol.debug.LogMessage, self._on_message) # @UndefinedVariable
self._handle = node.add_handler(pyuavcan_v0_5.protocol.debug.LogMessage, self._on_message) # @UndefinedVariable

def close(self):
self._handle.remove()
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import time
from logging import getLogger
import pyuavcan_v0
import pyuavcan_v0_5


logger = getLogger(__name__)


class NodeMonitor(object):
TIMEOUT = pyuavcan_v0.protocol.NodeStatus().OFFLINE_TIMEOUT_MS / 1000 # @UndefinedVariable
TRANSFER_PRIORITY = pyuavcan_v0.TRANSFER_PRIORITY_LOWEST - 1
TIMEOUT = pyuavcan_v0_5.protocol.NodeStatus().OFFLINE_TIMEOUT_MS / 1000 # @UndefinedVariable
TRANSFER_PRIORITY = pyuavcan_v0_5.TRANSFER_PRIORITY_LOWEST - 1
MIN_RETRY_INTERVAL = 0.5
MAX_RETRIES = 10

Expand All @@ -29,6 +29,7 @@ def __init__(self):
self.info = None
self.monotonic_timestamp = None
self._remaining_retries = NodeMonitor.MAX_RETRIES
self._info_requested_at = 0

@property
def discovered(self):
Expand Down Expand Up @@ -89,7 +90,7 @@ def try_remove(self):

def __init__(self, node):
self._update_callbacks = []
self._handle = node.add_handler(pyuavcan_v0.protocol.NodeStatus, self._on_node_status) # @UndefinedVariable
self._handle = node.add_handler(pyuavcan_v0_5.protocol.NodeStatus, self._on_node_status) # @UndefinedVariable
self._registry = {} # {node_id: Entry}
self._timer = node.periodic(1, self._remove_stale)

Expand Down Expand Up @@ -180,7 +181,7 @@ def _on_node_status(self, e):
entry._info_requested_at = entry.monotonic_timestamp
# noinspection PyProtectedMember
entry._register_retry()
e.node.request(pyuavcan_v0.protocol.GetNodeInfo.Request(), node_id, # @UndefinedVariable
e.node.request(pyuavcan_v0_5.protocol.GetNodeInfo.Request(), node_id, # @UndefinedVariable
priority=self.TRANSFER_PRIORITY, callback=self._on_info_response)

def _on_info_response(self, e):
Expand All @@ -198,7 +199,7 @@ def _on_info_response(self, e):

hw_unique_id = "".join(format(c, "02X") for c in e.response.hardware_version.unique_id)
msg = (
"[#{0:03d}:pyuavcan_v0.protocol.GetNodeInfo] " +
"[#{0:03d}:pyuavcan_v0_5.protocol.GetNodeInfo] " +
"software_version.major={1:d} " +
"software_version.minor={2:d} " +
"software_version.vcs_commit={3:08x} " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
import sys
from .python_can import PythonCAN
from .slcan import SLCAN
from .common import DriverError, CANFrame
from .common import DriverError, CANFrame, CANFrameFd

if sys.platform.startswith('linux'):
from .socketcan import SocketCAN
else:
SocketCAN = None

__all__ = ['make_driver', 'DriverError', 'CANFrame']
__all__ = ['make_driver', 'DriverError', 'CANFrame', 'CANFrameFd']


def make_driver(device_name, **kwargs):
Expand Down
21 changes: 21 additions & 0 deletions pyuavcan_v0/driver/common.py → pyuavcan_v0_5/driver/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@
logger = getLogger(__name__)


# The CAN 2.0B FD specification DLC (indexes) sizes (values)
DLCS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64]
assert len(DLCS) == 16, "Must have exactly 16 entries"


def dlc_to_len(dlc: int) -> int:
""" Converts a DLC to a length """
return DLCS[dlc & 0xF]


def len_to_dlc(length: int) -> int:
""" Returns None if not a valid DLC. """
return DLCS.index(length)


class DriverError(UAVCANException):
pass

Expand Down Expand Up @@ -49,6 +64,12 @@ def __str__(self):

__repr__ = __str__

class CANFrameFd(CANFrame):
MAX_DATA_LENGTH = 64

def __init__(self, can_id, data, extended, ts_monotonic=None, ts_real=None):
super(CANFrameFd, self).__init__(can_id, data, extended, ts_monotonic=ts_monotonic, ts_real=ts_real)


class AbstractDriver(object):
FRAME_DIRECTION_INCOMING = 'rx'
Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit aa42988

Please sign in to comment.