-
Notifications
You must be signed in to change notification settings - Fork 178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ros2cli diagnostics #301
Draft
robobe
wants to merge
25
commits into
ros:ros2
Choose a base branch
from
robobe:ros2cli_diagnostics
base: ros2
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Ros2cli diagnostics #301
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
0262e0b
ros2diagnostics cli init
f177bed
rename hello to diagnostics
c2a4669
add option with string
6c7269a
arg parse examples
871095d
add csv verb
e9f6803
version 0.0.1
c4fd102
Update ros2diagnostics_cli/package.xml
robobe c4175a7
clean test and scripts file
2b13ff7
add copyright
8124d08
fix: support python 3.8 remove match
064d3f3
format and run linters
c4c08f0
fix: suport python 3.8
d57f8c8
fix flake8 errors
deba6b3
init tutorials
2e23d8b
fix: handle name without clone task name separator
98b78aa
fix: extract node name
feacb0d
fix: handle name without clone task name separator
c77d4e4
fix: extract node name
ddfba3b
set version and license
e44dbbf
Merge branch 'ros2cli_diagnostics' of dia:robobe/diagnostics into ros…
82cc52b
remove diagnostic_tutorial from PR
64de761
Merge branch 'ros2' into pr/robobe/301
ct2034 b8ce45d
gh tests for the new package
ct2034 e6a8956
fixing flake8 errors
ct2034 c5bf964
fixing pep257 errors
ct2034 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?xml version="1.0"?> | ||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> | ||
<package format="3"> | ||
<name>ros2diagnostics_cli</name> | ||
<version>0.0.0</version> | ||
<description>TODO: Package description</description> | ||
<maintainer email="[email protected]">user</maintainer> | ||
<license>TODO: License declaration</license> | ||
|
||
<test_depend>ament_copyright</test_depend> | ||
<test_depend>ament_flake8</test_depend> | ||
<test_depend>ament_pep257</test_depend> | ||
<test_depend>python3-pytest</test_depend> | ||
<depend>ros2cli</depend> | ||
<depend>python3-pyyaml</depend> | ||
robobe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<export> | ||
<build_type>ament_python</build_type> | ||
</export> | ||
</package> |
Empty file.
Empty file.
191 changes: 191 additions & 0 deletions
191
ros2diagnostics_cli/ros2diagnostics_cli/api/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,191 @@ | ||||||||
"""_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, DiagnosticStatus, KeyValue | ||||||||
from rclpy.qos import qos_profile_system_default | ||||||||
from argparse import ArgumentParser | ||||||||
import pathlib | ||||||||
import re | ||||||||
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, name_filter=None) -> None: | ||||||||
self.__name_filter = name_filter | ||||||||
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 | ||||||||
Comment on lines
+101
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
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: | ||||||||
"""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) | ||||||||
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_list_handler(msg: DiagnosticArray) -> None: | ||||||||
"""Group diagnostics Task by node | ||||||||
print group data as yaml to stdout | ||||||||
|
||||||||
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 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", | ||||||||
) |
Empty file.
22 changes: 22 additions & 0 deletions
22
ros2diagnostics_cli/ros2diagnostics_cli/command/diagnostics.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Empty file.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All required packages from this repo must be declared here