diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a2109cc20..9ecf0a12f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -36,4 +36,5 @@ jobs: diagnostic_aggregator diagnostic_common_diagnostics diagnostic_updater + ros2diagnostics_cli self_test diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index dcd065e67..e526fe9cb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,6 +19,7 @@ jobs: diagnostic_aggregator, diagnostic_common_diagnostics, diagnostic_updater, + ros2diagnostics_cli, self_test, ] distro: [foxy, humble, rolling] diff --git a/ros2diagnostics_cli/README.md b/ros2diagnostics_cli/README.md new file mode 100644 index 000000000..db9355a53 --- /dev/null +++ b/ros2diagnostics_cli/README.md @@ -0,0 +1,152 @@ +# ROS2 diagnostic cli + +ROS2 cli to analysis and monitor `/diagnostics` topic +It's alternative to `diagnostic_analysis` project that not ported yet to ROS2. + +The project add `diagnostics` command to ROS2 cli with there verbs. + +- list +- show +- csv + +### list +Monitor the current diagnostics_status message from `/diagnostics` topic group by Node name and print `diagnostics status` name + +```bash +ros2 diagnostics list +# Result +--- time: 1682528234 --- +diagnostic_simple: +- DemoTask +- DemoTask2 +``` + +### show +Monitor `/diagnostics` topic and print the diagnostics_status data can filter by level and node/status name + +```bash +ros2 diagnostics show -h +usage: ros2 diagnostics show [-h] [-1] [-f FILTER] [--verbose] [-l {info,warn,error}] + +Show diagnostics status item info + +options: + -h, --help show this help message and exit + -1, --once run only once + -f FILTER, --filter FILTER + filter diagnostic status name + --verbose, -v Display more info. + -l {info,warn,error}, --levels {info,warn,error} + levels to filter, can be multiple times +``` + +#### demo + +```bash title="show all diagnostics status" +ros2 diagnostics show +# +--- time: 1682528494 --- +diagnostic_simple: DemoTask: WARN, running +diagnostic_simple: DemoTask2: ERROR, bad +--- time: 1682528495 --- +diagnostic_simple: DemoTask: WARN, running +diagnostic_simple: DemoTask2: ERROR, bad +``` + +```bash title="filter by level" +ros2 diagnostics show -l error +--- time: 1682528568 --- +diagnostic_simple: DemoTask2: ERROR, bad +--- time: 1682528569 --- +diagnostic_simple: DemoTask2: ERROR, bad +--- time: 1682528570 --- +``` + +```bash title="filter by name" +ros2 diagnostics show -f Task2 +# +--- time: 1682528688 --- +diagnostic_simple: DemoTask2: ERROR, bad +--- time: 1682528689 --- +diagnostic_simple: DemoTask2: ERROR, bad +``` + +```bash title="verbose usage" +ros2 diagnostics show -l warn -v +# +--- time: 1682528760 --- +diagnostic_simple: DemoTask: WARN, running +- key1=val1 +- key2=val2 +--- time: 1682528761 --- +diagnostic_simple: DemoTask: WARN, running +- key1=val1 +- key2=val2 + +``` + +### csv +Export `/diagnostics` topic to csv file + +**CSV headers**: +- time (sec) +- level +- node name +- diagnostics status name +- message +- hardware id +- values from keyvalue field (only on verbose) + + +```bash +ros2 diagnostics csv --help +usage: ros2 diagnostics csv [-h] [-1] [-f FILTER] [-l {info,warn,error}] [--output OUTPUT] [--verbose] + +export /diagnostics message to csv file + +options: + -h, --help show this help message and exit + -1, --once run only once + -f FILTER, --filter FILTER + filter diagnostic status name + -l {info,warn,error}, --levels {info,warn,error} + levels to filter, can be multiple times + --output OUTPUT, -o OUTPUT + export file full path + --verbose, -v export DiagnosticStatus values filed +``` + +#### Demos + +```bash title="simple csv file" +ros2 diagnostics csv -o /tmp/1.csv +--- time: 1682529183 --- +1682529183,WARN,diagnostic_simple,DemoTask,running, +``` + +```bash title="show csv file" +cat /tmp/1.csv + +1682529183,WARN,diagnostic_simple,DemoTask,running, +1682529183,ERROR,diagnostic_simple,DemoTask2,bad, +``` + +```bash title="filter by level" + ros2 diagnostics csv -o /tmp/1.csv -l error +``` + +```bash title="filter by name with regex" +ros2 diagnostics csv -o /tmp/1.csv -f Task$ -v +``` + +## Todo +- More tests +- Add unit test +- DEB package and install tests +- Ideas + + +## Tests +``` +ros2 launch diagnostic_aggregator example.launch.py +``` diff --git a/ros2diagnostics_cli/package.xml b/ros2diagnostics_cli/package.xml new file mode 100644 index 000000000..c17591dce --- /dev/null +++ b/ros2diagnostics_cli/package.xml @@ -0,0 +1,20 @@ + + + + ros2diagnostics_cli + 3.1.2 + diagnostic command for ROS2 command line, parse and show /diagnostics topic + + Christian Henkel + BSD-3-Clause + + ament_copyright + ament_flake8 + + python3-pytest + ros2cli + python3-yaml + + ament_python + + diff --git a/ros2diagnostics_cli/resource/ros2diagnostics_cli b/ros2diagnostics_cli/resource/ros2diagnostics_cli new file mode 100644 index 000000000..e69de29bb diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py new file mode 100644 index 000000000..10616b095 --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -0,0 +1,222 @@ +# Copyright 2023 robobe +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the robobe nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from argparse import ArgumentParser +from enum import IntEnum +import pathlib +import re +from typing import Dict, List, TextIO, Tuple + +from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue + +import rclpy +from rclpy.qos import qos_profile_system_default + +import yaml + +TOPIC_DIAGNOSTICS = '/diagnostics' + +COLOR_DEFAULT = '\033[39m' +GREEN = '\033[92m' +RED = '\033[91m' +YELLOW = '\033[93m' + +level_to_str_mapping = { + b'\x00': ('OK', GREEN + 'OK' + COLOR_DEFAULT), + b'\x01': ('WARN', YELLOW + 'WARN' + COLOR_DEFAULT), + b'\x02': ('ERROR', RED + 'ERROR' + COLOR_DEFAULT), + b'\x03': ('STALE', RED + 'STALE' + COLOR_DEFAULT), +} + + +def convert_level_to_str(level: bytes) -> Tuple[str, str]: + return level_to_str_mapping[level]() + + +def open_file_for_output(csv_file: str) -> TextIO: + base_dir = pathlib.Path(csv_file).parents[0] + folder_exists = pathlib.Path(base_dir).is_dir() + if not folder_exists: + raise Exception( + f'Folder {csv_file} not exists' + ) # pylint: disable=[broad-exception-raised] + return open(csv_file, 'w', encoding='utf-8') + + +class ParserModeEnum(IntEnum): + LIST = 1 + CSV = 2 + SHOW = 3 + + +class DiagnosticsParser: + def __init__( + self, + mode: ParserModeEnum, + verbose=False, + levels=None, + run_once=False, + name_filter=None, + ) -> None: + self.__name_filter = name_filter + self.__status_render_handler = DiagnosticsParser.render + self.__mode = mode + self.__run_once = run_once + self.__verbose = verbose + self.__levels_info = ','.join(levels) if levels is not None else '' + self.__levels = DiagnosticsParser.map_level_from_name(levels) + + def set_render(self, handler): + self.__status_render_handler = handler + + def run(self): + self.register_diagnostics_topic() + + def __filter_level(self, level): + if not self.__levels: + return False + return level not in self.__levels + + @staticmethod + def map_level_from_name(levels: List[str]) -> List[bytes]: + b_levels = [] + if levels is None: + return b_levels + + map_levels = { + 'info': lambda: b_levels.append(b'\x00'), + 'warn': lambda: b_levels.append(b'\x01'), + 'error': lambda: b_levels.append(b'\x02'), + } + + for level in levels: + map_levels[level]() + return b_levels + + @staticmethod + def render(status: DiagnosticStatus, time_sec, verbose): + _, level_name = convert_level_to_str(status.level) + item = f'{status.name}: {level_name}, {status.message}' + print(item) + if verbose: + kv: KeyValue + for kv in status.values: + print(f'- {kv.key}={kv.value}') + + def diagnostics_status_handler(self, msg: DiagnosticArray) -> None: + """ + Filter DiagnosticStatus by level, name and node name. + + Args: + ---- + msg (DiagnosticArray): _description_ + + """ + counter: int = 0 + status: DiagnosticStatus + print(f'--- time: {msg.header.stamp.sec} ---') + for status in msg.status: + if self.__name_filter: + result = re.search(self.__name_filter, status.name) + if not result: + continue + if self.__filter_level(status.level): + continue + self.__status_render_handler( + status, msg.header.stamp.sec, self.__verbose) + counter += 1 + + if not counter: + print(f'No diagnostic for levels: {self.__levels_info}') + + def register_diagnostics_topic(self): + """Create ros node and subscribe to /diagnostic topic.""" + if self.__mode == ParserModeEnum.LIST: + handler = diagnostic_list_handler + else: + handler = self.diagnostics_status_handler + + rclpy.init() + node = rclpy.create_node('ros2diagnostics_cli_filter') + node.create_subscription( + DiagnosticArray, + TOPIC_DIAGNOSTICS, + handler, + qos_profile=qos_profile_system_default, + ) + try: + if self.__run_once: + rclpy.spin_once(node) + else: + rclpy.spin(node) + finally: + node.destroy_node() + rclpy.try_shutdown() + + +def diagnostic_list_handler(msg: DiagnosticArray) -> None: + """ + Print group data as yaml to stdout. + + Args: + ---- + msg (DiagnosticArray): /diagnostics topic message + + """ + status: DiagnosticStatus + data: Dict[str, List[str]] = {} + print(f'--- time: {msg.header.stamp.sec} ---') + for status in msg.status: + if ':' in status.name: + node, name = status.name.split(':') + else: + node = '---' + name = status.name + name = name.strip() + if node in data: + data[node].append(name) + else: + data[node] = [name] + + print(yaml.dump(data)) + + +def add_common_arguments(parser: ArgumentParser): + """Add common arguments for csv and show verbs.""" + parser.add_argument('-1', '--once', action='store_true', help='run only once') + parser.add_argument( + '-f', '--filter', type=str, help='filter diagnostic status name' + ) + parser.add_argument( + '-l', + '--levels', + action='append', + type=str, + choices=['info', 'warn', 'error'], + help='levels to filter, can be multiple times', + ) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/command/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/command/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py b/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py new file mode 100644 index 000000000..352973eac --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py @@ -0,0 +1,56 @@ +# Copyright 2023 robobe +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the robobe nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +from ros2cli.command import add_subparsers_on_demand +from ros2cli.command import CommandExtension + + +class DiagCommand(CommandExtension): + + def __init__(self): + super(DiagCommand, self).__init__() + self._subparser = None + + def add_arguments(self, parser, cli_name): + self._subparser = parser + add_subparsers_on_demand( + parser, + cli_name, + '_verb', + 'ros2diagnostics_cli.verb', + required=False) + + def main(self, *, _, args): + if not hasattr(args, '_verb'): + self._subparser.print_help() + return 0 + + extension = getattr(args, '_verb') + + return extension.main(args=args) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py new file mode 100644 index 000000000..956450296 --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -0,0 +1,113 @@ +# Copyright 2023 robobe +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the robobe nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +from typing import TextIO + +from diagnostic_msgs.msg import DiagnosticStatus, KeyValue + +from ros2cli.verb import VerbExtension +from ros2diagnostics_cli.api import ( + add_common_arguments, + convert_level_to_str, + DiagnosticsParser, + open_file_for_output, + ParserModeEnum +) + + +class CSVVerb(VerbExtension): + """Export /diagnostics message to csv file.""" + + def __init__(self): + super().__init__() + self.csv: TextIO = None + + def add_arguments(self, parser, _): + add_common_arguments(parser) + parser.add_argument( + '--output', + '-o', + type=str, + required=True, + help='export file full path' + ) + + parser.add_argument( + '--verbose', + '-v', + action='store_true', + help='export DiagnosticStatus values filed', + ) + + def render(self, status: DiagnosticStatus, time_sec, verbose=False): + level_name, _ = convert_level_to_str(status.level) + if ':' in status.name: + node, name = status.name.split(':') + else: + node = '---' + name = status.name + name = name.strip() + line = [ + str(time_sec), + level_name, + node, + name, + status.message, + status.hardware_id, + ] + if verbose: + kv: KeyValue + for kv in status.values: + line.append(kv.value) + + s_line = ','.join(line) + '\n' + print(s_line) + self.csv.write(s_line) + + def main(self, *, args): + if args.output: + try: + self.csv = open_file_for_output(args.output) + except Exception as error: + print(str(error)) + return + + try: + handler = DiagnosticsParser( + mode=ParserModeEnum.CSV, + verbose=args.verbose, + run_once=args.once, + name_filter=args.filter, + levels=args.levels, + ) + handler.set_render(self.render) + handler.run() + finally: + if self.csv: + self.csv.close() diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py new file mode 100644 index 000000000..2f7610bdf --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py @@ -0,0 +1,39 @@ +# Copyright 2023 robobe +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the robobe nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +from ros2cli.verb import VerbExtension +from ros2diagnostics_cli.api import DiagnosticsParser, ParserModeEnum + + +class ListVerb(VerbExtension): + """List all diagnostic status items group by node name.""" + + def main(self, *, args): + diagnostic_parser = DiagnosticsParser(ParserModeEnum.LIST) + diagnostic_parser.run() diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py new file mode 100644 index 000000000..1ee9d821c --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py @@ -0,0 +1,58 @@ +# Copyright 2023 robobe +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the robobe nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +from ros2cli.verb import VerbExtension +from ros2diagnostics_cli.api import ( + add_common_arguments, + DiagnosticsParser, + ParserModeEnum, +) + + +class ShowVerb(VerbExtension): + """Show diagnostics status item info.""" + + def add_arguments(self, parser, cli_name): + add_common_arguments(parser) + parser.add_argument( + '--verbose', + '-v', + action='store_true', + help='Display more info.', + ) + + def main(self, *, args): + diagnostic_parser = DiagnosticsParser( + mode=ParserModeEnum.SHOW, + verbose=args.verbose, + levels=args.levels, + run_once=args.once, + name_filter=args.filter, + ) + diagnostic_parser.run() diff --git a/ros2diagnostics_cli/setup.cfg b/ros2diagnostics_cli/setup.cfg new file mode 100644 index 000000000..3f0786bae --- /dev/null +++ b/ros2diagnostics_cli/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/ros2diagnostics_cli +[install] +install_scripts=$base/lib/ros2diagnostics_cli diff --git a/ros2diagnostics_cli/setup.py b/ros2diagnostics_cli/setup.py new file mode 100644 index 000000000..a08d428ea --- /dev/null +++ b/ros2diagnostics_cli/setup.py @@ -0,0 +1,33 @@ +from setuptools import find_packages +from setuptools import setup + +PACKAGE_NAME = 'ros2diagnostics_cli' + +setup( + name=PACKAGE_NAME, + version='3.1.2', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + PACKAGE_NAME]), + ('share/' + PACKAGE_NAME, ['package.xml']), + ], + install_requires=['ros2cli'], + zip_safe=True, + maintainer='user', + maintainer_email='robo2020@gmail.com', + description='diagnostic command for ROS2 command line, parse and show /diagnostics topic', + license='BSD-3-Clause', + tests_require=['pytest'], + entry_points={ + 'ros2cli.command': [ + 'diagnostics = ros2diagnostics_cli.command.diagnostics:DiagCommand', + ], + 'ros2diagnostics_cli.verb': [ + 'world = ros2diagnostics_cli.verb.world:WorldVerb', + 'csv = ros2diagnostics_cli.verb.csv:CSVVerb', + 'list = ros2diagnostics_cli.verb.list:ListVerb', + 'show = ros2diagnostics_cli.verb.show:ShowVerb' + ] + }, +) diff --git a/ros2diagnostics_cli/test/test_copyright.py b/ros2diagnostics_cli/test/test_copyright.py new file mode 100644 index 000000000..395f7f316 --- /dev/null +++ b/ros2diagnostics_cli/test/test_copyright.py @@ -0,0 +1,25 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_copyright.main import main +import pytest + + +# Remove the `skip` decorator once the source file(s) have a copyright header +# @pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/ros2diagnostics_cli/test/test_flake8.py b/ros2diagnostics_cli/test/test_flake8.py new file mode 100644 index 000000000..27ee1078f --- /dev/null +++ b/ros2diagnostics_cli/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors)