From 0262e0b36edaa4e2874e3b1209d1d8a3718a472f Mon Sep 17 00:00:00 2001 From: robo Date: Sat, 22 Apr 2023 22:57:45 +0300 Subject: [PATCH 01/23] ros2diagnostics cli init --- ros2diagnostics_cli/package.xml | 18 ++++++++++++ .../resource/ros2diagnostics_cli | 0 .../ros2diagnostics_cli/__init__.py | 0 .../ros2diagnostics_cli/command/__init__.py | 0 .../ros2diagnostics_cli/command/hello.py | 22 ++++++++++++++ .../ros2diagnostics_cli/verb/__init__.py | 0 .../ros2diagnostics_cli/verb/world.py | 8 +++++ ros2diagnostics_cli/setup.cfg | 4 +++ ros2diagnostics_cli/setup.py | 29 +++++++++++++++++++ ros2diagnostics_cli/test/test_copyright.py | 25 ++++++++++++++++ ros2diagnostics_cli/test/test_flake8.py | 25 ++++++++++++++++ ros2diagnostics_cli/test/test_pep257.py | 23 +++++++++++++++ 12 files changed, 154 insertions(+) create mode 100644 ros2diagnostics_cli/package.xml create mode 100644 ros2diagnostics_cli/resource/ros2diagnostics_cli create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/__init__.py create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/command/__init__.py create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/command/hello.py create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/verb/__init__.py create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py create mode 100644 ros2diagnostics_cli/setup.cfg create mode 100644 ros2diagnostics_cli/setup.py create mode 100644 ros2diagnostics_cli/test/test_copyright.py create mode 100644 ros2diagnostics_cli/test/test_flake8.py create mode 100644 ros2diagnostics_cli/test/test_pep257.py diff --git a/ros2diagnostics_cli/package.xml b/ros2diagnostics_cli/package.xml new file mode 100644 index 000000000..8d7b05094 --- /dev/null +++ b/ros2diagnostics_cli/package.xml @@ -0,0 +1,18 @@ + + + + ros2diagnostics_cli + 0.0.0 + TODO: Package description + user + TODO: License declaration + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + ros2cli + + 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/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/hello.py b/ros2diagnostics_cli/ros2diagnostics_cli/command/hello.py new file mode 100644 index 000000000..60914a03c --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/command/hello.py @@ -0,0 +1,22 @@ +from ros2cli.command import add_subparsers +from ros2cli.command import CommandExtension +from ros2cli.verb import get_verb_extensions + + +class HelloCommand(CommandExtension): + """The 'hello' command extension.""" + + def add_arguments(self, parser, cli_name): + self._subparser = parser + verb_extensions = get_verb_extensions('ros2diagnostics_cli.verb') + add_subparsers( + parser, cli_name, '_verb', verb_extensions, required=False) + + def main(self, *, parser, args): + if not hasattr(args, '_verb'): + self._subparser.print_help() + return 0 + + extension = getattr(args, '_verb') + + return extension.main(args=args) \ No newline at end of file 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/world.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py new file mode 100644 index 000000000..d517b469a --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py @@ -0,0 +1,8 @@ +from ros2cli.verb import VerbExtension + + +class WorldVerb(VerbExtension): + """Prints Hello World on the terminal.""" + + def main(self, *, args): + print("Hello, ROS 2 World!") 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..d4584971f --- /dev/null +++ b/ros2diagnostics_cli/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup + +package_name = 'ros2diagnostics_cli' + +setup( + name=package_name, + version='0.0.0', + packages=[package_name], + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='user', + maintainer_email='robo2020@gmail.com', + description='TODO: Package description', + license='TODO: License declaration', + tests_require=['pytest'], + entry_points={ + 'ros2cli.command': [ + 'hello = ros2diagnostics_cli.command.hello:HelloCommand', + ], + 'ros2diagnostics_cli.verb': [ + 'world = ros2diagnostics_cli.verb.world:WorldVerb', + ] + }, +) diff --git a/ros2diagnostics_cli/test/test_copyright.py b/ros2diagnostics_cli/test/test_copyright.py new file mode 100644 index 000000000..97a39196e --- /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) diff --git a/ros2diagnostics_cli/test/test_pep257.py b/ros2diagnostics_cli/test/test_pep257.py new file mode 100644 index 000000000..b234a3840 --- /dev/null +++ b/ros2diagnostics_cli/test/test_pep257.py @@ -0,0 +1,23 @@ +# 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_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings' From f177bed68abb223f662aac6d8085c2beee8c2208 Mon Sep 17 00:00:00 2001 From: robo Date: Sat, 22 Apr 2023 23:27:30 +0300 Subject: [PATCH 02/23] rename hello to diagnostics --- .../ros2diagnostics_cli/api/__init__.py | 5 +++++ .../command/diagnostics.py | 22 +++++++++++++++++++ .../ros2diagnostics_cli/command/hello.py | 22 ------------------- .../ros2diagnostics_cli/verb/world.py | 12 +++++++++- ros2diagnostics_cli/setup.py | 2 +- 5 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py delete mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/command/hello.py diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py new file mode 100644 index 000000000..032e5801e --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -0,0 +1,5 @@ +def get_hello_world(): + return 'Hello, ROS 2 World!' + +def get_hello_world_leet(): + return 'He110, R0S 2 W04ld!' \ No newline at end of file diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py b/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py new file mode 100644 index 000000000..52d6c84b0 --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py @@ -0,0 +1,22 @@ +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, *, parser, 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/command/hello.py b/ros2diagnostics_cli/ros2diagnostics_cli/command/hello.py deleted file mode 100644 index 60914a03c..000000000 --- a/ros2diagnostics_cli/ros2diagnostics_cli/command/hello.py +++ /dev/null @@ -1,22 +0,0 @@ -from ros2cli.command import add_subparsers -from ros2cli.command import CommandExtension -from ros2cli.verb import get_verb_extensions - - -class HelloCommand(CommandExtension): - """The 'hello' command extension.""" - - def add_arguments(self, parser, cli_name): - self._subparser = parser - verb_extensions = get_verb_extensions('ros2diagnostics_cli.verb') - add_subparsers( - parser, cli_name, '_verb', verb_extensions, required=False) - - def main(self, *, parser, args): - if not hasattr(args, '_verb'): - self._subparser.print_help() - return 0 - - extension = getattr(args, '_verb') - - return extension.main(args=args) \ No newline at end of file diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py index d517b469a..4eade5c9c 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py @@ -1,8 +1,18 @@ from ros2cli.verb import VerbExtension +from ros2diagnostics_cli.api import get_hello_world, get_hello_world_leet class WorldVerb(VerbExtension): """Prints Hello World on the terminal.""" + def add_arguments(self, parser, cli_name): + parser.add_argument( + "--leet", + "-l", + action="store_true", + help="Display the message in 'l33t' form.", + ) + def main(self, *, args): - print("Hello, ROS 2 World!") + message = get_hello_world() if not args.leet else get_hello_world_leet() + print(message) diff --git a/ros2diagnostics_cli/setup.py b/ros2diagnostics_cli/setup.py index d4584971f..3ca56a4f7 100644 --- a/ros2diagnostics_cli/setup.py +++ b/ros2diagnostics_cli/setup.py @@ -20,7 +20,7 @@ tests_require=['pytest'], entry_points={ 'ros2cli.command': [ - 'hello = ros2diagnostics_cli.command.hello:HelloCommand', + 'diagnostics = ros2diagnostics_cli.command.diagnostics:DiagCommand', ], 'ros2diagnostics_cli.verb': [ 'world = ros2diagnostics_cli.verb.world:WorldVerb', From c2a4669a30fd0755fe3223f04f9b600eb9463616 Mon Sep 17 00:00:00 2001 From: robo Date: Tue, 25 Apr 2023 15:00:54 +0300 Subject: [PATCH 03/23] add option with string csv --output --- .../ros2diagnostics_cli/api/__init__.py | 18 ++++++++++++++++-- .../ros2diagnostics_cli/verb/csv.py | 12 ++++++++++++ ros2diagnostics_cli/setup.py | 6 ++++-- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index 032e5801e..d758aefc2 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -1,5 +1,19 @@ +import rclpy +from diagnostic_msgs.msg import DiagnosticArray + +TOPIC_DIAGNOSTICS = "/diagnostics" + + +def diagnostic_handler(msg: DiagnosticArray) -> None: + print(msg) + + def get_hello_world(): - return 'Hello, ROS 2 World!' + rclpy.init() + node = rclpy.create_node("ros2diagnostics_cli_filter") + node.create_subscription(DiagnosticArray, TOPIC_DIAGNOSTICS, diagnostic_handler, 10) + rclpy.spin_once(node) + def get_hello_world_leet(): - return 'He110, R0S 2 W04ld!' \ No newline at end of file + return "He110, R0S 2 W04ld!" diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py new file mode 100644 index 000000000..88be28507 --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -0,0 +1,12 @@ +import click +from ros2cli.verb import VerbExtension +from ros2diagnostics_cli.api import get_hello_world, get_hello_world_leet + + +class CSVVerb(VerbExtension): + def add_arguments(self, parser, cli_name): + parser.add_argument("--output", "-o", type=str, help="export file to file") + + def main(self, *, args): + if args.output: + print(args.output) diff --git a/ros2diagnostics_cli/setup.py b/ros2diagnostics_cli/setup.py index 3ca56a4f7..5c6680f99 100644 --- a/ros2diagnostics_cli/setup.py +++ b/ros2diagnostics_cli/setup.py @@ -1,17 +1,18 @@ from setuptools import setup +from setuptools import find_packages package_name = 'ros2diagnostics_cli' setup( name=package_name, version='0.0.0', - packages=[package_name], + packages=find_packages(exclude=['test']), data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), ], - install_requires=['setuptools'], + install_requires=['ros2cli'], zip_safe=True, maintainer='user', maintainer_email='robo2020@gmail.com', @@ -24,6 +25,7 @@ ], 'ros2diagnostics_cli.verb': [ 'world = ros2diagnostics_cli.verb.world:WorldVerb', + 'csv = ros2diagnostics_cli.verb.csv:CSVVerb' ] }, ) From 6c7269a547cbd49601bf089ddeb85cfc0538702b Mon Sep 17 00:00:00 2001 From: robo Date: Tue, 25 Apr 2023 15:02:40 +0300 Subject: [PATCH 04/23] arg parse examples --- ros2diagnostics_cli/scripts/README.md | 100 ++++++++++++++++++ .../scripts/arg_parse_demo1.py | 6 ++ .../scripts/arg_parse_demo2.py | 6 ++ .../scripts/arg_parse_demo3.py | 16 +++ .../scripts/arg_parse_demo4.py | 23 ++++ .../scripts/arg_parse_demo4a.py | 17 +++ .../scripts/arg_parse_demo5.py | 12 +++ .../scripts/arg_parse_demo6.py | 25 +++++ .../scripts/arg_parse_demo7.py | 30 ++++++ 9 files changed, 235 insertions(+) create mode 100644 ros2diagnostics_cli/scripts/README.md create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo1.py create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo2.py create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo3.py create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo4.py create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo4a.py create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo5.py create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo6.py create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo7.py diff --git a/ros2diagnostics_cli/scripts/README.md b/ros2diagnostics_cli/scripts/README.md new file mode 100644 index 000000000..a71a90e1c --- /dev/null +++ b/ros2diagnostics_cli/scripts/README.md @@ -0,0 +1,100 @@ +Type: diagnostic_msgs/msg/DiagnosticArray + +``` +std_msgs/Header header # for timestamp + builtin_interfaces/Time stamp + int32 sec + uint32 nanosec + string frame_id +DiagnosticStatus[] status # an array of components being reported on + byte OK=0 + byte WARN=1 + byte ERROR=2 + byte STALE=3 + byte level + string name + string message + string hardware_id + KeyValue[] values + string key + string value + +``` + +```bash +# status info +--- +header: + stamp: + sec: 1682396036 + nanosec: 978628351 + frame_id: '' +status: +- level: "\0" + name: 'diagnostic_simple: DemoTask' + message: running + hardware_id: '' + values: [] +--- + +# status error +--- +header: + stamp: + sec: 1682396095 + nanosec: 590431434 + frame_id: '' +status: +- level: "\x02" + name: 'diagnostic_simple: DemoTask' + message: running + hardware_id: '' + values: [] +--- + +# status warning +--- +header: + stamp: + sec: 1682396159 + nanosec: 892125876 + frame_id: '' +status: +- level: "\x01" + name: 'diagnostic_simple: DemoTask' + message: running + hardware_id: '' + values: [] +--- + +``` + +## Add other data to Task +```python +def run(self, stat: DiagnosticStatusWrapper): + stat.summary(DiagnosticStatus.WARN, "running") + stat.add("key1", "val1") + stat.add("key2", "val2") + return stat +``` + +```bash +--- +header: + stamp: + sec: 1682396583 + nanosec: 587749252 + frame_id: '' +status: +- level: "\x01" + name: 'diagnostic_simple: DemoTask' + message: running + hardware_id: '' + values: + - key: key1 + value: val1 + - key: key2 + value: val2 +--- + +``` \ No newline at end of file diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo1.py b/ros2diagnostics_cli/scripts/arg_parse_demo1.py new file mode 100644 index 000000000..d178a2193 --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo1.py @@ -0,0 +1,6 @@ +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +parser.add_argument("echo", help="echo given string") +args: Namespace = parser.parse_args() +print(args.echo) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo2.py b/ros2diagnostics_cli/scripts/arg_parse_demo2.py new file mode 100644 index 000000000..00da0bfb5 --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo2.py @@ -0,0 +1,6 @@ +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +parser.add_argument("square", help="square given number", type=int) +args: Namespace = parser.parse_args() +print(args.square**2) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo3.py b/ros2diagnostics_cli/scripts/arg_parse_demo3.py new file mode 100644 index 000000000..17c02dd14 --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo3.py @@ -0,0 +1,16 @@ +""" Add positional argument +position argument: +- required: True/False +- action: boolean +""" +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +parser.add_argument("square", help="square given number", type=int) +parser.add_argument("-v", "--verbose", action="store_true", required=True, help="verbose opt") +args: Namespace = parser.parse_args() + +if args.verbose: + print(f"verbose mode {args.square**2}") +else: + print(args.square**2) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo4.py b/ros2diagnostics_cli/scripts/arg_parse_demo4.py new file mode 100644 index 000000000..36dbb1c1a --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo4.py @@ -0,0 +1,23 @@ +""" +- chose option +""" +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +parser.add_argument( + "-v", + "--verbose", + required=True, + help="verbose opt", + type=int, + choices=[1, 2, 3], +) + +args: Namespace = parser.parse_args() + +if args.verbose == 1: + print("mode 1") +elif args.verbose == 2: + print("mode 2") +elif args.verbose == 3: + print("mode 3") diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo4a.py b/ros2diagnostics_cli/scripts/arg_parse_demo4a.py new file mode 100644 index 000000000..5cba7ac6f --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo4a.py @@ -0,0 +1,17 @@ +""" +- chose option +""" +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +parser.add_argument( + "-o", + "--output", + help="output path", + type=str +) + +args: Namespace = parser.parse_args() + +if args.output: + print(args.output) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo5.py b/ros2diagnostics_cli/scripts/arg_parse_demo5.py new file mode 100644 index 000000000..e750147c7 --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo5.py @@ -0,0 +1,12 @@ +""" +- chose option +""" +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +parser.add_argument("square", help="square given", type=int, default=0, nargs="?") + +args: Namespace = parser.parse_args() +result: int = args.square**2 +print(result) + diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo6.py b/ros2diagnostics_cli/scripts/arg_parse_demo6.py new file mode 100644 index 000000000..5eb6ef1bc --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo6.py @@ -0,0 +1,25 @@ +""" +- option count +arg_parse_demo6 -v +arg_parse_demo6 -vv +arg_parse_demo6 --verbose --verbose +""" +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +parser.add_argument("square", help="square given", type=int, default=0, nargs="?") +parser.add_argument("-v", "--verbose", action="count", help="verbose, use -vv for more") +args: Namespace = parser.parse_args() +result: int = args.square**2 + +if args.verbose == 1: + print("verbose") +elif args.verbose == 2: + print("more verbose") +elif args.verbose > 2: + print("no extra verbode option max -vv") +else: + print("no verbose") + + + diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo7.py b/ros2diagnostics_cli/scripts/arg_parse_demo7.py new file mode 100644 index 000000000..94f568e47 --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo7.py @@ -0,0 +1,30 @@ +""" +- select argument + + +arg_parse_demo7 -v +arg_parse_demo7 -vv +arg_parse_demo7 -v -s +usage: arg_parse_demo7.py [-h] [-v | -s] [square] +""" +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +group = parser.add_mutually_exclusive_group() +parser.add_argument("square", help="square given", type=int, default=0, nargs="?") +group.add_argument("-v", "--verbose", action="count", help="verbose, use -vv for more") +group.add_argument("-s", "--silence", action="store_true", help="no verbose") +args: Namespace = parser.parse_args() +result: int = args.square**2 + +match args.verbose: + case 1: + print("verbose") + case 2: + print("more verbose") + case _: + print("") + + + + From 871095d99339bf6208e943cc4204c9d9deec1be8 Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 26 Apr 2023 08:55:19 +0300 Subject: [PATCH 05/23] add csv verb --- ros2diagnostics_cli/package.xml | 1 + .../ros2diagnostics_cli/api/__init__.py | 171 +++++++++++++++++- .../ros2diagnostics_cli/verb/csv.py | 50 ++++- .../ros2diagnostics_cli/verb/list.py | 11 ++ .../ros2diagnostics_cli/verb/show.py | 31 ++++ .../ros2diagnostics_cli/verb/world.py | 18 -- .../scripts/arg_parse_demo6str.py | 15 ++ .../scripts/simple_diag_reader.py | 34 ++++ .../scripts/simple_diagnostics.py | 50 +++++ .../scripts/yaml_demos/demo1.py | 12 ++ .../scripts/yaml_demos/example.yaml | 14 ++ ros2diagnostics_cli/setup.py | 4 +- ros2diagnostics_cli/test/test_csv.py | 8 + .../test/test_diagnostic_parse.py | 24 +++ 14 files changed, 410 insertions(+), 33 deletions(-) create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py create mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py delete mode 100644 ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py create mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo6str.py create mode 100644 ros2diagnostics_cli/scripts/simple_diag_reader.py create mode 100644 ros2diagnostics_cli/scripts/simple_diagnostics.py create mode 100644 ros2diagnostics_cli/scripts/yaml_demos/demo1.py create mode 100644 ros2diagnostics_cli/scripts/yaml_demos/example.yaml create mode 100644 ros2diagnostics_cli/test/test_csv.py create mode 100644 ros2diagnostics_cli/test/test_diagnostic_parse.py diff --git a/ros2diagnostics_cli/package.xml b/ros2diagnostics_cli/package.xml index 8d7b05094..90c8e0757 100644 --- a/ros2diagnostics_cli/package.xml +++ b/ros2diagnostics_cli/package.xml @@ -12,6 +12,7 @@ ament_pep257 python3-pytest ros2cli + python3-pyyaml ament_python diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index d758aefc2..48ef101b6 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -1,19 +1,170 @@ +"""_summary_ + +std_msgs/Header header # for timestamp +builtin_interfaces/Time stamp + int32 sec + uint32 nanosec +string frame_id +DiagnosticStatus[] status # an array of components being reported on +byte OK=0 +byte WARN=1 +byte ERROR=2 +byte STALE=3 +byte level +string name +string message +string hardware_id +KeyValue[] values + string key + string value +""" +from typing import Dict, List, Tuple, TextIO +import yaml import rclpy -from diagnostic_msgs.msg import DiagnosticArray +from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue +from rclpy.qos import qos_profile_system_default +from argparse import ArgumentParser +import pathlib +from enum import IntEnum TOPIC_DIAGNOSTICS = "/diagnostics" +COLOR_DEFAULT = "\033[39m" +GREEN = "\033[92m" +RED = "\033[91m" +YELLOW = "\033[93m" + + +def open_file_for_output(csv_file) -> 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") + f = open(csv_file, "w", encoding="utf-8") + return f + +class ParserModeEnum(IntEnum): + List = 1 + CSV = 2 + Show = 3 +class DiagnosticsParser: + def __init__(self, mode: ParserModeEnum, verbose=False, levels=None, run_once=False) -> None: + self.__status_render_handler = self.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_and_parse_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 + + for level in levels: + match level: + case "info": + b_levels.append(b"\x00") + case "warn": + b_levels.append(b"\x01") + case "error": + b_levels.append(b"\x02") + return b_levels + + + + @staticmethod + def convert_level_to_str(level) -> Tuple(str, str): + match level: + case b"\x00": + return ("OK", GREEN + "OK" + COLOR_DEFAULT) + case b"\x01": + return ("WARN", YELLOW + "WARN" + COLOR_DEFAULT) + case b"\x02": + return ("ERROR", RED + "ERROR" + COLOR_DEFAULT) + case b"\x03": + return ("STALE", RED + "STALE" + COLOR_DEFAULT) + case _: + return ("UNDEFINED", "UNDEFINED") + + def render(self, status: DiagnosticStatus, time_sec, verbose): + _, level_name = DiagnosticsParser.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: + counter: int = 0 + status: DiagnosticStatus + print(f"--- time: {msg.header.stamp.sec} ---") + for status in msg.status: + 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_and_parse_diagnostics_topic(self): + match self.__mode: + case ParserModeEnum.List: + handler = diagnostic_list_handler + case _ : + 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_handler(msg: DiagnosticArray) -> None: - print(msg) +def diagnostic_list_handler(msg: DiagnosticArray) -> None: + """Group diagnostics Task by node + print group data as yaml to stdout -def get_hello_world(): - rclpy.init() - node = rclpy.create_node("ros2diagnostics_cli_filter") - node.create_subscription(DiagnosticArray, TOPIC_DIAGNOSTICS, diagnostic_handler, 10) - rclpy.spin_once(node) + Args: + msg (DiagnosticArray): _description_ + """ + status: DiagnosticStatus + data: Dict[str, List[str]] = {} + print(f"--- time: {msg.header.stamp.sec} ---") + for status in msg.status: + node, name = status.name.split(":") + name = name.strip() + if node in data: + data[node].append(name) + else: + data[node] = [name] + print(yaml.dump(data)) -def get_hello_world_leet(): - return "He110, R0S 2 W04ld!" +def add_common_arguments(parser: ArgumentParser): + parser.add_argument("-1", "--once", action="store_true", help="run only once") diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index 88be28507..453763fff 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -1,12 +1,54 @@ -import click +from typing import TextIO from ros2cli.verb import VerbExtension -from ros2diagnostics_cli.api import get_hello_world, get_hello_world_leet +from ros2diagnostics_cli.api import ( + DiagnosticsParser, + open_file_for_output, + add_common_arguments, + ParserModeEnum, +) +from diagnostic_msgs.msg import DiagnosticStatus, KeyValue class CSVVerb(VerbExtension): + """export diagnostics message to csv file""" + def __init__(self): + super().__init__() + self.csv: TextIO = None + def add_arguments(self, parser, cli_name): - parser.add_argument("--output", "-o", type=str, help="export file to file") + add_common_arguments(parser) + parser.add_argument("--output", "-o", type=str, help="export file full path") + + def render(self, status: DiagnosticStatus, time_sec, verbose=False): + level_name, _ = DiagnosticsParser.convert_level_to_str(status.level) + line = [ + str(time_sec), + level_name, + status.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: - print(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, run_once=args.once) + 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..7eb98f0fe --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py @@ -0,0 +1,11 @@ +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..d0a7d2a7f --- /dev/null +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py @@ -0,0 +1,31 @@ +from ros2cli.verb import VerbExtension +from ros2diagnostics_cli.api import DiagnosticsParser, add_common_arguments, 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.", + ) + + parser.add_argument( + "-l", + "--levels", + action="append", + type=str, + choices=["info", "warn", "error"], + help="levels to filter, can be multiple times", + ) + + def main(self, *, args): + diagnostic_parser = DiagnosticsParser( + mode = ParserModeEnum.Show, + verbose=args.verbose, + levels=args.levels, + run_once=args.once) + diagnostic_parser.run() diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py deleted file mode 100644 index 4eade5c9c..000000000 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/world.py +++ /dev/null @@ -1,18 +0,0 @@ -from ros2cli.verb import VerbExtension -from ros2diagnostics_cli.api import get_hello_world, get_hello_world_leet - - -class WorldVerb(VerbExtension): - """Prints Hello World on the terminal.""" - - def add_arguments(self, parser, cli_name): - parser.add_argument( - "--leet", - "-l", - action="store_true", - help="Display the message in 'l33t' form.", - ) - - def main(self, *, args): - message = get_hello_world() if not args.leet else get_hello_world_leet() - print(message) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo6str.py b/ros2diagnostics_cli/scripts/arg_parse_demo6str.py new file mode 100644 index 000000000..70df3144c --- /dev/null +++ b/ros2diagnostics_cli/scripts/arg_parse_demo6str.py @@ -0,0 +1,15 @@ +""" +- option count +arg_parse_demo6 -v +arg_parse_demo6 -vv +arg_parse_demo6 --verbose --verbose +""" +from argparse import ArgumentParser, Namespace + +parser = ArgumentParser() +parser.add_argument( + "-l", "--levels", action="append", type=str, choices=["info", "warn", "error"], help="levels to filter" +) +args: Namespace = parser.parse_args() + +print(args.levels) diff --git a/ros2diagnostics_cli/scripts/simple_diag_reader.py b/ros2diagnostics_cli/scripts/simple_diag_reader.py new file mode 100644 index 000000000..7bddf93d6 --- /dev/null +++ b/ros2diagnostics_cli/scripts/simple_diag_reader.py @@ -0,0 +1,34 @@ +import rclpy +from rclpy.node import Node +from rclpy.qos import qos_profile_system_default +from diagnostic_msgs.msg import DiagnosticArray +from rosidl_runtime_py import message_to_csv + +class MyNode(Node): + def __init__(self): + node_name="simple_diag_reader" + super().__init__(node_name) + self.create_subscription(DiagnosticArray, "/diagnostics", self.__handler, qos_profile=qos_profile_system_default) + self.get_logger().info("Hello diagnostics parser") + + def __handler(self, msg: DiagnosticArray): + for status in msg.status: + to_print = message_to_csv( + status) + + print(to_print) + + +def main(): + rclpy.init() + node = MyNode() + try: + rclpy.spin(node) + except KeyboardInterrupt: + print("User exit") + finally: + node.destroy_node() + rclpy.try_shutdown() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/ros2diagnostics_cli/scripts/simple_diagnostics.py b/ros2diagnostics_cli/scripts/simple_diagnostics.py new file mode 100644 index 000000000..751a4ec92 --- /dev/null +++ b/ros2diagnostics_cli/scripts/simple_diagnostics.py @@ -0,0 +1,50 @@ +import rclpy +from rclpy.node import Node +from diagnostic_updater import (DiagnosticTask, + Updater, + DiagnosticStatusWrapper) +from diagnostic_msgs.msg import DiagnosticStatus + + +class DemoStatus(DiagnosticTask): + def __init__(self, name): + super().__init__(name) + + def run(self, stat: DiagnosticStatusWrapper): + stat.summary(DiagnosticStatus.WARN, "running") + stat.add("key1", "val1") + stat.add("key2", "val2") + return stat + +class Demo1Status(DiagnosticTask): + def __init__(self, name): + super().__init__(name) + + def run(self, stat: DiagnosticStatusWrapper): + stat.summary(DiagnosticStatus.ERROR, "bad") + return stat + +class MyNode(Node): + def __init__(self): + node_name = "diagnostic_simple" + super().__init__(node_name) + self.diag_update = Updater(self) + self.diag_update.add(DemoStatus("DemoTask")) + self.diag_update.add(Demo1Status("DemoTask2")) + self.get_logger().info("Hello ROS2") + + +def main(): + rclpy.init() + node = MyNode() + try: + rclpy.spin(node) + except KeyboardInterrupt: + print("User exit") + finally: + node.destroy_node() + rclpy.try_shutdown() + + +if __name__ == "__main__": + main() diff --git a/ros2diagnostics_cli/scripts/yaml_demos/demo1.py b/ros2diagnostics_cli/scripts/yaml_demos/demo1.py new file mode 100644 index 000000000..3aaf854e1 --- /dev/null +++ b/ros2diagnostics_cli/scripts/yaml_demos/demo1.py @@ -0,0 +1,12 @@ +import yaml +from pathlib import Path + +base_dir = Path(__file__).parents[0] + +file_path = Path.joinpath(base_dir, "example.yaml") +with open(file_path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + +print(data) + +print(yaml.dump(data)) diff --git a/ros2diagnostics_cli/scripts/yaml_demos/example.yaml b/ros2diagnostics_cli/scripts/yaml_demos/example.yaml new file mode 100644 index 000000000..65ca45cc8 --- /dev/null +++ b/ros2diagnostics_cli/scripts/yaml_demos/example.yaml @@ -0,0 +1,14 @@ +key: value +mylist: + - item1 + - item2 + - item3 + +my_dict: + key1: value1 + key2: value2 + +multi-line-str: | + hello world + this is a + multi line string \ No newline at end of file diff --git a/ros2diagnostics_cli/setup.py b/ros2diagnostics_cli/setup.py index 5c6680f99..5a9551c00 100644 --- a/ros2diagnostics_cli/setup.py +++ b/ros2diagnostics_cli/setup.py @@ -25,7 +25,9 @@ ], 'ros2diagnostics_cli.verb': [ 'world = ros2diagnostics_cli.verb.world:WorldVerb', - 'csv = ros2diagnostics_cli.verb.csv:CSVVerb' + '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_csv.py b/ros2diagnostics_cli/test/test_csv.py new file mode 100644 index 000000000..29cb08c4b --- /dev/null +++ b/ros2diagnostics_cli/test/test_csv.py @@ -0,0 +1,8 @@ +from ros2diagnostics_cli.api import open_file_for_output + + +def test_file_create(): + f = open_file_for_output("/tmp/1/a.csv") + assert f is not None + +test_file_create() \ No newline at end of file diff --git a/ros2diagnostics_cli/test/test_diagnostic_parse.py b/ros2diagnostics_cli/test/test_diagnostic_parse.py new file mode 100644 index 000000000..b4f1812ad --- /dev/null +++ b/ros2diagnostics_cli/test/test_diagnostic_parse.py @@ -0,0 +1,24 @@ +import unittest +import rclpy +from rclpy.executors import SingleThreadedExecutor + +TEST_NODE = "cli_diag_parse_test_node" +TEST_NAMESPACE = "cli_diag_parse" + + +class TestROS2DiagnosticParse(unittest.TestCase): + def setUp(self): + self.context = rclpy.context.Context() + rclpy.init(context=self.context) + self.node = rclpy.create_node( + TEST_NODE, namespace=TEST_NAMESPACE, context=self.context + ) + self.executor = SingleThreadedExecutor(context=self.context) + self.executor.add_node(self.node) + + def tearDown(self): + self.node.destroy_node() + rclpy.shutdown(context=self.context) + + def test_diagnostic_pub(self): + assert True From e9f680306ccffff781361b74f00c8d9c3e4407e4 Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 26 Apr 2023 20:22:51 +0300 Subject: [PATCH 06/23] version 0.0.1 --- ros2diagnostics_cli/README.md | 146 ++++++++++++++++++ .../ros2diagnostics_cli/api/__init__.py | 25 ++- .../ros2diagnostics_cli/verb/csv.py | 25 ++- .../ros2diagnostics_cli/verb/show.py | 14 +- ros2diagnostics_cli/scripts/reg_demo.py | 5 + ros2diagnostics_cli/setup.py | 2 +- 6 files changed, 200 insertions(+), 17 deletions(-) create mode 100644 ros2diagnostics_cli/README.md create mode 100644 ros2diagnostics_cli/scripts/reg_demo.py diff --git a/ros2diagnostics_cli/README.md b/ros2diagnostics_cli/README.md new file mode 100644 index 000000000..fa411f2d3 --- /dev/null +++ b/ros2diagnostics_cli/README.md @@ -0,0 +1,146 @@ +# 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 \ No newline at end of file diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index 48ef101b6..e943d096c 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -25,6 +25,7 @@ from rclpy.qos import qos_profile_system_default from argparse import ArgumentParser import pathlib +import re from enum import IntEnum TOPIC_DIAGNOSTICS = "/diagnostics" @@ -48,7 +49,8 @@ class ParserModeEnum(IntEnum): CSV = 2 Show = 3 class DiagnosticsParser: - def __init__(self, mode: ParserModeEnum, verbose=False, levels=None, run_once=False) -> None: + 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 = self.render self.__mode = mode self.__run_once = run_once @@ -86,7 +88,7 @@ def map_level_from_name(levels: List[str]) -> List[bytes]: @staticmethod - def convert_level_to_str(level) -> Tuple(str, str): + def convert_level_to_str(level) -> Tuple[str, str]: match level: case b"\x00": return ("OK", GREEN + "OK" + COLOR_DEFAULT) @@ -109,10 +111,20 @@ def render(self, status: DiagnosticStatus, time_sec, verbose): print(f"- {kv.key}={kv.value}") def diagnostics_status_handler(self, msg: DiagnosticArray) -> None: + """Run handler for each DiagnosticStatus in array + filter status by level, name, 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) @@ -168,3 +180,12 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: def add_common_arguments(parser: ArgumentParser): 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/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index 453763fff..9f4855a03 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -10,21 +10,32 @@ class CSVVerb(VerbExtension): - """export diagnostics message to csv file""" + """export /diagnostics message to csv file""" + def __init__(self): super().__init__() self.csv: TextIO = None def add_arguments(self, parser, cli_name): add_common_arguments(parser) - parser.add_argument("--output", "-o", type=str, help="export file full path") + 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, _ = DiagnosticsParser.convert_level_to_str(status.level) + node_name, name = status.name.split(":") + name = name.strip() line = [ str(time_sec), level_name, - status.name, + node_name, + name, status.message, status.hardware_id, ] @@ -46,7 +57,13 @@ def main(self, *, args): return try: - handler = DiagnosticsParser(mode=ParserModeEnum.CSV, run_once=args.once) + 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: diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py index d0a7d2a7f..49fa21305 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py @@ -13,19 +13,13 @@ def add_arguments(self, parser, cli_name): help="Display more info.", ) - parser.add_argument( - "-l", - "--levels", - action="append", - type=str, - choices=["info", "warn", "error"], - help="levels to filter, can be multiple times", - ) + def main(self, *, args): diagnostic_parser = DiagnosticsParser( mode = ParserModeEnum.Show, verbose=args.verbose, - levels=args.levels, - run_once=args.once) + levels=args.levels, + run_once=args.once, + name_filter=args.filter) diagnostic_parser.run() diff --git a/ros2diagnostics_cli/scripts/reg_demo.py b/ros2diagnostics_cli/scripts/reg_demo.py new file mode 100644 index 000000000..dbb6e81c0 --- /dev/null +++ b/ros2diagnostics_cli/scripts/reg_demo.py @@ -0,0 +1,5 @@ +import re + +txt = "node_name: DemoTask2" +x = re.search("name", txt) +print(x) \ No newline at end of file diff --git a/ros2diagnostics_cli/setup.py b/ros2diagnostics_cli/setup.py index 5a9551c00..925bd51e1 100644 --- a/ros2diagnostics_cli/setup.py +++ b/ros2diagnostics_cli/setup.py @@ -5,7 +5,7 @@ setup( name=package_name, - version='0.0.0', + version='0.0.1', packages=find_packages(exclude=['test']), data_files=[ ('share/ament_index/resource_index/packages', From c4fd1028579cd72a12bf5e83bb19cd5aaefbf632 Mon Sep 17 00:00:00 2001 From: robobe <3914144+robobe@users.noreply.github.com> Date: Wed, 26 Apr 2023 22:29:40 +0300 Subject: [PATCH 07/23] Update ros2diagnostics_cli/package.xml Co-authored-by: Christian Henkel <6976069+ct2034@users.noreply.github.com> --- ros2diagnostics_cli/package.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ros2diagnostics_cli/package.xml b/ros2diagnostics_cli/package.xml index 90c8e0757..387b54ce7 100644 --- a/ros2diagnostics_cli/package.xml +++ b/ros2diagnostics_cli/package.xml @@ -12,7 +12,7 @@ ament_pep257 python3-pytest ros2cli - python3-pyyaml + python3-yaml ament_python From c4175a70443e3ce305d4c7ddcfdf8d0e1cec9b02 Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 26 Apr 2023 22:50:08 +0300 Subject: [PATCH 08/23] clean test and scripts file --- ros2diagnostics_cli/scripts/README.md | 100 ------------------ .../scripts/arg_parse_demo1.py | 6 -- .../scripts/arg_parse_demo2.py | 6 -- .../scripts/arg_parse_demo3.py | 16 --- .../scripts/arg_parse_demo4.py | 23 ---- .../scripts/arg_parse_demo4a.py | 17 --- .../scripts/arg_parse_demo5.py | 12 --- .../scripts/arg_parse_demo6.py | 25 ----- .../scripts/arg_parse_demo6str.py | 15 --- .../scripts/arg_parse_demo7.py | 30 ------ ros2diagnostics_cli/scripts/reg_demo.py | 5 - .../scripts/simple_diag_reader.py | 34 ------ .../scripts/simple_diagnostics.py | 50 --------- .../scripts/yaml_demos/demo1.py | 12 --- .../scripts/yaml_demos/example.yaml | 14 --- ros2diagnostics_cli/test/test_csv.py | 8 -- .../test/test_diagnostic_parse.py | 24 ----- 17 files changed, 397 deletions(-) delete mode 100644 ros2diagnostics_cli/scripts/README.md delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo1.py delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo2.py delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo3.py delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo4.py delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo4a.py delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo5.py delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo6.py delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo6str.py delete mode 100644 ros2diagnostics_cli/scripts/arg_parse_demo7.py delete mode 100644 ros2diagnostics_cli/scripts/reg_demo.py delete mode 100644 ros2diagnostics_cli/scripts/simple_diag_reader.py delete mode 100644 ros2diagnostics_cli/scripts/simple_diagnostics.py delete mode 100644 ros2diagnostics_cli/scripts/yaml_demos/demo1.py delete mode 100644 ros2diagnostics_cli/scripts/yaml_demos/example.yaml delete mode 100644 ros2diagnostics_cli/test/test_csv.py delete mode 100644 ros2diagnostics_cli/test/test_diagnostic_parse.py diff --git a/ros2diagnostics_cli/scripts/README.md b/ros2diagnostics_cli/scripts/README.md deleted file mode 100644 index a71a90e1c..000000000 --- a/ros2diagnostics_cli/scripts/README.md +++ /dev/null @@ -1,100 +0,0 @@ -Type: diagnostic_msgs/msg/DiagnosticArray - -``` -std_msgs/Header header # for timestamp - builtin_interfaces/Time stamp - int32 sec - uint32 nanosec - string frame_id -DiagnosticStatus[] status # an array of components being reported on - byte OK=0 - byte WARN=1 - byte ERROR=2 - byte STALE=3 - byte level - string name - string message - string hardware_id - KeyValue[] values - string key - string value - -``` - -```bash -# status info ---- -header: - stamp: - sec: 1682396036 - nanosec: 978628351 - frame_id: '' -status: -- level: "\0" - name: 'diagnostic_simple: DemoTask' - message: running - hardware_id: '' - values: [] ---- - -# status error ---- -header: - stamp: - sec: 1682396095 - nanosec: 590431434 - frame_id: '' -status: -- level: "\x02" - name: 'diagnostic_simple: DemoTask' - message: running - hardware_id: '' - values: [] ---- - -# status warning ---- -header: - stamp: - sec: 1682396159 - nanosec: 892125876 - frame_id: '' -status: -- level: "\x01" - name: 'diagnostic_simple: DemoTask' - message: running - hardware_id: '' - values: [] ---- - -``` - -## Add other data to Task -```python -def run(self, stat: DiagnosticStatusWrapper): - stat.summary(DiagnosticStatus.WARN, "running") - stat.add("key1", "val1") - stat.add("key2", "val2") - return stat -``` - -```bash ---- -header: - stamp: - sec: 1682396583 - nanosec: 587749252 - frame_id: '' -status: -- level: "\x01" - name: 'diagnostic_simple: DemoTask' - message: running - hardware_id: '' - values: - - key: key1 - value: val1 - - key: key2 - value: val2 ---- - -``` \ No newline at end of file diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo1.py b/ros2diagnostics_cli/scripts/arg_parse_demo1.py deleted file mode 100644 index d178a2193..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo1.py +++ /dev/null @@ -1,6 +0,0 @@ -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -parser.add_argument("echo", help="echo given string") -args: Namespace = parser.parse_args() -print(args.echo) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo2.py b/ros2diagnostics_cli/scripts/arg_parse_demo2.py deleted file mode 100644 index 00da0bfb5..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo2.py +++ /dev/null @@ -1,6 +0,0 @@ -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -parser.add_argument("square", help="square given number", type=int) -args: Namespace = parser.parse_args() -print(args.square**2) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo3.py b/ros2diagnostics_cli/scripts/arg_parse_demo3.py deleted file mode 100644 index 17c02dd14..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo3.py +++ /dev/null @@ -1,16 +0,0 @@ -""" Add positional argument -position argument: -- required: True/False -- action: boolean -""" -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -parser.add_argument("square", help="square given number", type=int) -parser.add_argument("-v", "--verbose", action="store_true", required=True, help="verbose opt") -args: Namespace = parser.parse_args() - -if args.verbose: - print(f"verbose mode {args.square**2}") -else: - print(args.square**2) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo4.py b/ros2diagnostics_cli/scripts/arg_parse_demo4.py deleted file mode 100644 index 36dbb1c1a..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo4.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -- chose option -""" -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -parser.add_argument( - "-v", - "--verbose", - required=True, - help="verbose opt", - type=int, - choices=[1, 2, 3], -) - -args: Namespace = parser.parse_args() - -if args.verbose == 1: - print("mode 1") -elif args.verbose == 2: - print("mode 2") -elif args.verbose == 3: - print("mode 3") diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo4a.py b/ros2diagnostics_cli/scripts/arg_parse_demo4a.py deleted file mode 100644 index 5cba7ac6f..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo4a.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -- chose option -""" -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -parser.add_argument( - "-o", - "--output", - help="output path", - type=str -) - -args: Namespace = parser.parse_args() - -if args.output: - print(args.output) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo5.py b/ros2diagnostics_cli/scripts/arg_parse_demo5.py deleted file mode 100644 index e750147c7..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo5.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -- chose option -""" -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -parser.add_argument("square", help="square given", type=int, default=0, nargs="?") - -args: Namespace = parser.parse_args() -result: int = args.square**2 -print(result) - diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo6.py b/ros2diagnostics_cli/scripts/arg_parse_demo6.py deleted file mode 100644 index 5eb6ef1bc..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo6.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -- option count -arg_parse_demo6 -v -arg_parse_demo6 -vv -arg_parse_demo6 --verbose --verbose -""" -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -parser.add_argument("square", help="square given", type=int, default=0, nargs="?") -parser.add_argument("-v", "--verbose", action="count", help="verbose, use -vv for more") -args: Namespace = parser.parse_args() -result: int = args.square**2 - -if args.verbose == 1: - print("verbose") -elif args.verbose == 2: - print("more verbose") -elif args.verbose > 2: - print("no extra verbode option max -vv") -else: - print("no verbose") - - - diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo6str.py b/ros2diagnostics_cli/scripts/arg_parse_demo6str.py deleted file mode 100644 index 70df3144c..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo6str.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -- option count -arg_parse_demo6 -v -arg_parse_demo6 -vv -arg_parse_demo6 --verbose --verbose -""" -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -parser.add_argument( - "-l", "--levels", action="append", type=str, choices=["info", "warn", "error"], help="levels to filter" -) -args: Namespace = parser.parse_args() - -print(args.levels) diff --git a/ros2diagnostics_cli/scripts/arg_parse_demo7.py b/ros2diagnostics_cli/scripts/arg_parse_demo7.py deleted file mode 100644 index 94f568e47..000000000 --- a/ros2diagnostics_cli/scripts/arg_parse_demo7.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -- select argument - - -arg_parse_demo7 -v -arg_parse_demo7 -vv -arg_parse_demo7 -v -s -usage: arg_parse_demo7.py [-h] [-v | -s] [square] -""" -from argparse import ArgumentParser, Namespace - -parser = ArgumentParser() -group = parser.add_mutually_exclusive_group() -parser.add_argument("square", help="square given", type=int, default=0, nargs="?") -group.add_argument("-v", "--verbose", action="count", help="verbose, use -vv for more") -group.add_argument("-s", "--silence", action="store_true", help="no verbose") -args: Namespace = parser.parse_args() -result: int = args.square**2 - -match args.verbose: - case 1: - print("verbose") - case 2: - print("more verbose") - case _: - print("") - - - - diff --git a/ros2diagnostics_cli/scripts/reg_demo.py b/ros2diagnostics_cli/scripts/reg_demo.py deleted file mode 100644 index dbb6e81c0..000000000 --- a/ros2diagnostics_cli/scripts/reg_demo.py +++ /dev/null @@ -1,5 +0,0 @@ -import re - -txt = "node_name: DemoTask2" -x = re.search("name", txt) -print(x) \ No newline at end of file diff --git a/ros2diagnostics_cli/scripts/simple_diag_reader.py b/ros2diagnostics_cli/scripts/simple_diag_reader.py deleted file mode 100644 index 7bddf93d6..000000000 --- a/ros2diagnostics_cli/scripts/simple_diag_reader.py +++ /dev/null @@ -1,34 +0,0 @@ -import rclpy -from rclpy.node import Node -from rclpy.qos import qos_profile_system_default -from diagnostic_msgs.msg import DiagnosticArray -from rosidl_runtime_py import message_to_csv - -class MyNode(Node): - def __init__(self): - node_name="simple_diag_reader" - super().__init__(node_name) - self.create_subscription(DiagnosticArray, "/diagnostics", self.__handler, qos_profile=qos_profile_system_default) - self.get_logger().info("Hello diagnostics parser") - - def __handler(self, msg: DiagnosticArray): - for status in msg.status: - to_print = message_to_csv( - status) - - print(to_print) - - -def main(): - rclpy.init() - node = MyNode() - try: - rclpy.spin(node) - except KeyboardInterrupt: - print("User exit") - finally: - node.destroy_node() - rclpy.try_shutdown() - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/ros2diagnostics_cli/scripts/simple_diagnostics.py b/ros2diagnostics_cli/scripts/simple_diagnostics.py deleted file mode 100644 index 751a4ec92..000000000 --- a/ros2diagnostics_cli/scripts/simple_diagnostics.py +++ /dev/null @@ -1,50 +0,0 @@ -import rclpy -from rclpy.node import Node -from diagnostic_updater import (DiagnosticTask, - Updater, - DiagnosticStatusWrapper) -from diagnostic_msgs.msg import DiagnosticStatus - - -class DemoStatus(DiagnosticTask): - def __init__(self, name): - super().__init__(name) - - def run(self, stat: DiagnosticStatusWrapper): - stat.summary(DiagnosticStatus.WARN, "running") - stat.add("key1", "val1") - stat.add("key2", "val2") - return stat - -class Demo1Status(DiagnosticTask): - def __init__(self, name): - super().__init__(name) - - def run(self, stat: DiagnosticStatusWrapper): - stat.summary(DiagnosticStatus.ERROR, "bad") - return stat - -class MyNode(Node): - def __init__(self): - node_name = "diagnostic_simple" - super().__init__(node_name) - self.diag_update = Updater(self) - self.diag_update.add(DemoStatus("DemoTask")) - self.diag_update.add(Demo1Status("DemoTask2")) - self.get_logger().info("Hello ROS2") - - -def main(): - rclpy.init() - node = MyNode() - try: - rclpy.spin(node) - except KeyboardInterrupt: - print("User exit") - finally: - node.destroy_node() - rclpy.try_shutdown() - - -if __name__ == "__main__": - main() diff --git a/ros2diagnostics_cli/scripts/yaml_demos/demo1.py b/ros2diagnostics_cli/scripts/yaml_demos/demo1.py deleted file mode 100644 index 3aaf854e1..000000000 --- a/ros2diagnostics_cli/scripts/yaml_demos/demo1.py +++ /dev/null @@ -1,12 +0,0 @@ -import yaml -from pathlib import Path - -base_dir = Path(__file__).parents[0] - -file_path = Path.joinpath(base_dir, "example.yaml") -with open(file_path, "r", encoding="utf-8") as f: - data = yaml.safe_load(f) - -print(data) - -print(yaml.dump(data)) diff --git a/ros2diagnostics_cli/scripts/yaml_demos/example.yaml b/ros2diagnostics_cli/scripts/yaml_demos/example.yaml deleted file mode 100644 index 65ca45cc8..000000000 --- a/ros2diagnostics_cli/scripts/yaml_demos/example.yaml +++ /dev/null @@ -1,14 +0,0 @@ -key: value -mylist: - - item1 - - item2 - - item3 - -my_dict: - key1: value1 - key2: value2 - -multi-line-str: | - hello world - this is a - multi line string \ No newline at end of file diff --git a/ros2diagnostics_cli/test/test_csv.py b/ros2diagnostics_cli/test/test_csv.py deleted file mode 100644 index 29cb08c4b..000000000 --- a/ros2diagnostics_cli/test/test_csv.py +++ /dev/null @@ -1,8 +0,0 @@ -from ros2diagnostics_cli.api import open_file_for_output - - -def test_file_create(): - f = open_file_for_output("/tmp/1/a.csv") - assert f is not None - -test_file_create() \ No newline at end of file diff --git a/ros2diagnostics_cli/test/test_diagnostic_parse.py b/ros2diagnostics_cli/test/test_diagnostic_parse.py deleted file mode 100644 index b4f1812ad..000000000 --- a/ros2diagnostics_cli/test/test_diagnostic_parse.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest -import rclpy -from rclpy.executors import SingleThreadedExecutor - -TEST_NODE = "cli_diag_parse_test_node" -TEST_NAMESPACE = "cli_diag_parse" - - -class TestROS2DiagnosticParse(unittest.TestCase): - def setUp(self): - self.context = rclpy.context.Context() - rclpy.init(context=self.context) - self.node = rclpy.create_node( - TEST_NODE, namespace=TEST_NAMESPACE, context=self.context - ) - self.executor = SingleThreadedExecutor(context=self.context) - self.executor.add_node(self.node) - - def tearDown(self): - self.node.destroy_node() - rclpy.shutdown(context=self.context) - - def test_diagnostic_pub(self): - assert True From 2b13ff7716f08a347077d3948668b60e77350b1e Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 26 Apr 2023 22:52:44 +0300 Subject: [PATCH 09/23] add copyright --- .../ros2diagnostics_cli/api/__init__.py | 29 +++++++++++++++++++ .../command/diagnostics.py | 29 +++++++++++++++++++ .../ros2diagnostics_cli/verb/csv.py | 29 +++++++++++++++++++ .../ros2diagnostics_cli/verb/list.py | 29 +++++++++++++++++++ .../ros2diagnostics_cli/verb/show.py | 29 +++++++++++++++++++ 5 files changed, 145 insertions(+) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index e943d096c..255b17c66 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -1,3 +1,32 @@ +# 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. + + """_summary_ std_msgs/Header header # for timestamp diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py b/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py index 52d6c84b0..577f02aea 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py @@ -1,3 +1,32 @@ +# 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 diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index 9f4855a03..db4483476 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -1,3 +1,32 @@ +# 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 ros2cli.verb import VerbExtension from ros2diagnostics_cli.api import ( diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py index 7eb98f0fe..5fac53280 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py @@ -1,3 +1,32 @@ +# 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 diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py index 49fa21305..5278bf7bb 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py @@ -1,3 +1,32 @@ +# 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, add_common_arguments, ParserModeEnum From 8124d0805e7fd4641ac6ae4e8a6387c33847e97f Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 26 Apr 2023 23:02:41 +0300 Subject: [PATCH 10/23] fix: support python 3.8 remove match --- .../ros2diagnostics_cli/api/__init__.py | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index 255b17c66..b13665c0c 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -73,12 +73,22 @@ def open_file_for_output(csv_file) -> TextIO: f = open(csv_file, "w", encoding="utf-8") return f + 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: + 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 = self.render self.__mode = mode @@ -104,18 +114,16 @@ def map_level_from_name(levels: List[str]) -> List[bytes]: 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: - match level: - case "info": - b_levels.append(b"\x00") - case "warn": - b_levels.append(b"\x01") - case "error": - b_levels.append(b"\x02") + map_levels[level]() return b_levels - - @staticmethod def convert_level_to_str(level) -> Tuple[str, str]: match level: @@ -166,9 +174,9 @@ def register_and_parse_diagnostics_topic(self): match self.__mode: case ParserModeEnum.List: handler = diagnostic_list_handler - case _ : + case _: handler = self.diagnostics_status_handler - + rclpy.init() node = rclpy.create_node("ros2diagnostics_cli_filter") node.create_subscription( @@ -207,14 +215,17 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: print(yaml.dump(data)) + def add_common_arguments(parser: ArgumentParser): 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", - ) + "-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", + ) From 064d3f3afe47f0a4c6ad98ba40e61a3342fc2ebd Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 26 Apr 2023 23:19:05 +0300 Subject: [PATCH 11/23] format and run linters --- ros2diagnostics_cli/package.xml | 2 +- .../ros2diagnostics_cli/verb/csv.py | 6 +++-- .../ros2diagnostics_cli/verb/list.py | 3 +-- .../ros2diagnostics_cli/verb/show.py | 14 +++++++---- ros2diagnostics_cli/test/test_copyright.py | 2 +- ros2diagnostics_cli/test/test_pep257.py | 23 ------------------- 6 files changed, 16 insertions(+), 34 deletions(-) delete mode 100644 ros2diagnostics_cli/test/test_pep257.py diff --git a/ros2diagnostics_cli/package.xml b/ros2diagnostics_cli/package.xml index 387b54ce7..4b1d75b4e 100644 --- a/ros2diagnostics_cli/package.xml +++ b/ros2diagnostics_cli/package.xml @@ -9,7 +9,7 @@ ament_copyright ament_flake8 - ament_pep257 + python3-pytest ros2cli python3-yaml diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index db4483476..8454993ae 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -47,7 +47,9 @@ def __init__(self): def add_arguments(self, parser, cli_name): add_common_arguments(parser) - parser.add_argument("--output", "-o", type=str, required=True, help="export file full path") + parser.add_argument( + "--output", "-o", type=str, required=True, help="export file full path" + ) parser.add_argument( "--verbose", @@ -91,7 +93,7 @@ def main(self, *, args): verbose=args.verbose, run_once=args.once, name_filter=args.filter, - levels=args.levels + levels=args.levels, ) handler.set_render(self.render) handler.run() diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py index 5fac53280..8b9f7541a 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py @@ -30,11 +30,10 @@ 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 index 5278bf7bb..c58aee398 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py @@ -28,11 +28,16 @@ from ros2cli.verb import VerbExtension -from ros2diagnostics_cli.api import DiagnosticsParser, add_common_arguments, ParserModeEnum +from ros2diagnostics_cli.api import ( + DiagnosticsParser, + add_common_arguments, + ParserModeEnum, +) class ShowVerb(VerbExtension): """Show diagnostics status item info""" + def add_arguments(self, parser, cli_name): add_common_arguments(parser) parser.add_argument( @@ -42,13 +47,12 @@ def add_arguments(self, parser, cli_name): help="Display more info.", ) - - def main(self, *, args): diagnostic_parser = DiagnosticsParser( - mode = ParserModeEnum.Show, + mode=ParserModeEnum.Show, verbose=args.verbose, levels=args.levels, run_once=args.once, - name_filter=args.filter) + name_filter=args.filter, + ) diagnostic_parser.run() diff --git a/ros2diagnostics_cli/test/test_copyright.py b/ros2diagnostics_cli/test/test_copyright.py index 97a39196e..395f7f316 100644 --- a/ros2diagnostics_cli/test/test_copyright.py +++ b/ros2diagnostics_cli/test/test_copyright.py @@ -17,7 +17,7 @@ # 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.skip(reason='No copyright header has been placed in the generated source file.') @pytest.mark.copyright @pytest.mark.linter def test_copyright(): diff --git a/ros2diagnostics_cli/test/test_pep257.py b/ros2diagnostics_cli/test/test_pep257.py deleted file mode 100644 index b234a3840..000000000 --- a/ros2diagnostics_cli/test/test_pep257.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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_pep257.main import main -import pytest - - -@pytest.mark.linter -@pytest.mark.pep257 -def test_pep257(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found code style errors / warnings' From c4c08f0fe12b8d258a6199d61e275479e6c2f07f Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 26 Apr 2023 23:44:54 +0300 Subject: [PATCH 12/23] fix: suport python 3.8 remove math case remove some of pylint warnings --- .../ros2diagnostics_cli/api/__init__.py | 55 +++++++++---------- .../ros2diagnostics_cli/verb/csv.py | 3 +- .../ros2diagnostics_cli/verb/list.py | 2 +- .../ros2diagnostics_cli/verb/show.py | 2 +- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index b13665c0c..392e50af8 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -47,15 +47,16 @@ string key string value """ -from typing import Dict, List, Tuple, TextIO -import yaml -import rclpy -from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue -from rclpy.qos import qos_profile_system_default -from argparse import ArgumentParser import pathlib import re +from argparse import ArgumentParser from enum import IntEnum +from typing import Dict, List, TextIO, Tuple + +import rclpy +import yaml +from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue +from rclpy.qos import qos_profile_system_default TOPIC_DIAGNOSTICS = "/diagnostics" @@ -64,6 +65,16 @@ RED = "\033[91m" YELLOW = "\033[93m" +level_to_str_mapping = { + b"\x00": lambda: ("OK", GREEN + "OK" + COLOR_DEFAULT), + b"\x01": lambda: ("WARN", YELLOW + "WARN" + COLOR_DEFAULT), + b"\x02": lambda: ("ERROR", RED + "ERROR" + COLOR_DEFAULT), + b"\x03": lambda: ("STALE", RED + "STALE" + COLOR_DEFAULT) +} + +def convert_level_to_str(level) -> Tuple[str, str]: + return level_to_str_mapping[level]() + def open_file_for_output(csv_file) -> TextIO: base_dir = pathlib.Path(csv_file).parents[0] @@ -75,9 +86,9 @@ def open_file_for_output(csv_file) -> TextIO: class ParserModeEnum(IntEnum): - List = 1 + LIST = 1 CSV = 2 - Show = 3 + SHOW = 3 class DiagnosticsParser: @@ -90,7 +101,7 @@ def __init__( name_filter=None, ) -> None: self.__name_filter = name_filter - self.__status_render_handler = self.render + self.__status_render_handler = DiagnosticsParser.render self.__mode = mode self.__run_once = run_once self.__verbose = verbose @@ -125,21 +136,8 @@ def map_level_from_name(levels: List[str]) -> List[bytes]: return b_levels @staticmethod - def convert_level_to_str(level) -> Tuple[str, str]: - match level: - case b"\x00": - return ("OK", GREEN + "OK" + COLOR_DEFAULT) - case b"\x01": - return ("WARN", YELLOW + "WARN" + COLOR_DEFAULT) - case b"\x02": - return ("ERROR", RED + "ERROR" + COLOR_DEFAULT) - case b"\x03": - return ("STALE", RED + "STALE" + COLOR_DEFAULT) - case _: - return ("UNDEFINED", "UNDEFINED") - - def render(self, status: DiagnosticStatus, time_sec, verbose): - _, level_name = DiagnosticsParser.convert_level_to_str(status.level) + 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: @@ -171,11 +169,10 @@ def diagnostics_status_handler(self, msg: DiagnosticArray) -> None: print(f"No diagnostic for levels: {self.__levels_info}") def register_and_parse_diagnostics_topic(self): - match self.__mode: - case ParserModeEnum.List: - handler = diagnostic_list_handler - case _: - handler = self.diagnostics_status_handler + if self.__mode == ParserModeEnum.LIST: + handler = diagnostic_list_handler + else: + handler = self.diagnostics_status_handler rclpy.init() node = rclpy.create_node("ros2diagnostics_cli_filter") diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index 8454993ae..2c02e021a 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -34,6 +34,7 @@ open_file_for_output, add_common_arguments, ParserModeEnum, + convert_level_to_str ) from diagnostic_msgs.msg import DiagnosticStatus, KeyValue @@ -59,7 +60,7 @@ def add_arguments(self, parser, cli_name): ) def render(self, status: DiagnosticStatus, time_sec, verbose=False): - level_name, _ = DiagnosticsParser.convert_level_to_str(status.level) + level_name, _ = convert_level_to_str(status.level) node_name, name = status.name.split(":") name = name.strip() line = [ diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py index 8b9f7541a..9169208ff 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py @@ -35,5 +35,5 @@ class ListVerb(VerbExtension): """List all diagnostic status items group by node name""" def main(self, *, args): - diagnostic_parser = DiagnosticsParser(ParserModeEnum.List) + 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 index c58aee398..61eb409ac 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py @@ -49,7 +49,7 @@ def add_arguments(self, parser, cli_name): def main(self, *, args): diagnostic_parser = DiagnosticsParser( - mode=ParserModeEnum.Show, + mode=ParserModeEnum.SHOW, verbose=args.verbose, levels=args.levels, run_once=args.once, From d57f8c831ed11a8dda5348f1867bc727ef82e2a5 Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 26 Apr 2023 23:47:31 +0300 Subject: [PATCH 13/23] fix flake8 errors --- ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index 392e50af8..776e963eb 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -69,9 +69,10 @@ b"\x00": lambda: ("OK", GREEN + "OK" + COLOR_DEFAULT), b"\x01": lambda: ("WARN", YELLOW + "WARN" + COLOR_DEFAULT), b"\x02": lambda: ("ERROR", RED + "ERROR" + COLOR_DEFAULT), - b"\x03": lambda: ("STALE", RED + "STALE" + COLOR_DEFAULT) + b"\x03": lambda: ("STALE", RED + "STALE" + COLOR_DEFAULT), } + def convert_level_to_str(level) -> Tuple[str, str]: return level_to_str_mapping[level]() From deba6b3e6801143d9fb1ff2227f948bd83f606c7 Mon Sep 17 00:00:00 2001 From: robo Date: Fri, 28 Apr 2023 08:55:21 +0300 Subject: [PATCH 14/23] init tutorials --- diagnostics_tutorial/README.md | 28 ++++++++++ .../diagnostics_tutorial/__init__.py | 0 .../composite_task_demo.py | 53 +++++++++++++++++++ .../freq_diagnostics_demo.py | 38 +++++++++++++ .../status_wrapper_demo.py | 45 ++++++++++++++++ .../diagnostics_tutorial/task_demo.py | 40 ++++++++++++++ diagnostics_tutorial/package.xml | 18 +++++++ .../resource/diagnostics_tutorial | 0 diagnostics_tutorial/setup.cfg | 4 ++ diagnostics_tutorial/setup.py | 29 ++++++++++ diagnostics_tutorial/test/test_copyright.py | 25 +++++++++ diagnostics_tutorial/test/test_flake8.py | 25 +++++++++ diagnostics_tutorial/test/test_pep257.py | 23 ++++++++ .../ros2diagnostics_cli/api/__init__.py | 20 ++++--- 14 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 diagnostics_tutorial/README.md create mode 100644 diagnostics_tutorial/diagnostics_tutorial/__init__.py create mode 100644 diagnostics_tutorial/diagnostics_tutorial/composite_task_demo.py create mode 100644 diagnostics_tutorial/diagnostics_tutorial/freq_diagnostics_demo.py create mode 100644 diagnostics_tutorial/diagnostics_tutorial/status_wrapper_demo.py create mode 100644 diagnostics_tutorial/diagnostics_tutorial/task_demo.py create mode 100644 diagnostics_tutorial/package.xml create mode 100644 diagnostics_tutorial/resource/diagnostics_tutorial create mode 100644 diagnostics_tutorial/setup.cfg create mode 100644 diagnostics_tutorial/setup.py create mode 100644 diagnostics_tutorial/test/test_copyright.py create mode 100644 diagnostics_tutorial/test/test_flake8.py create mode 100644 diagnostics_tutorial/test/test_pep257.py diff --git a/diagnostics_tutorial/README.md b/diagnostics_tutorial/README.md new file mode 100644 index 000000000..a397f6706 --- /dev/null +++ b/diagnostics_tutorial/README.md @@ -0,0 +1,28 @@ +# Diagnostics mini tutorial +base on [code from here](http://docs.ros.org/en/melodic/api/diagnostic_updater/html/example_8cpp_source.html) + +## DiagnosticTask +[API]() + + +[code example](diagnostics_tutorial/task_demo.py) + +## DiagnosticStatusWrapper +[API](https://docs.ros.org/en/humble/p/diagnostic_updater/generated/classdiagnostic__updater_1_1DiagnosticStatusWrapper.html) +Wrapper for the diagnostic_msgs::msg::DiagnosticStatus message that makes it easier to update and handle + +[code example](diagnostics_tutorial/status_wrapper_demo.py) + +## CompositeDiagnosticTask +[API](https://docs.ros.org/en/humble/p/diagnostic_updater/generated/classdiagnostic__updater_1_1CompositeDiagnosticTask.html) +The CompositeDiagnosticTask allows multiple DiagnosticTask instances to be combined into a single task that produces a single DiagnosticStatusWrapped + +[code example](diagnostics_tutorial/composite_task_demo.py) + +## HeaderlessTopicDiagnostic +[API](https://docs.ros.org/en/humble/p/diagnostic_updater/generated/classdiagnostic__updater_1_1HeaderlessTopicDiagnostic.html) +A class to facilitate making diagnostics for a topic using a [FrequencyStatus](#frequencystatus). + +## FrequencyStatus +[API](https://docs.ros.org/en/humble/p/diagnostic_updater/generated/classdiagnostic__updater_1_1FrequencyStatus.html#classdiagnostic__updater_1_1FrequencyStatus) +A diagnostic task that monitors the frequency of an event. \ No newline at end of file diff --git a/diagnostics_tutorial/diagnostics_tutorial/__init__.py b/diagnostics_tutorial/diagnostics_tutorial/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/diagnostics_tutorial/diagnostics_tutorial/composite_task_demo.py b/diagnostics_tutorial/diagnostics_tutorial/composite_task_demo.py new file mode 100644 index 000000000..9b76c64fe --- /dev/null +++ b/diagnostics_tutorial/diagnostics_tutorial/composite_task_demo.py @@ -0,0 +1,53 @@ +import rclpy +from diagnostic_msgs.msg import DiagnosticStatus +from diagnostic_updater import ( + CompositeDiagnosticTask, + Updater, + FunctionDiagnosticTask, + DiagnosticStatusWrapper, +) +from rclpy.node import Node + + +def check_lower_bound(state: DiagnosticStatusWrapper): + state.summary(DiagnosticStatus.OK, "lower ok") + return state + + +def check_upper_bound(state: DiagnosticStatusWrapper): + state.summary(DiagnosticStatus.OK, "upper ok") + return state + + +class MyNode(Node): + def __init__(self): + node_name = "minimal" + super().__init__(node_name) + self.diagnostic_updater = Updater(self) + lower = FunctionDiagnosticTask("lower check", check_lower_bound) + upper = FunctionDiagnosticTask("upper check", check_upper_bound) + comp_task = CompositeDiagnosticTask("range demo") + comp_task.addTask(lower) + comp_task.addTask(upper) + self.diagnostic_updater.add(comp_task) + self.create_timer(1.0, self.__timer_handler) + self.get_logger().info("Hello ROS2") + + def __timer_handler(self): + self.diagnostic_updater.update() + + +def main(): + rclpy.init() + node = MyNode() + try: + rclpy.spin(node) + except KeyboardInterrupt: + print("User exit") + finally: + node.destroy_node() + rclpy.try_shutdown() + + +if __name__ == "__main__": + main() diff --git a/diagnostics_tutorial/diagnostics_tutorial/freq_diagnostics_demo.py b/diagnostics_tutorial/diagnostics_tutorial/freq_diagnostics_demo.py new file mode 100644 index 000000000..74305cffb --- /dev/null +++ b/diagnostics_tutorial/diagnostics_tutorial/freq_diagnostics_demo.py @@ -0,0 +1,38 @@ +import rclpy +from rclpy.node import Node +from diagnostic_updater import HeaderlessTopicDiagnostic, FrequencyStatusParam, Updater + + +class MyNode(Node): + def __init__(self): + node_name = "frequency_diagnostics" + super().__init__(node_name) + + self.diagnostic_updater = Updater(self) + freq_bound = {"min": 0.5, "max": 2} + self.pub_freq = HeaderlessTopicDiagnostic( + "topic1", self.diagnostic_updater, FrequencyStatusParam(freq_bound) + ) + + self.create_timer(3.0, self.__timer_handler) + self.get_logger().info("Hello ROS2") + + def __timer_handler(self): + self.pub_freq.tick() + self.diagnostic_updater.update() + + +def main(args=None): + rclpy.init(args=args) + node = MyNode() + try: + rclpy.spin(node) + except KeyboardInterrupt: + print("User exit") + finally: + node.destroy_node() + rclpy.try_shutdown() + + +if __name__ == "__main__": + main() diff --git a/diagnostics_tutorial/diagnostics_tutorial/status_wrapper_demo.py b/diagnostics_tutorial/diagnostics_tutorial/status_wrapper_demo.py new file mode 100644 index 000000000..740c4e912 --- /dev/null +++ b/diagnostics_tutorial/diagnostics_tutorial/status_wrapper_demo.py @@ -0,0 +1,45 @@ +import rclpy +from diagnostic_msgs.msg import DiagnosticStatus +from diagnostic_updater import DiagnosticStatusWrapper, Updater +from rclpy.node import Node + + +class DummyClass: + def produce_diagnostics(self, stat: DiagnosticStatusWrapper) -> DiagnosticStatusWrapper: + stat.summary(DiagnosticStatus.WARN, "demo class status") + stat.add("dummy data", "2000") + return stat + +def dummy_diagnostics(stat: DiagnosticStatusWrapper) -> DiagnosticStatusWrapper: + stat.summary(DiagnosticStatus.OK, "dummy_diagnostics function") + return stat + +class MyNode(Node): + def __init__(self): + node_name = "minimal" + super().__init__(node_name) + self.diagnostic_updater = Updater(self) + dc = DummyClass() + self.diagnostic_updater.add("method updater", dc.produce_diagnostics) + self.diagnostic_updater.add("function update", dummy_diagnostics) + self.create_timer(1.0, self.__timer_handler) + self.get_logger().info("Hello ROS2") + + def __timer_handler(self): + self.diagnostic_updater.update() + + +def main(): + rclpy.init() + node = MyNode() + try: + rclpy.spin(node) + except KeyboardInterrupt: + print("User exit") + finally: + node.destroy_node() + rclpy.try_shutdown() + + +if __name__ == "__main__": + main() diff --git a/diagnostics_tutorial/diagnostics_tutorial/task_demo.py b/diagnostics_tutorial/diagnostics_tutorial/task_demo.py new file mode 100644 index 000000000..8f1ba0aa9 --- /dev/null +++ b/diagnostics_tutorial/diagnostics_tutorial/task_demo.py @@ -0,0 +1,40 @@ +import rclpy +from rclpy.node import Node +from diagnostic_updater import DiagnosticTask, DiagnosticStatusWrapper, Updater +from diagnostic_msgs.msg import DiagnosticStatus + + +class DummyTask(DiagnosticTask): + def run(self, stat: DiagnosticStatusWrapper): + stat.summary(DiagnosticStatus.WARN, "DummyTask demo") + return stat + + +class MyNode(Node): + def __init__(self): + node_name = "minimal" + super().__init__(node_name) + self.diagnostic_updater = Updater(self) + self.diagnostic_updater.setHardwareID("demo hw") + self.diagnostic_updater.add(DummyTask("dummy_task")) + self.create_timer(1.0, self.__timer_handler) + self.get_logger().info("Hello ROS2") + + def __timer_handler(self): + self.diagnostic_updater.update() + + +def main(args=None): + rclpy.init(args=args) + node = MyNode() + try: + rclpy.spin(node) + except KeyboardInterrupt: + print("User exit") + finally: + node.destroy_node() + rclpy.try_shutdown() + + +if __name__ == "__main__": + main() diff --git a/diagnostics_tutorial/package.xml b/diagnostics_tutorial/package.xml new file mode 100644 index 000000000..d375e65a4 --- /dev/null +++ b/diagnostics_tutorial/package.xml @@ -0,0 +1,18 @@ + + + + diagnostics_tutorial + 0.0.0 + TODO: Package description + user + TODO: License declaration + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/diagnostics_tutorial/resource/diagnostics_tutorial b/diagnostics_tutorial/resource/diagnostics_tutorial new file mode 100644 index 000000000..e69de29bb diff --git a/diagnostics_tutorial/setup.cfg b/diagnostics_tutorial/setup.cfg new file mode 100644 index 000000000..ad477c415 --- /dev/null +++ b/diagnostics_tutorial/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/diagnostics_tutorial +[install] +install_scripts=$base/lib/diagnostics_tutorial diff --git a/diagnostics_tutorial/setup.py b/diagnostics_tutorial/setup.py new file mode 100644 index 000000000..6f7c820e9 --- /dev/null +++ b/diagnostics_tutorial/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup + +package_name = "diagnostics_tutorial" + +setup( + name=package_name, + version="0.0.0", + packages=[package_name], + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + ], + install_requires=["setuptools"], + zip_safe=True, + maintainer="user", + maintainer_email="robo2020@gmail.com", + description="TODO: Package description", + license="TODO: License declaration", + tests_require=["pytest"], + entry_points={ + "console_scripts": [ + "wrapper_demo=diagnostics_tutorial.status_wrapper_demo:main", + "comp=diagnostics_tutorial.composite_task_demo:main", + "freq=diagnostics_tutorial.freq_diagnostics_demo:main", + "task=diagnostics_tutorial.task_demo:main" + + ], + }, +) diff --git a/diagnostics_tutorial/test/test_copyright.py b/diagnostics_tutorial/test/test_copyright.py new file mode 100644 index 000000000..97a39196e --- /dev/null +++ b/diagnostics_tutorial/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/diagnostics_tutorial/test/test_flake8.py b/diagnostics_tutorial/test/test_flake8.py new file mode 100644 index 000000000..27ee1078f --- /dev/null +++ b/diagnostics_tutorial/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) diff --git a/diagnostics_tutorial/test/test_pep257.py b/diagnostics_tutorial/test/test_pep257.py new file mode 100644 index 000000000..b234a3840 --- /dev/null +++ b/diagnostics_tutorial/test/test_pep257.py @@ -0,0 +1,23 @@ +# 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_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings' diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index 776e963eb..4c8cfddf5 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -73,15 +73,17 @@ } -def convert_level_to_str(level) -> Tuple[str, str]: +def convert_level_to_str(level: bytes) -> Tuple[str, str]: return level_to_str_mapping[level]() -def open_file_for_output(csv_file) -> TextIO: +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") + raise Exception( + f"Folder {csv_file} not exists" + ) # pylint: disable=[broad-exception-raised] f = open(csv_file, "w", encoding="utf-8") return f @@ -113,7 +115,7 @@ def set_render(self, handler): self.__status_render_handler = handler def run(self): - self.register_and_parse_diagnostics_topic() + self.register_diagnostics_topic() def __filter_level(self, level): if not self.__levels: @@ -169,7 +171,11 @@ def diagnostics_status_handler(self, msg: DiagnosticArray) -> None: if not counter: print(f"No diagnostic for levels: {self.__levels_info}") - def register_and_parse_diagnostics_topic(self): + def register_diagnostics_topic(self): + """ + create ros node and + subscribe to /diagnostic topic + """ if self.__mode == ParserModeEnum.LIST: handler = diagnostic_list_handler else: @@ -198,7 +204,8 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: print group data as yaml to stdout Args: - msg (DiagnosticArray): _description_ + msg (DiagnosticArray): /diagnostics topic message + http://docs.ros.org/en/noetic/api/diagnostic_msgs/html/msg/DiagnosticArray.html """ status: DiagnosticStatus data: Dict[str, List[str]] = {} @@ -215,6 +222,7 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: def add_common_arguments(parser: ArgumentParser): + """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" From 2e23d8b896909921f21f407bd1738c1e868922dc Mon Sep 17 00:00:00 2001 From: robo Date: Tue, 2 May 2023 20:25:18 +0300 Subject: [PATCH 15/23] fix: handle name without clone task name separator --- ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py | 6 +++++- ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index 4c8cfddf5..a376026ad 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -211,7 +211,11 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: data: Dict[str, List[str]] = {} print(f"--- time: {msg.header.stamp.sec} ---") for status in msg.status: - node, name = status.name.split(":") + if ":" in name: + node, name = status.name.split(":") + else: + node = "Aggr" + name = status.name name = name.strip() if node in data: data[node].append(name) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index 2c02e021a..308d6b57b 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -61,12 +61,16 @@ def add_arguments(self, parser, cli_name): def render(self, status: DiagnosticStatus, time_sec, verbose=False): level_name, _ = convert_level_to_str(status.level) - node_name, name = status.name.split(":") + if ":" in name: + node, name = status.name.split(":") + else: + node = "Aggr" + name = status.name name = name.strip() line = [ str(time_sec), level_name, - node_name, + node, name, status.message, status.hardware_id, From 98b78aaa544b5ad06b4810d61bd5394b425f2013 Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 3 May 2023 05:53:53 +0300 Subject: [PATCH 16/23] fix: extract node name todo: add regex for extract node name from diagnostic name --- ros2diagnostics_cli/README.md | 8 +++++++- ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py | 4 ++-- ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ros2diagnostics_cli/README.md b/ros2diagnostics_cli/README.md index fa411f2d3..db9355a53 100644 --- a/ros2diagnostics_cli/README.md +++ b/ros2diagnostics_cli/README.md @@ -143,4 +143,10 @@ ros2 diagnostics csv -o /tmp/1.csv -f Task$ -v - More tests - Add unit test - DEB package and install tests -- Ideas \ No newline at end of file +- Ideas + + +## Tests +``` +ros2 launch diagnostic_aggregator example.launch.py +``` diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index a376026ad..1308ee9dd 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -211,10 +211,10 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: data: Dict[str, List[str]] = {} print(f"--- time: {msg.header.stamp.sec} ---") for status in msg.status: - if ":" in name: + if ":" in status.name: node, name = status.name.split(":") else: - node = "Aggr" + node = "---" name = status.name name = name.strip() if node in data: diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index 308d6b57b..e3f5b1ada 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -61,10 +61,10 @@ def add_arguments(self, parser, cli_name): def render(self, status: DiagnosticStatus, time_sec, verbose=False): level_name, _ = convert_level_to_str(status.level) - if ":" in name: + if ":" in status.name: node, name = status.name.split(":") else: - node = "Aggr" + node = "---" name = status.name name = name.strip() line = [ From feacb0dfc7a8b0e51984b396ad3101243d8cb11d Mon Sep 17 00:00:00 2001 From: robo Date: Tue, 2 May 2023 20:25:18 +0300 Subject: [PATCH 17/23] fix: handle name without clone task name separator --- ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py | 6 +++++- ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index 776e963eb..d067b3a70 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -204,7 +204,11 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: data: Dict[str, List[str]] = {} print(f"--- time: {msg.header.stamp.sec} ---") for status in msg.status: - node, name = status.name.split(":") + if ":" in name: + node, name = status.name.split(":") + else: + node = "Aggr" + name = status.name name = name.strip() if node in data: data[node].append(name) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index 2c02e021a..308d6b57b 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -61,12 +61,16 @@ def add_arguments(self, parser, cli_name): def render(self, status: DiagnosticStatus, time_sec, verbose=False): level_name, _ = convert_level_to_str(status.level) - node_name, name = status.name.split(":") + if ":" in name: + node, name = status.name.split(":") + else: + node = "Aggr" + name = status.name name = name.strip() line = [ str(time_sec), level_name, - node_name, + node, name, status.message, status.hardware_id, From c77d4e441577b974244d1542cd912b8028f030b8 Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 3 May 2023 05:53:53 +0300 Subject: [PATCH 18/23] fix: extract node name todo: add regex for extract node name from diagnostic name --- ros2diagnostics_cli/README.md | 8 +++++++- ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py | 4 ++-- ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ros2diagnostics_cli/README.md b/ros2diagnostics_cli/README.md index fa411f2d3..db9355a53 100644 --- a/ros2diagnostics_cli/README.md +++ b/ros2diagnostics_cli/README.md @@ -143,4 +143,10 @@ ros2 diagnostics csv -o /tmp/1.csv -f Task$ -v - More tests - Add unit test - DEB package and install tests -- Ideas \ No newline at end of file +- Ideas + + +## Tests +``` +ros2 launch diagnostic_aggregator example.launch.py +``` diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index d067b3a70..4b490338f 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -204,10 +204,10 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: data: Dict[str, List[str]] = {} print(f"--- time: {msg.header.stamp.sec} ---") for status in msg.status: - if ":" in name: + if ":" in status.name: node, name = status.name.split(":") else: - node = "Aggr" + node = "---" name = status.name name = name.strip() if node in data: diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index 308d6b57b..e3f5b1ada 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -61,10 +61,10 @@ def add_arguments(self, parser, cli_name): def render(self, status: DiagnosticStatus, time_sec, verbose=False): level_name, _ = convert_level_to_str(status.level) - if ":" in name: + if ":" in status.name: node, name = status.name.split(":") else: - node = "Aggr" + node = "---" name = status.name name = name.strip() line = [ From ddfba3bcb237d05ad5d4da9f6e0acbf6d065c59a Mon Sep 17 00:00:00 2001 From: robo Date: Thu, 4 May 2023 21:21:02 +0300 Subject: [PATCH 19/23] set version and license --- ros2diagnostics_cli/package.xml | 13 +++++++------ ros2diagnostics_cli/setup.py | 14 +++++++------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/ros2diagnostics_cli/package.xml b/ros2diagnostics_cli/package.xml index 4b1d75b4e..c17591dce 100644 --- a/ros2diagnostics_cli/package.xml +++ b/ros2diagnostics_cli/package.xml @@ -1,11 +1,12 @@ - - + + ros2diagnostics_cli - 0.0.0 - TODO: Package description - user - TODO: License declaration + 3.1.2 + diagnostic command for ROS2 command line, parse and show /diagnostics topic + + Christian Henkel + BSD-3-Clause ament_copyright ament_flake8 diff --git a/ros2diagnostics_cli/setup.py b/ros2diagnostics_cli/setup.py index 925bd51e1..0e5fda977 100644 --- a/ros2diagnostics_cli/setup.py +++ b/ros2diagnostics_cli/setup.py @@ -1,23 +1,23 @@ from setuptools import setup from setuptools import find_packages -package_name = 'ros2diagnostics_cli' +PACKAGE_NAME = 'ros2diagnostics_cli' setup( - name=package_name, - version='0.0.1', + 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']), + ['resource/' + PACKAGE_NAME]), + ('share/' + PACKAGE_NAME, ['package.xml']), ], install_requires=['ros2cli'], zip_safe=True, maintainer='user', maintainer_email='robo2020@gmail.com', - description='TODO: Package description', - license='TODO: License declaration', + description='diagnostic command for ROS2 command line, parse and show /diagnostics topic', + license='BSD-3-Clause', tests_require=['pytest'], entry_points={ 'ros2cli.command': [ From 82cc52bc4ac776e81f35bed49fab4261b8c69447 Mon Sep 17 00:00:00 2001 From: robo Date: Thu, 4 May 2023 21:27:29 +0300 Subject: [PATCH 20/23] remove diagnostic_tutorial from PR --- diagnostics_tutorial/README.md | 28 ---------- .../diagnostics_tutorial/__init__.py | 0 .../composite_task_demo.py | 53 ------------------- .../freq_diagnostics_demo.py | 38 ------------- .../status_wrapper_demo.py | 45 ---------------- .../diagnostics_tutorial/task_demo.py | 40 -------------- diagnostics_tutorial/package.xml | 18 ------- .../resource/diagnostics_tutorial | 0 diagnostics_tutorial/setup.cfg | 4 -- diagnostics_tutorial/setup.py | 29 ---------- diagnostics_tutorial/test/test_copyright.py | 25 --------- diagnostics_tutorial/test/test_flake8.py | 25 --------- diagnostics_tutorial/test/test_pep257.py | 23 -------- 13 files changed, 328 deletions(-) delete mode 100644 diagnostics_tutorial/README.md delete mode 100644 diagnostics_tutorial/diagnostics_tutorial/__init__.py delete mode 100644 diagnostics_tutorial/diagnostics_tutorial/composite_task_demo.py delete mode 100644 diagnostics_tutorial/diagnostics_tutorial/freq_diagnostics_demo.py delete mode 100644 diagnostics_tutorial/diagnostics_tutorial/status_wrapper_demo.py delete mode 100644 diagnostics_tutorial/diagnostics_tutorial/task_demo.py delete mode 100644 diagnostics_tutorial/package.xml delete mode 100644 diagnostics_tutorial/resource/diagnostics_tutorial delete mode 100644 diagnostics_tutorial/setup.cfg delete mode 100644 diagnostics_tutorial/setup.py delete mode 100644 diagnostics_tutorial/test/test_copyright.py delete mode 100644 diagnostics_tutorial/test/test_flake8.py delete mode 100644 diagnostics_tutorial/test/test_pep257.py diff --git a/diagnostics_tutorial/README.md b/diagnostics_tutorial/README.md deleted file mode 100644 index a397f6706..000000000 --- a/diagnostics_tutorial/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Diagnostics mini tutorial -base on [code from here](http://docs.ros.org/en/melodic/api/diagnostic_updater/html/example_8cpp_source.html) - -## DiagnosticTask -[API]() - - -[code example](diagnostics_tutorial/task_demo.py) - -## DiagnosticStatusWrapper -[API](https://docs.ros.org/en/humble/p/diagnostic_updater/generated/classdiagnostic__updater_1_1DiagnosticStatusWrapper.html) -Wrapper for the diagnostic_msgs::msg::DiagnosticStatus message that makes it easier to update and handle - -[code example](diagnostics_tutorial/status_wrapper_demo.py) - -## CompositeDiagnosticTask -[API](https://docs.ros.org/en/humble/p/diagnostic_updater/generated/classdiagnostic__updater_1_1CompositeDiagnosticTask.html) -The CompositeDiagnosticTask allows multiple DiagnosticTask instances to be combined into a single task that produces a single DiagnosticStatusWrapped - -[code example](diagnostics_tutorial/composite_task_demo.py) - -## HeaderlessTopicDiagnostic -[API](https://docs.ros.org/en/humble/p/diagnostic_updater/generated/classdiagnostic__updater_1_1HeaderlessTopicDiagnostic.html) -A class to facilitate making diagnostics for a topic using a [FrequencyStatus](#frequencystatus). - -## FrequencyStatus -[API](https://docs.ros.org/en/humble/p/diagnostic_updater/generated/classdiagnostic__updater_1_1FrequencyStatus.html#classdiagnostic__updater_1_1FrequencyStatus) -A diagnostic task that monitors the frequency of an event. \ No newline at end of file diff --git a/diagnostics_tutorial/diagnostics_tutorial/__init__.py b/diagnostics_tutorial/diagnostics_tutorial/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/diagnostics_tutorial/diagnostics_tutorial/composite_task_demo.py b/diagnostics_tutorial/diagnostics_tutorial/composite_task_demo.py deleted file mode 100644 index 9b76c64fe..000000000 --- a/diagnostics_tutorial/diagnostics_tutorial/composite_task_demo.py +++ /dev/null @@ -1,53 +0,0 @@ -import rclpy -from diagnostic_msgs.msg import DiagnosticStatus -from diagnostic_updater import ( - CompositeDiagnosticTask, - Updater, - FunctionDiagnosticTask, - DiagnosticStatusWrapper, -) -from rclpy.node import Node - - -def check_lower_bound(state: DiagnosticStatusWrapper): - state.summary(DiagnosticStatus.OK, "lower ok") - return state - - -def check_upper_bound(state: DiagnosticStatusWrapper): - state.summary(DiagnosticStatus.OK, "upper ok") - return state - - -class MyNode(Node): - def __init__(self): - node_name = "minimal" - super().__init__(node_name) - self.diagnostic_updater = Updater(self) - lower = FunctionDiagnosticTask("lower check", check_lower_bound) - upper = FunctionDiagnosticTask("upper check", check_upper_bound) - comp_task = CompositeDiagnosticTask("range demo") - comp_task.addTask(lower) - comp_task.addTask(upper) - self.diagnostic_updater.add(comp_task) - self.create_timer(1.0, self.__timer_handler) - self.get_logger().info("Hello ROS2") - - def __timer_handler(self): - self.diagnostic_updater.update() - - -def main(): - rclpy.init() - node = MyNode() - try: - rclpy.spin(node) - except KeyboardInterrupt: - print("User exit") - finally: - node.destroy_node() - rclpy.try_shutdown() - - -if __name__ == "__main__": - main() diff --git a/diagnostics_tutorial/diagnostics_tutorial/freq_diagnostics_demo.py b/diagnostics_tutorial/diagnostics_tutorial/freq_diagnostics_demo.py deleted file mode 100644 index 74305cffb..000000000 --- a/diagnostics_tutorial/diagnostics_tutorial/freq_diagnostics_demo.py +++ /dev/null @@ -1,38 +0,0 @@ -import rclpy -from rclpy.node import Node -from diagnostic_updater import HeaderlessTopicDiagnostic, FrequencyStatusParam, Updater - - -class MyNode(Node): - def __init__(self): - node_name = "frequency_diagnostics" - super().__init__(node_name) - - self.diagnostic_updater = Updater(self) - freq_bound = {"min": 0.5, "max": 2} - self.pub_freq = HeaderlessTopicDiagnostic( - "topic1", self.diagnostic_updater, FrequencyStatusParam(freq_bound) - ) - - self.create_timer(3.0, self.__timer_handler) - self.get_logger().info("Hello ROS2") - - def __timer_handler(self): - self.pub_freq.tick() - self.diagnostic_updater.update() - - -def main(args=None): - rclpy.init(args=args) - node = MyNode() - try: - rclpy.spin(node) - except KeyboardInterrupt: - print("User exit") - finally: - node.destroy_node() - rclpy.try_shutdown() - - -if __name__ == "__main__": - main() diff --git a/diagnostics_tutorial/diagnostics_tutorial/status_wrapper_demo.py b/diagnostics_tutorial/diagnostics_tutorial/status_wrapper_demo.py deleted file mode 100644 index 740c4e912..000000000 --- a/diagnostics_tutorial/diagnostics_tutorial/status_wrapper_demo.py +++ /dev/null @@ -1,45 +0,0 @@ -import rclpy -from diagnostic_msgs.msg import DiagnosticStatus -from diagnostic_updater import DiagnosticStatusWrapper, Updater -from rclpy.node import Node - - -class DummyClass: - def produce_diagnostics(self, stat: DiagnosticStatusWrapper) -> DiagnosticStatusWrapper: - stat.summary(DiagnosticStatus.WARN, "demo class status") - stat.add("dummy data", "2000") - return stat - -def dummy_diagnostics(stat: DiagnosticStatusWrapper) -> DiagnosticStatusWrapper: - stat.summary(DiagnosticStatus.OK, "dummy_diagnostics function") - return stat - -class MyNode(Node): - def __init__(self): - node_name = "minimal" - super().__init__(node_name) - self.diagnostic_updater = Updater(self) - dc = DummyClass() - self.diagnostic_updater.add("method updater", dc.produce_diagnostics) - self.diagnostic_updater.add("function update", dummy_diagnostics) - self.create_timer(1.0, self.__timer_handler) - self.get_logger().info("Hello ROS2") - - def __timer_handler(self): - self.diagnostic_updater.update() - - -def main(): - rclpy.init() - node = MyNode() - try: - rclpy.spin(node) - except KeyboardInterrupt: - print("User exit") - finally: - node.destroy_node() - rclpy.try_shutdown() - - -if __name__ == "__main__": - main() diff --git a/diagnostics_tutorial/diagnostics_tutorial/task_demo.py b/diagnostics_tutorial/diagnostics_tutorial/task_demo.py deleted file mode 100644 index 8f1ba0aa9..000000000 --- a/diagnostics_tutorial/diagnostics_tutorial/task_demo.py +++ /dev/null @@ -1,40 +0,0 @@ -import rclpy -from rclpy.node import Node -from diagnostic_updater import DiagnosticTask, DiagnosticStatusWrapper, Updater -from diagnostic_msgs.msg import DiagnosticStatus - - -class DummyTask(DiagnosticTask): - def run(self, stat: DiagnosticStatusWrapper): - stat.summary(DiagnosticStatus.WARN, "DummyTask demo") - return stat - - -class MyNode(Node): - def __init__(self): - node_name = "minimal" - super().__init__(node_name) - self.diagnostic_updater = Updater(self) - self.diagnostic_updater.setHardwareID("demo hw") - self.diagnostic_updater.add(DummyTask("dummy_task")) - self.create_timer(1.0, self.__timer_handler) - self.get_logger().info("Hello ROS2") - - def __timer_handler(self): - self.diagnostic_updater.update() - - -def main(args=None): - rclpy.init(args=args) - node = MyNode() - try: - rclpy.spin(node) - except KeyboardInterrupt: - print("User exit") - finally: - node.destroy_node() - rclpy.try_shutdown() - - -if __name__ == "__main__": - main() diff --git a/diagnostics_tutorial/package.xml b/diagnostics_tutorial/package.xml deleted file mode 100644 index d375e65a4..000000000 --- a/diagnostics_tutorial/package.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diagnostics_tutorial - 0.0.0 - TODO: Package description - user - TODO: License declaration - - ament_copyright - ament_flake8 - ament_pep257 - python3-pytest - - - ament_python - - diff --git a/diagnostics_tutorial/resource/diagnostics_tutorial b/diagnostics_tutorial/resource/diagnostics_tutorial deleted file mode 100644 index e69de29bb..000000000 diff --git a/diagnostics_tutorial/setup.cfg b/diagnostics_tutorial/setup.cfg deleted file mode 100644 index ad477c415..000000000 --- a/diagnostics_tutorial/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/diagnostics_tutorial -[install] -install_scripts=$base/lib/diagnostics_tutorial diff --git a/diagnostics_tutorial/setup.py b/diagnostics_tutorial/setup.py deleted file mode 100644 index 6f7c820e9..000000000 --- a/diagnostics_tutorial/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -from setuptools import setup - -package_name = "diagnostics_tutorial" - -setup( - name=package_name, - version="0.0.0", - packages=[package_name], - data_files=[ - ("share/ament_index/resource_index/packages", ["resource/" + package_name]), - ("share/" + package_name, ["package.xml"]), - ], - install_requires=["setuptools"], - zip_safe=True, - maintainer="user", - maintainer_email="robo2020@gmail.com", - description="TODO: Package description", - license="TODO: License declaration", - tests_require=["pytest"], - entry_points={ - "console_scripts": [ - "wrapper_demo=diagnostics_tutorial.status_wrapper_demo:main", - "comp=diagnostics_tutorial.composite_task_demo:main", - "freq=diagnostics_tutorial.freq_diagnostics_demo:main", - "task=diagnostics_tutorial.task_demo:main" - - ], - }, -) diff --git a/diagnostics_tutorial/test/test_copyright.py b/diagnostics_tutorial/test/test_copyright.py deleted file mode 100644 index 97a39196e..000000000 --- a/diagnostics_tutorial/test/test_copyright.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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/diagnostics_tutorial/test/test_flake8.py b/diagnostics_tutorial/test/test_flake8.py deleted file mode 100644 index 27ee1078f..000000000 --- a/diagnostics_tutorial/test/test_flake8.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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) diff --git a/diagnostics_tutorial/test/test_pep257.py b/diagnostics_tutorial/test/test_pep257.py deleted file mode 100644 index b234a3840..000000000 --- a/diagnostics_tutorial/test/test_pep257.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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_pep257.main import main -import pytest - - -@pytest.mark.linter -@pytest.mark.pep257 -def test_pep257(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found code style errors / warnings' From b8ce45da6e192226d81fec710150cf571e5196a7 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Wed, 17 May 2023 12:09:35 +0200 Subject: [PATCH 21/23] gh tests for the new package Signed-off-by: Christian Henkel --- .github/workflows/lint.yaml | 1 + .github/workflows/test.yaml | 1 + 2 files changed, 2 insertions(+) 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] From e6a895601fe491d9c7208fcc0186b2677bab7f6a Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Wed, 17 May 2023 13:48:21 +0200 Subject: [PATCH 22/23] fixing flake8 errors Signed-off-by: Christian Henkel --- .../ros2diagnostics_cli/api/__init__.py | 115 +++++++----------- .../command/diagnostics.py | 9 +- .../ros2diagnostics_cli/verb/csv.py | 36 +++--- .../ros2diagnostics_cli/verb/list.py | 2 +- .../ros2diagnostics_cli/verb/show.py | 12 +- ros2diagnostics_cli/setup.py | 2 +- 6 files changed, 82 insertions(+), 94 deletions(-) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index 1308ee9dd..ef74762c1 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -26,50 +26,31 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. - -"""_summary_ - -std_msgs/Header header # for timestamp -builtin_interfaces/Time stamp - int32 sec - uint32 nanosec -string frame_id -DiagnosticStatus[] status # an array of components being reported on -byte OK=0 -byte WARN=1 -byte ERROR=2 -byte STALE=3 -byte level -string name -string message -string hardware_id -KeyValue[] values - string key - string value -""" -import pathlib -import re from argparse import ArgumentParser from enum import IntEnum +import pathlib +import re from typing import Dict, List, TextIO, Tuple -import rclpy -import yaml from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue + +import rclpy from rclpy.qos import qos_profile_system_default -TOPIC_DIAGNOSTICS = "/diagnostics" +import yaml + +TOPIC_DIAGNOSTICS = '/diagnostics' -COLOR_DEFAULT = "\033[39m" -GREEN = "\033[92m" -RED = "\033[91m" -YELLOW = "\033[93m" +COLOR_DEFAULT = '\033[39m' +GREEN = '\033[92m' +RED = '\033[91m' +YELLOW = '\033[93m' level_to_str_mapping = { - b"\x00": lambda: ("OK", GREEN + "OK" + COLOR_DEFAULT), - b"\x01": lambda: ("WARN", YELLOW + "WARN" + COLOR_DEFAULT), - b"\x02": lambda: ("ERROR", RED + "ERROR" + COLOR_DEFAULT), - b"\x03": lambda: ("STALE", RED + "STALE" + COLOR_DEFAULT), + 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), } @@ -82,10 +63,9 @@ def open_file_for_output(csv_file: str) -> TextIO: folder_exists = pathlib.Path(base_dir).is_dir() if not folder_exists: raise Exception( - f"Folder {csv_file} not exists" + f'Folder {csv_file} not exists' ) # pylint: disable=[broad-exception-raised] - f = open(csv_file, "w", encoding="utf-8") - return f + return open(csv_file, 'w', encoding='utf-8') class ParserModeEnum(IntEnum): @@ -108,7 +88,7 @@ def __init__( self.__mode = mode self.__run_once = run_once self.__verbose = verbose - self.__levels_info = ",".join(levels) if levels is not None else "" + self.__levels_info = ','.join(levels) if levels is not None else '' self.__levels = DiagnosticsParser.map_level_from_name(levels) def set_render(self, handler): @@ -129,9 +109,9 @@ def map_level_from_name(levels: List[str]) -> List[bytes]: 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"), + '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: @@ -141,23 +121,23 @@ def map_level_from_name(levels: List[str]) -> List[bytes]: @staticmethod def render(status: DiagnosticStatus, time_sec, verbose): _, level_name = convert_level_to_str(status.level) - item = f"{status.name}: {level_name}, {status.message}" + 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}") + print(f'- {kv.key}={kv.value}') def diagnostics_status_handler(self, msg: DiagnosticArray) -> None: - """Run handler for each DiagnosticStatus in array - filter status by level, name, node name + """ + Filter DiagnosticStatus by level, name and node name. Args: msg (DiagnosticArray): _description_ """ counter: int = 0 status: DiagnosticStatus - print(f"--- time: {msg.header.stamp.sec} ---") + print(f'--- time: {msg.header.stamp.sec} ---') for status in msg.status: if self.__name_filter: result = re.search(self.__name_filter, status.name) @@ -165,24 +145,22 @@ def diagnostics_status_handler(self, msg: DiagnosticArray) -> None: continue if self.__filter_level(status.level): continue - self.__status_render_handler(status, msg.header.stamp.sec, self.__verbose) + 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}") + print(f'No diagnostic for levels: {self.__levels_info}') def register_diagnostics_topic(self): - """ - create ros node and - subscribe to /diagnostic topic - """ + """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 = rclpy.create_node('ros2diagnostics_cli_filter') node.create_subscription( DiagnosticArray, TOPIC_DIAGNOSTICS, @@ -200,21 +178,20 @@ def register_diagnostics_topic(self): def diagnostic_list_handler(msg: DiagnosticArray) -> None: - """Group diagnostics Task by node - print group data as yaml to stdout + """ + Print group data as yaml to stdout. Args: - msg (DiagnosticArray): /diagnostics topic message - http://docs.ros.org/en/noetic/api/diagnostic_msgs/html/msg/DiagnosticArray.html + msg (DiagnosticArray): /diagnostics topic message """ status: DiagnosticStatus data: Dict[str, List[str]] = {} - print(f"--- time: {msg.header.stamp.sec} ---") + print(f'--- time: {msg.header.stamp.sec} ---') for status in msg.status: - if ":" in status.name: - node, name = status.name.split(":") + if ':' in status.name: + node, name = status.name.split(':') else: - node = "---" + node = '---' name = status.name name = name.strip() if node in data: @@ -226,16 +203,16 @@ def diagnostic_list_handler(msg: DiagnosticArray) -> None: def add_common_arguments(parser: ArgumentParser): - """common arguments for csv and show verbs""" - parser.add_argument("-1", "--once", action="store_true", help="run only once") + """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" + '-f', '--filter', type=str, help='filter diagnostic status name' ) parser.add_argument( - "-l", - "--levels", - action="append", + '-l', + '--levels', + action='append', type=str, - choices=["info", "warn", "error"], - help="levels to filter, can be multiple times", + choices=['info', 'warn', 'error'], + help='levels to filter, can be multiple times', ) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py b/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py index 577f02aea..352973eac 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py @@ -32,6 +32,7 @@ class DiagCommand(CommandExtension): + def __init__(self): super(DiagCommand, self).__init__() self._subparser = None @@ -39,9 +40,13 @@ def __init__(self): def add_arguments(self, parser, cli_name): self._subparser = parser add_subparsers_on_demand( - parser, cli_name, '_verb', "ros2diagnostics_cli.verb", required=False) + parser, + cli_name, + '_verb', + 'ros2diagnostics_cli.verb', + required=False) - def main(self, *, parser, args): + def main(self, *, _, args): if not hasattr(args, '_verb'): self._subparser.print_help() return 0 diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py index e3f5b1ada..956450296 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/csv.py @@ -28,43 +28,49 @@ 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, - add_common_arguments, - ParserModeEnum, - convert_level_to_str + ParserModeEnum ) -from diagnostic_msgs.msg import DiagnosticStatus, KeyValue class CSVVerb(VerbExtension): - """export /diagnostics message to csv file""" + """Export /diagnostics message to csv file.""" def __init__(self): super().__init__() self.csv: TextIO = None - def add_arguments(self, parser, cli_name): + def add_arguments(self, parser, _): add_common_arguments(parser) parser.add_argument( - "--output", "-o", type=str, required=True, help="export file full path" + '--output', + '-o', + type=str, + required=True, + help='export file full path' ) parser.add_argument( - "--verbose", - "-v", - action="store_true", - help="export DiagnosticStatus values filed", + '--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(":") + if ':' in status.name: + node, name = status.name.split(':') else: - node = "---" + node = '---' name = status.name name = name.strip() line = [ @@ -80,7 +86,7 @@ def render(self, status: DiagnosticStatus, time_sec, verbose=False): for kv in status.values: line.append(kv.value) - s_line = ",".join(line) + "\n" + s_line = ','.join(line) + '\n' print(s_line) self.csv.write(s_line) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py index 9169208ff..2f7610bdf 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/list.py @@ -32,7 +32,7 @@ class ListVerb(VerbExtension): - """List all diagnostic status items group by node name""" + """List all diagnostic status items group by node name.""" def main(self, *, args): diagnostic_parser = DiagnosticsParser(ParserModeEnum.LIST) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py index 61eb409ac..1ee9d821c 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/verb/show.py @@ -29,22 +29,22 @@ from ros2cli.verb import VerbExtension from ros2diagnostics_cli.api import ( - DiagnosticsParser, add_common_arguments, + DiagnosticsParser, ParserModeEnum, ) class ShowVerb(VerbExtension): - """Show diagnostics status item info""" + """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.", + '--verbose', + '-v', + action='store_true', + help='Display more info.', ) def main(self, *, args): diff --git a/ros2diagnostics_cli/setup.py b/ros2diagnostics_cli/setup.py index 0e5fda977..a08d428ea 100644 --- a/ros2diagnostics_cli/setup.py +++ b/ros2diagnostics_cli/setup.py @@ -1,5 +1,5 @@ -from setuptools import setup from setuptools import find_packages +from setuptools import setup PACKAGE_NAME = 'ros2diagnostics_cli' From c5bf964db56644aa24622cde382267bd15e1bc7c Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Wed, 17 May 2023 13:49:42 +0200 Subject: [PATCH 23/23] fixing pep257 errors Signed-off-by: Christian Henkel --- ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py index ef74762c1..10616b095 100644 --- a/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py +++ b/ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py @@ -133,7 +133,9 @@ 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 @@ -182,7 +184,9 @@ 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]] = {}