From 6c173b3a42fcb93547311628e8f55f1a3588592b Mon Sep 17 00:00:00 2001 From: John Morris Date: Wed, 18 May 2022 16:43:11 -0500 Subject: [PATCH 1/6] logging: Separate out rclpy & python logging classes --- hw_device_mgr/logging.py | 33 -------------- hw_device_mgr/logging/__init__.py | 45 +++++++++++++++++++ hw_device_mgr/logging/ros.py | 30 +++++++++++++ hw_device_mgr/logging/tests/__init__.py | 0 hw_device_mgr/logging/tests/test_logging.py | 21 +++++++++ .../logging/tests/test_ros_logging.py | 13 ++++++ setup.py | 1 + 7 files changed, 110 insertions(+), 33 deletions(-) delete mode 100644 hw_device_mgr/logging.py create mode 100644 hw_device_mgr/logging/__init__.py create mode 100644 hw_device_mgr/logging/ros.py create mode 100644 hw_device_mgr/logging/tests/__init__.py create mode 100644 hw_device_mgr/logging/tests/test_logging.py create mode 100644 hw_device_mgr/logging/tests/test_ros_logging.py diff --git a/hw_device_mgr/logging.py b/hw_device_mgr/logging.py deleted file mode 100644 index 83531aac..00000000 --- a/hw_device_mgr/logging.py +++ /dev/null @@ -1,33 +0,0 @@ -from rclpy import logging - - -class Logging: - """Wrapper for `rclpy.logging` object.""" - - def __init__(self, name): - self._logger = logging.get_logger(name) - - # Translate Python str levels to rclpy str levels (int levels are - # the same) - _rclpy_level_map = dict( - critical="fatal", - error="error", - warning="warn", - info="info", - debug="debug", - notset="unset", - ) - - def setLevel(self, level): - if isinstance(level, str): - level = self._rclpy_level_map.get(level.upper(), level).upper() - self._logger.set_logger_level(level) - - def __getattr__(self, name): - if name in self._rclpy_level_map: - return getattr(self._logger, self._rclpy_level_map[name]) - raise AttributeError(f"'Logging' object has no attribute '{name}'") - - @classmethod - def getLogger(cls, name): - return cls(name) diff --git a/hw_device_mgr/logging/__init__.py b/hw_device_mgr/logging/__init__.py new file mode 100644 index 00000000..1ef29588 --- /dev/null +++ b/hw_device_mgr/logging/__init__.py @@ -0,0 +1,45 @@ +import logging + + +class Logging: + """Wrapper for `logging` module.""" + + _logging_class = logging + + def __init__(self, name): + lc = self._logging_class + lo = self._logger = lc.getLogger(name) + lh = self._log_handler = lc.StreamHandler() + lf = lc.Formatter("%(asctime)s [%(levelname)s]%(name)s: %(message)s") + lh.setFormatter(lf) + lo.addHandler(lh) + + # Translate Python str levels to str levels (int levels are the same) + _level_map = dict( + fatal="fatal", + error="error", + warning="warning", + info="info", + debug="debug", + notset="notset", + ) + + def setLevel(self, level): + if isinstance(level, str): + level = self._level_map.get(level.upper(), level).upper() + self._logger.setLevel(level) + + def getLevel(self): + return self._logger.getEffectiveLevel() + + def __getattr__(self, name): + if name in self._level_map: + return getattr(self._logger, self._level_map[name]) + if name.lower() in self._level_map: + attr = self._level_map[name.lower()].upper() + return getattr(self._logging_class, attr) + raise AttributeError(f"'Logging' object has no attribute '{name}'") + + @classmethod + def getLogger(cls, name): + return cls(name) diff --git a/hw_device_mgr/logging/ros.py b/hw_device_mgr/logging/ros.py new file mode 100644 index 00000000..c04ddc44 --- /dev/null +++ b/hw_device_mgr/logging/ros.py @@ -0,0 +1,30 @@ +from rclpy import logging +from . import Logging +from rclpy.logging import LoggingSeverity + + +class ROSLogging(Logging): + """Wrapper for `rclpy.logging` object.""" + + def __init__(self, name): + self._logger = logging.get_logger(name) + + # Translate Python str levels to rclpy str levels (int levels are + # the same) + _rclpy_level_map = dict( + critical=LoggingSeverity.FATAL, + error=LoggingSeverity.ERROR, + warning=LoggingSeverity.WARN, + info=LoggingSeverity.INFO, + debug=LoggingSeverity.DEBUG, + notset=LoggingSeverity.UNSET, + ) + + def setLevel(self, level): + if isinstance(level, str): + level = self._rclpy_level_map.get(level.lower(), None) + assert level is not None, f"Invalid log level '{level}'" + self._logger.set_level(level) + + def getLevel(self): + return self._logger.get_effective_level() diff --git a/hw_device_mgr/logging/tests/__init__.py b/hw_device_mgr/logging/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw_device_mgr/logging/tests/test_logging.py b/hw_device_mgr/logging/tests/test_logging.py new file mode 100644 index 00000000..2bdf45d1 --- /dev/null +++ b/hw_device_mgr/logging/tests/test_logging.py @@ -0,0 +1,21 @@ +import pytest +from .. import Logging + + +class TestLogging: + tc = Logging + + @pytest.fixture + def obj(self): + return self.tc("test") + + def test_init(self, obj): + assert hasattr(obj, "_logger") + + def test_getLevel_setLevel(self, obj): + obj.setLevel("error") + assert obj.getLevel() == obj.ERROR + + def test_getLogger(self): + logger = self.tc.getLogger("test") + assert isinstance(logger, self.tc) diff --git a/hw_device_mgr/logging/tests/test_ros_logging.py b/hw_device_mgr/logging/tests/test_ros_logging.py new file mode 100644 index 00000000..030dcbf7 --- /dev/null +++ b/hw_device_mgr/logging/tests/test_ros_logging.py @@ -0,0 +1,13 @@ +import pytest + +try: + import rclpy # noqa: F401 +except ModuleNotFoundError: + pytest.skip(allow_module_level=True) + +from ..ros import ROSLogging +from .test_logging import TestLogging as _TestLogging + + +class TestROSLogging(_TestLogging): + tc = ROSLogging diff --git a/setup.py b/setup.py index c23a1e4d..34bfd937 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ "ethercat", "hal", "lcec", + "logging", "mgr", "mgr_hal", "mgr_ros", From 03c8d62180264603bfb945ab2979085a962372be Mon Sep 17 00:00:00 2001 From: John Morris Date: Wed, 1 Jun 2022 14:00:51 -0500 Subject: [PATCH 2/6] Add `logging` submodule to setup.py --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 34bfd937..fc43bfce 100644 --- a/setup.py +++ b/setup.py @@ -10,14 +10,17 @@ "ethercat", "hal", "lcec", - "logging", "mgr", "mgr_hal", "mgr_ros", "mgr_ros_hal", ] # Packages like hw_device_mgr.{pkg}.tests -pkgs_t = ["devices"] + pkgs_bd +pkgs_t = [ + "devices", + "logging", + *pkgs_bd, +] # Generate lists packages = ( [ From 3a39ecebf48300cb77903578e8fe59f2b9114d01 Mon Sep 17 00:00:00 2001 From: John Morris Date: Wed, 18 May 2022 16:46:39 -0500 Subject: [PATCH 3/6] mgr_ros: Skip tests if `rclpy` module not available --- hw_device_mgr/mgr_ros/tests/base_test_class.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hw_device_mgr/mgr_ros/tests/base_test_class.py b/hw_device_mgr/mgr_ros/tests/base_test_class.py index 560e22de..f2929c46 100644 --- a/hw_device_mgr/mgr_ros/tests/base_test_class.py +++ b/hw_device_mgr/mgr_ros/tests/base_test_class.py @@ -1,8 +1,13 @@ from ...mgr.tests.base_test_class import BaseMgrTestClass -from .bogus_devices.mgr import ROSHWDeviceMgrTest import yaml import pytest +try: + import rclpy # noqa: F401 +except ModuleNotFoundError: + pytest.skip(allow_module_level=True) +from .bogus_devices.mgr import ROSHWDeviceMgrTest + ############################### # Test class @@ -75,7 +80,7 @@ def device_config_path(self, tmp_path, device_config, mock_rclpy): yield tmpfile def test_mock_rclpy_fixture(self, mock_rclpy): - from ..mgr import rclpy + from ..mgr import rclpy # noqa: F811 node = rclpy.create_node("foo") assert node is self.node From a1a7671bedb308a7d4c068a8b4e0e35a6871522a Mon Sep 17 00:00:00 2001 From: John Morris Date: Wed, 1 Jun 2022 14:13:59 -0500 Subject: [PATCH 4/6] Convert to `ament_python` package (ROS2 python-only) --- CMakeLists.txt | 57 ------------------------------------------ package.xml | 5 +++- resource/hw_device_mgr | 0 3 files changed, 4 insertions(+), 58 deletions(-) delete mode 100644 CMakeLists.txt create mode 100644 resource/hw_device_mgr diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index c493202d..00000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,57 +0,0 @@ -cmake_minimum_required(VERSION 3.5) -project(hw_device_mgr) - -# Find dependencies -find_package(ament_cmake REQUIRED) -# - RT -find_package(hal_hw_interface REQUIRED) -# - Python -find_package(ament_cmake_python REQUIRED) -find_package(rclpy REQUIRED) -# - Msg -find_package(rosidl_default_generators REQUIRED) - -#*********************************************** -#* Declare ROS messages, services and actions ** -#*********************************************** - -rosidl_generate_interfaces(${PROJECT_NAME} - msg/MsgError.msg - ADD_LINTER_TESTS -) - -#************ -#* Install ** -#************ - -# Install python modules -ament_python_install_package(${PROJECT_NAME}) - -#********** -#* Tests ** -#********** - -if(BUILD_TESTING) - - find_package(ament_cmake_flake8 REQUIRED) - ament_flake8() - find_package(ament_cmake_lint_cmake REQUIRED) - ament_lint_cmake() - # FIXME disable until this is resolved, either by loading the xsd - # file or by moving the ESI files out of this repo or by excluding - # ESI files from the linter. - # - # warning: failed to load external entity "EtherCATInfo.xsd" - # Schemas parser error : Failed to locate the main schema resource at 'EtherCATInfo.xsd'. - # WXS schema EtherCATInfo.xsd failed to compile - # - # find_package(ament_cmake_xmllint REQUIRED) - # ament_xmllint() - find_package(ament_cmake_pytest REQUIRED) - ament_add_pytest_test(test_modules "hw_device_mgr" TIMEOUT 120) - -endif() - -ament_export_dependencies(rosidl_default_runtime) - -ament_package() diff --git a/package.xml b/package.xml index 4a1fe168..765fb57b 100644 --- a/package.xml +++ b/package.xml @@ -24,6 +24,9 @@ python-fysom python3-lxml + ament_copyright + ament_flake8 + ament_pep257 python3-pytest python3-pytest-cov python3-pytest-mock @@ -31,6 +34,6 @@ rosidl_interface_packages - ament_cmake + ament_python diff --git a/resource/hw_device_mgr b/resource/hw_device_mgr new file mode 100644 index 00000000..e69de29b From ab232357b97eaa5f0abde5ff129e0f3fca324001 Mon Sep 17 00:00:00 2001 From: John Morris Date: Mon, 6 Jun 2022 14:05:42 -0500 Subject: [PATCH 5/6] mgr: Replace old ROS1 rate with `time.sleep()` --- hw_device_mgr/mgr/mgr.py | 5 ++--- hw_device_mgr/mgr/tests/bogus_devices/mgr_config.yaml | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hw_device_mgr/mgr/mgr.py b/hw_device_mgr/mgr/mgr.py index 1948d811..5d30c4ad 100644 --- a/hw_device_mgr/mgr/mgr.py +++ b/hw_device_mgr/mgr/mgr.py @@ -47,8 +47,6 @@ def device_model_id(cls): command_out_defaults = dict(state_cmd=0, reset=0) command_out_data_types = dict(state_cmd="uint8", reset="bit") - update_rate = 10 # Hz - #################################################### # Initialization @@ -406,6 +404,7 @@ def fsm_finalize_command(self, e): def run(self): """Program main loop.""" + update_period = 1.0 / self.mgr_config.get("update_rate", 10.0) while not self.shutdown: try: self.read_update_write() @@ -423,7 +422,7 @@ def run(self): # the `sleep()` before the next update self.fast_track = False continue - self.rate.sleep() + time.sleep(update_period) def read_update_write(self): """ diff --git a/hw_device_mgr/mgr/tests/bogus_devices/mgr_config.yaml b/hw_device_mgr/mgr/tests/bogus_devices/mgr_config.yaml index 9aae8a01..836ba8cb 100644 --- a/hw_device_mgr/mgr/tests/bogus_devices/mgr_config.yaml +++ b/hw_device_mgr/mgr/tests/bogus_devices/mgr_config.yaml @@ -1,5 +1,6 @@ init_timeout: 30.0 # seconds goal_state_timeout: 5.0 # seconds +update_rate: 20.0 # updates/second devices: # 6 DOF joints From a37e813d6985d8f3c6a4ca4fc062b728ea515c3d Mon Sep 17 00:00:00 2001 From: John Morris Date: Thu, 22 Sep 2022 13:45:52 -0500 Subject: [PATCH 6/6] devices: Add `devices/device_xml/__init__.py` Fixup for PR #14 commit f6c516cd, "setup.py: Add device_xml package resources to install" --- hw_device_mgr/devices/device_xml/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hw_device_mgr/devices/device_xml/__init__.py diff --git a/hw_device_mgr/devices/device_xml/__init__.py b/hw_device_mgr/devices/device_xml/__init__.py new file mode 100644 index 00000000..e69de29b