diff --git a/docs/software/index.rst b/docs/software/index.rst index 5b06f569e..299168e7c 100644 --- a/docs/software/index.rst +++ b/docs/software/index.rst @@ -16,6 +16,7 @@ Various documentation related to practices followed by the MIL Software team. rqt rostest alarms + preflight Bash Style Guide C++ Style Guide Python Style Guide diff --git a/docs/software/preflight.md b/docs/software/preflight.md new file mode 100644 index 000000000..a2a088fb3 --- /dev/null +++ b/docs/software/preflight.md @@ -0,0 +1,2 @@ +```{include} ../../mil_common/utils/mil_tools/scripts/mil-preflight/preflight.md +``` diff --git a/mil_common/utils/mil_tools/scripts/mil-preflight/helper.py b/mil_common/utils/mil_tools/scripts/mil-preflight/helper.py new file mode 100644 index 000000000..bb5cf6ce9 --- /dev/null +++ b/mil_common/utils/mil_tools/scripts/mil-preflight/helper.py @@ -0,0 +1,222 @@ +################################################################################ +# File name: helper.py +# Author: Keith Khadar +# Description: This is used to store the helper functions for preflight +################################################################################ +# ----- Imports ----- # +# ----- Console -----# +# ----- Async -----# +import asyncio +import subprocess +import time + +# ----- Misc -----# +from contextlib import suppress + +# ----- Preflight -----# +import menus + +# ----- ROS -----# +import rospy +import rostopic +import tests +from axros import NodeHandle +from PyInquirer import prompt +from rich.console import Console +from rich.progress import Progress, track +from rich.table import Table + +# ----- Variables ----- # +report = [] + + +# ----- Functions ----- # +def clear_screen(): + # Clears the screen and prints the preflight header + subprocess.run("clear", shell=True) + Console().print(menus.title) + + +def init_report(): + # Function to initialize the report + report.clear() + + +def init_ros(): + # Checks if ROS is running and initializes it. + # Returns False if ROS is not running and TRUE if the ROS node was properly setup. + + # Check that ROS is running! + try: + rostopic._check_master() + except Exception: + Console().print("[bold] ROS not running! Please try again later[/]") + prompt(menus.press_anykey) + return False + + # Initialize the ROS node + with suppress(Exception): + rospy.init_node("preflight") + return True + + +async def axros_check_nodes(nodes): + # Asynchronously check all the nodes and then print/save the results + + # Setup AXROS + nh = NodeHandle.from_argv("Preflight_nh", "", anonymous=True) + + # Using async.io check all the nodes + answers = [] + async with nh: + # Create all the function calls to check_node + tasks = [check_node(node, answers, nh) for node in nodes] + + # Go through all the function calls and await them. + # Using track to display a loading bar. + for task in track( + asyncio.as_completed(tasks), + description="Checking Nodes...", + total=len(tasks), + ): + await task + + # Clear the screen, print and save the response to the report + print_results(answers, "Node Liveliness") + + +async def check_node(node, results, nh): + # Using AXROS lookup a node and save the result in the results list + + try: + results.append((node, bool(await nh.lookup_node(node)))) + except Exception: + results.append((node, False)) + + +async def axros_check_topics(topics): + # Asynchronously check all the topics and then print/save the results + + # Setup AXROS + nh = NodeHandle.from_argv("Preflight_nh", "", anonymous=True) + + # Using async.io check all the topics + answers = [] + async with nh: + # Create all the function calls to check topic + tasks = [check_topic(topic, answers, nh) for topic in topics] + + # Go through all the function calls and await them. + # Using track to display a loading bar. + for task in track( + asyncio.as_completed(tasks), + description="Checking Topics...", + total=len(tasks), + ): + await task + + # Clear the screen, print and save the response to the report + print_results(answers, "Topic Liveliness") + + +async def check_topic(topic, results, nh): + # Using AXROS subscribe to a topic and wait for a message + + # Get the topic class + topicType, topicStr, _ = rostopic.get_topic_class(topic) + + # Create an AXROS subscriber + sub = nh.subscribe(topicStr, topicType) + + async with sub: + try: + await asyncio.wait_for(sub.get_next_message(), tests.topic_timeout) + results.append((topic, True)) + except Exception: + results.append((topic, False)) + + +def check_actuators(actuators): + # Check all the actuators using check_actuator + + answers = [] + for actuator in actuators: + check_actuator(actuator, answers) + + # Clear the screen, print and save the response to the report + print_results(answers, "Actuator Tests") + + +def check_actuator(actuator, results): + # Checks the actuator by publishing to a topic for a specified time + + try: + # Confirm that it is safe to run this actuator + Console().print(menus.safety_check, actuator[0]) + menu_ans = prompt(menus.continue_question) + if next(iter(menu_ans.values())) is False: + # Go back to main menu + return + + # Create a publisher + topicType, topicStr, _ = rostopic.get_topic_class(actuator[1][0]) + pub = rospy.Publisher(topicStr, topicType, queue_size=10) + + # Publish to the topic for the specified timeout + with Progress() as progress: + t_start = time.time() + t_end = t_start + tests.actuator_timeout + t_prev = time.time() + task = progress.add_task("Running", total=(t_end - t_start)) + while time.time() <= t_end: + pub.publish(actuator[1][1]) + progress.update(task, advance=(time.time() - t_prev)) + t_prev = time.time() + progress.update(task, advance=t_end) + + # Ask if the actuator worked + Console().print(menus.actuator_check) + results.append((actuator[0], next(iter(prompt(menus.yes_no).values())))) + except Exception: + Console().print(menus.actuator_failed) + results.append((actuator[0], False)) + + +def generate_report(): + # Attempts to create and display the report + + # Check that there is a report + if len(report) == 0: + Console().print( + "[bold]No report![/].\nPlease generate a report by running a full test.", + ) + prompt(menus.press_anykey) + return + # Generate the report + for result in report: + Console().print(result) + prompt(menus.press_anykey_menu_return) + + +def print_results(systems, name): + # This saves the result to the specified system and prints it + clear_screen() + result = create_result(systems, name) + Console().print(result) + + +def create_result(systems, name): + # This save the result into a RICH table + + # Generates a table to hold information about each system + result = Table(title=f"[bold]{name}[/]") + result.add_column("System Name", justify="center", style="cyan", no_wrap=True) + result.add_column("Status", justify="center", style="magenta", no_wrap=True) + + # Populates the table + for system, status in systems: + status_text = "[green]✔ Working[/]" if status else "[red]❌ Not Working[/]" + result.add_row(system, status_text) + report.append(result) + + return result diff --git a/mil_common/utils/mil_tools/scripts/mil-preflight/main.py b/mil_common/utils/mil_tools/scripts/mil-preflight/main.py new file mode 100644 index 000000000..b856c5028 --- /dev/null +++ b/mil_common/utils/mil_tools/scripts/mil-preflight/main.py @@ -0,0 +1,237 @@ +################################################################################ +# File name: main.py +# Author: Keith Khadar +# Description: This file is the entry point to preflight. +################################################################################ +# ----- Imports ----- # +# ----- Console -----# +# ----- Async -----# +import asyncio +import subprocess + +# ----- Misc -----# +from pathlib import Path + +import helper + +# ----- Preflight -----# +import menus +import tests +from PyInquirer import prompt +from rich.console import Console +from rich.markdown import Markdown + + +# ----- Main Routine ----- # +async def main(): + + # Clear Screen and Display Start menu + helper.clear_screen() + + # Print Info about Preflight + Console().print(menus.info_page) + + # Print start select menu + option = prompt(menus.start_menu) + try: + mode = next(iter(option.values())) + # Select the mode and run it + if mode == "Run Preflight Full Test": + await fullTest() + if mode == "View Report": + viewReport() + if mode == "Run Specific Test": + await specificTest() + if mode == "View Documentation": + viewDocumentation() + if mode == "Exit": + subprocess.run("clear", shell=True) + return + except StopIteration: + # Return if the user presses Ctrl-c + return + except Exception: + pass + + # Return to this screen after running the selected mode + await main() + + +# ----- Subroutines ----- # + + +# ----- Modes -----# +async def fullTest(): + + helper.init_report() + + ### Complete the setup tests ### + + # Clear the screen and display the setup checklist + helper.clear_screen() + Console().print(menus.setup_desc) + respond = prompt(menus.setupChecklist) + + # Filter the response and store checklist to the report + answers = [] + for i in range(len(tests.setup)): + if tests.setup[i] in next(iter(respond.values())): + answers.append((tests.setup[i], True)) + else: + answers.append((tests.setup[i], False)) + helper.create_result(answers, "Setup Checklist") + + # Check if the list is incomplete. If so prompt user for confirmation to continue + if len(next(iter(respond.values()))) != len(tests.setup): + menu_ans = prompt(menus.incomplete_continue) + if next(iter(menu_ans.values())) is False: + return + + ### Complete Software Tests ### + if not helper.init_ros(): + return + + # Clear the screen + helper.clear_screen() + + # Print Node Screen description + Console().print(menus.node_desc) + + # Check Nodes + await helper.axros_check_nodes(tests.nodes) + + # Prompt the user to continue to next test + menu_ans = prompt(menus.continue_question) + if next(iter(menu_ans.values())) is False: + # Go back to main menu + return + + # Print Topic screen description + helper.clear_screen() + Console().print(menus.topic_desc) + + # Check Topics + + await helper.axros_check_topics(tests.topics) + + # Prompt the user to continue to next test + menu_ans = prompt(menus.continue_question) + if next(iter(menu_ans.values())) is False: + # Go back to main menu + return + + ### Actuators Test ### + # Print Actuators Screen description + helper.clear_screen() + Console().print(menus.node_desc) + + helper.check_actuators(tests.actuatorsList) + + prompt(menus.press_anykey_menu_return) + return + + +def viewReport(): + # Clear the screen + helper.clear_screen() + + # Generate the report + helper.generate_report() + return + + +def viewDocumentation(): + # Clear the screen + helper.clear_screen() + + # Find path to README from current directory + mod_path = Path(__file__).parent + rel_path = "preflight.md" + src_path = (mod_path / rel_path).resolve() + # Print the documentation + with open(src_path, "r+") as help_file: + Console().print(Markdown(help_file.read())) + + prompt(menus.press_anykey_menu_return) + return + + +async def specificTest(): + # Init the report + helper.init_report() + + # Clear the screen and display the node checklist + helper.clear_screen() + + helper.init_ros() + + # Clear the screen and display the node checklist + helper.clear_screen() + Console().print(menus.specific_desc) + respond = prompt(menus.nodeChecklist) + + # Filter the response and store checklist to the report + nodes = [] + for i in range(len(tests.nodes)): + if tests.nodes[i] in next(iter(respond.values())): + nodes.append(tests.nodes[i]) + + # Print Node Screen description + Console().print(menus.node_desc) + + # Check Nodes + await helper.axros_check_nodes(nodes=nodes) + + # Prompt the user to continue to next test + menu_ans = prompt(menus.continue_question) + if next(iter(menu_ans.values())) is False: + # Go back to main menu + return + + # Clear the screen and display the topic checklist + helper.clear_screen() + Console().print(menus.specific_desc) + respond = prompt(menus.topicChecklist) + + # Filter the response and store checklist to the report + topics = [] + for i in range(len(tests.topics)): + if tests.topics[i] in next(iter(respond.values())): + topics.append(tests.topics[i]) + + # Print Topic screen description + helper.clear_screen() + Console().print(menus.topic_desc) + + # Check Topics + await helper.axros_check_topics(topics=topics) + + # Prompt the user to continue to next test + menu_ans = prompt(menus.continue_question) + if next(iter(menu_ans.values())) is False: + # Go back to main menu + return + + # Clear the screen and display the actuator checklist + helper.clear_screen() + Console().print(menus.specific_desc) + respond = prompt(menus.actuatorChecklist) + + # Filter the response and store checklist to the report + actuators = [] + for i in range(len(tests.actuatorsList)): + if tests.actuatorsList[i][0] in next(iter(respond.values())): + actuators.append(tests.actuatorsList[i]) + + # Print Actuators Screen description + helper.clear_screen() + Console().print(menus.node_desc) + + helper.check_actuators(actuators=actuators) + + prompt(menus.press_anykey_menu_return) + return + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/mil_common/utils/mil_tools/scripts/mil-preflight/menus.py b/mil_common/utils/mil_tools/scripts/mil-preflight/menus.py new file mode 100644 index 000000000..53a3cd738 --- /dev/null +++ b/mil_common/utils/mil_tools/scripts/mil-preflight/menus.py @@ -0,0 +1,147 @@ +################################################################################ +# File name: menus.py +# Author: Keith Khadar +# Description: This file is used to store all the menus used by preflight. Version 1.0 +################################################################################ +# ----- Imports -----# +import tests + +# ----- Home Page -----# +title = """ +[bold green]Preflight Program - Autonomous Robot Verification[/bold green] +""" +info_page = """ +Welcome to the Preflight Program, a tool inspired by the preflight checklists used by pilots before flying a plane. This program is designed to verify the functionality of all software and hardware systems on your autonomous robot. It ensures that everything is in working order, allowing you to safely deploy your robot with confidence.\n +""" + + +start_menu = [ + { + "type": "list", + "name": "mode selection", + "message": "Menu", + "choices": [ + "Run Preflight Full Test", + "View Report", + "Run Specific Test", + "View Documentation", + "Exit", + ], + }, +] + +# ----- Loading Screen -----# +continue_question = [ + { + "type": "confirm", + "name": "continue", + "message": "Continue?", + }, +] +press_anykey = [ + { + "type": "confirm", + "name": "continue", + "message": "Press any key to continue.", + "confirm": False, + }, +] +press_anykey_menu_return = [ + { + "type": "confirm", + "name": "continue", + "message": "Press any key to return to the menu.", + "confirm": False, + }, +] + +incomplete_continue = [ + { + "type": "confirm", + "name": "incomplete", + "message": "This checklist is incomplete. Do you wish to continue?", + }, +] +yes_no = [ + { + "type": "confirm", + "name": "yes_no", + "message": "Yes?", + }, +] + + +# ----- Software Screen -----# +node_desc = """ +Welcome to the [bold]ROS Node Liveliness Test screen[/]. This screen allows you to verify the liveliness of ROS nodes within the system. ROS nodes are essential components responsible for communication and control within the robot\'s architecture. Use this screen to ensure that all necessary nodes are running and responsive for proper system functionality. +""" +topic_desc = """ +Welcome to the Topic Monitoring screen. This screen allows you to monitor the topics in the ROS system. In a ROS (Robot Operating System) environment, topics serve as channels through which nodes communicate by publishing and subscribing to messages. Monitoring topics enables you to verify that sensors are actively publishing their data. +""" + +nodeChecklist = [ + { + "type": "checkbox", + "message": "Node Checklist:", + "name": "NodeChecklist: \nPlease check that all of the following are in working order.", + "choices": [{"name": item} for item in tests.nodes], + }, +] + +topicChecklist = [ + { + "type": "checkbox", + "message": "Topic Checklist:", + "name": "TopicChecklist: \nPlease check that all of the following are in working order.", + "choices": [{"name": item} for item in tests.topics], + }, +] + +# ----- Setup Screen -----# +setup_desc = """ +Welcome to the [bold]Setup Checklist[/] page of our preflight app! This checklist is designed to ensure that all critical hardware components of your robot are thoroughly inspected before each operation. By completing this checklist, you contribute to the safety and reliability of the robot during its mission. Please carefully examine each item listed below to verify its condition and functionality. + +Go through each item and check it off. Use the arrow keys to select and item and press space to check it off. Once all items have been checked press enter to continue. You can always review what items you checked off later in the report section of the main menu. + +""" +setupChecklist = [ + { + "type": "checkbox", + "message": "Setup Checklist:", + "name": "HardwareTests: \nPlease check that all of the following are in working order.", + "choices": [{"name": item} for item in tests.setup], + }, +] + +# ----- Actuators Screen -----# +actuator_desc = """ +Welcome to the [bold]Actuator Test Screen[/]. This screen allows you to test the functionality of various actuators on the robot. Actuators play a crucial role in the movement and operation of the robot. Use this screen with caution and follow the instructions carefully to avoid any accidents or damage to the robot. +\n[bold red]Caution:[/bold red] Actuators can be dangerous if mishandled. Please be careful when testing them, as they have the potential to cause harm or injury. For example, thrusters, when spinning, could chop off someone's finger. Always follow safety protocol and guidelines. +""" +safety_check = """ +[bold][yellow]Ensure that all fingers are clear of the area![/yellow][/bold] Is it safe to operate the following actuator? +""" + +actuator_check = """ +Did the actuator work? +""" + +actuator_failed = """ +Actuator failed! +""" + +actuatorChecklist = [ + { + "type": "checkbox", + "message": "Actuator Checklist:", + "name": "ActuatorTests: \nPlease check that all of the following are in working order.", + "choices": [{"name": item[0]} for item in tests.actuatorsList], + }, +] + +# ----- Specific Test Screen -----# +specific_desc = """ +Welcome to the [bold]Specific Test[/] page of our preflight app! Here you can specify which tests you want to run. This can be useful for debugging parts of the robot. +You can use the arrow keys to select which tests you want to run (use the spacebar to select). Once you are ready you can press enter to run those tests! + +""" diff --git a/mil_common/utils/mil_tools/scripts/mil-preflight/preflight.md b/mil_common/utils/mil_tools/scripts/mil-preflight/preflight.md new file mode 100644 index 000000000..942d85cb4 --- /dev/null +++ b/mil_common/utils/mil_tools/scripts/mil-preflight/preflight.md @@ -0,0 +1,24 @@ +# Preflight- Autonomous Robot Verification +## How to Use +Simply type "preflight" anywhere in the MIL directory. Make sure that a robot is connected and running or gazebo is running. To edit the hardware tests list and the automated software tests, edit the tests.py file in mil/mil_common/utils/mil_tools/scripts/mil-preflight/tests.py +## Description +Preflight is an automated testing tool that should be run after turning on and connecting to the robot to run a prelaunch hardware checklist and automated software checklist. + +### There are three types of automated software tests +#### Actuators +This test will prompt the user to enable physical moving actuators on the robot. Make sure that the area is cleared and the robot won't damage itself or others nearby. The user will have to watch and validate that they move as expected personally. + +To add an actuator test, add a topic with a list of commands and import the module it comes from. See the thrusters example for reference in tests.py +#### Nodes +A ROS Node is a live process currently running and performing a task. They communicate with each other through topics, services, and messages, etc. This test will ensure all listed ROS Nodes are running and alive as expected. + +To add a Node test, add a node to the list in tests.py +#### Topics +ROS Topics act as a channel for ROS Nodes to communicate by publishing and subscribing to messages. These tests check to verify that data is being published to these topics, ensuring that sensors under the listed topics are properly reading publishing data. + +To add a Topic test, add a topic to the list in tests.py + +### Setup Tests +There are also setup tests. These are used to verify certain features on the robot that cannot be automated. For example ensuring that the O-rings are greased. + +To add a Setup test, add a what need to be tested to the list in tests.py diff --git a/mil_common/utils/mil_tools/scripts/mil-preflight/tests.py b/mil_common/utils/mil_tools/scripts/mil-preflight/tests.py new file mode 100644 index 000000000..9503cf5cd --- /dev/null +++ b/mil_common/utils/mil_tools/scripts/mil-preflight/tests.py @@ -0,0 +1,94 @@ +################################################################################ +# File name: tests.py +# Author: Keith Khadar +# Description: This file is used to store tests used by Preflight. If you want +# add tests you would add them here. +################################################################################ + +# ----- Actuator Tests ----- # +# Add tests here for actuators. These will be turned on and the user +# will confirm their operation. Also include any custom message + +# imports here. + +# Thruster Messages +from subjugator_msgs.msg import ThrusterCmd + +# ----- Timeouts ----- # +node_timeout = 5 # seconds +topic_timeout = 5 # seconds +actuator_timeout = 1.5 # seconds + +actuatorsList = [ + ### ( + ### "Name of Test", + ### ("/topic", message), + ### ) + ( + "FLH Thruster Test", + ("/thrusters/thrust", [ThrusterCmd(name="FLH", thrust=10.0)]), + ), + ( + "FRH Thruster Test", + ("/thrusters/thrust", [ThrusterCmd(name="FRH", thrust=10.0)]), + ), + ( + "BLH Thruster Test", + ("/thrusters/thrust", [ThrusterCmd(name="BLH", thrust=10.0)]), + ), + ( + "BRH Thruster Test", + ("/thrusters/thrust", [ThrusterCmd(name="BRH", thrust=10.0)]), + ), + ( + "FLV Thruster Test", + ("/thrusters/thrust", [ThrusterCmd(name="FLV", thrust=10.0)]), + ), + ( + "FRV Thruster Test", + ("/thrusters/thrust", [ThrusterCmd(name="FRV", thrust=10.0)]), + ), + ( + "BLV Thruster Test", + ("/thrusters/thrust", [ThrusterCmd(name="BLV", thrust=10.0)]), + ), + ( + "BRV Thruster Test", + ("/thrusters/thrust", [ThrusterCmd(name="BRV", thrust=10.0)]), + ), +] + + +# ----- Setup Tests ----- # +# Add tests here for things that need to be physically inspected or check before the sub is running +setup = [ + "Grease O-rings with Molykote 55 every time a pressure vessel is closed.", + "Deploy sub. (Check for bubbles coming out of every pressure vessel, make sure buoyancy is correct)", +] + + +# ----- Software Tests ----- # +# Add tests here for software systems like sensors whose operations +# need to be verified. + +# ----- Nodes -----# +nodes = [ + # Navbox processing + "/odom_estimator", + "/transform_odometry", + "/c3_trajectory_generator", + "/adaptive_controller", + "/thruster_mapper", + "/mission_runner", +] + +# ----- Topics -----# +topics = [ + "/camera/front/right/image_raw", + "/camera/down/image_raw", + "/camera/front/left/image_raw", + "/dvl", + "/depth", + "/imu/data_raw", + "/imu/mag", +] diff --git a/requirements.txt b/requirements.txt index ffa19181d..82e9b83ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,11 @@ breathe==4.34.0 myst-parser==0.18.0 sphinx-copybutton==0.5.0 +# Terminal +PyInquirer==1.0.3 +rich==13.7.1 +pygments==2.17.2 + # External Devices pyserial==3.5 diff --git a/scripts/SubChecklist.txt b/scripts/SubChecklist.txt new file mode 100644 index 000000000..2d9664d4a --- /dev/null +++ b/scripts/SubChecklist.txt @@ -0,0 +1,2 @@ +dvl +odom diff --git a/scripts/setup.bash b/scripts/setup.bash index cd4b11510..cdf6a07e8 100755 --- a/scripts/setup.bash +++ b/scripts/setup.bash @@ -89,6 +89,9 @@ alias fd="fdfind" # Gazebo aliases alias gazebogui="rosrun gazebo_ros gzclient __name:=gzclient" +# Preflight aliases +alias preflight='python3 $MIL_REPO/mil_common/utils/mil_tools/scripts/mil-preflight/main.py' + # Process killing aliases alias killgazebo="killall -9 gzserver && killall -9 gzclient" alias killros='$MIL_REPO/scripts/kill_ros.sh'