diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a510eb91b..a99604c0e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,13 +37,13 @@ jobs: submodules: recursive path: catkin_ws/src/mil - name: Setup ROS Noetic - uses: ros-tooling/setup-ros@v0.3 + uses: ros-tooling/setup-ros@v0.7 with: required-ros-distributions: noetic - name: Install pip dependencies run: | cd $GITHUB_WORKSPACE/catkin_ws/src/mil - pip install -r requirements.txt + pip3 install -r requirements.txt # We want to run a full test suite in CI - this includes the BlueView # tests! - name: Install BlueView Sonar SDK diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2f0e2009..5db81ae41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,15 +15,15 @@ repos: hooks: - id: yamllint - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v16.0.4 + rev: v16.0.6 hooks: - id: clang-format - repo: https://github.com/PyCQA/autoflake - rev: v2.1.1 + rev: v2.2.1 hooks: - id: autoflake args: [--remove-all-unused-imports, --ignore-init-module-imports] @@ -34,18 +34,18 @@ repos: exclude: ^docker|deprecated|NaviGator/simulation/VRX args: [--severity=warning, --exclude=SC1090] - repo: https://github.com/scop/pre-commit-shfmt - rev: v3.6.0-2 + rev: v3.7.0-1 hooks: - id: shfmt exclude: ^docker|deprecated|NaviGator/simulation/VRX - - repo: https://github.com/charliermarsh/ruff-pre-commit + - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: 'v0.0.270' + rev: 'v0.0.287' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/codespell-project/codespell - rev: v2.2.4 + rev: v2.2.5 hooks: - id: codespell args: diff --git a/NaviGator/gnc/navigator_msg_multiplexer/nodes/ogrid_arbiter.py b/NaviGator/gnc/navigator_msg_multiplexer/nodes/ogrid_arbiter.py index ac8fffd4f..eb425ec7a 100755 --- a/NaviGator/gnc/navigator_msg_multiplexer/nodes/ogrid_arbiter.py +++ b/NaviGator/gnc/navigator_msg_multiplexer/nodes/ogrid_arbiter.py @@ -493,10 +493,7 @@ def publish(self, timer_event): ) print(e) fprint( - "w: {}, h: {}".format( - global_ogrid.info.width, - global_ogrid.info.height, - ), + f"w: {global_ogrid.info.width}, h: {global_ogrid.info.height}", msg_color="red", ) diff --git a/NaviGator/gnc/navigator_thrust_mapper/navigator_thrust_mapper/thruster_map.py b/NaviGator/gnc/navigator_thrust_mapper/navigator_thrust_mapper/thruster_map.py index 588679455..2fc19700a 100644 --- a/NaviGator/gnc/navigator_thrust_mapper/navigator_thrust_mapper/thruster_map.py +++ b/NaviGator/gnc/navigator_thrust_mapper/navigator_thrust_mapper/thruster_map.py @@ -166,15 +166,11 @@ def from_urdf( if find != -1 and find + len(transmission_suffix) == len(transmission.name): if len(transmission.joints) != 1: raise Exception( - "Transmission {} does not have 1 joint".format( - transmission.name, - ), + f"Transmission {transmission.name} does not have 1 joint", ) if len(transmission.actuators) != 1: raise Exception( - "Transmission {} does not have 1 actuator".format( - transmission.name, - ), + f"Transmission {transmission.name} does not have 1 actuator", ) t_ratio = transmission.actuators[0].mechanicalReduction @@ -195,9 +191,7 @@ def from_urdf( joint = t_joint if joint is None: rospy.logerr( - "Transmission joint {} not found".format( - transmission.joints[0].name, - ), + f"Transmission joint {transmission.joints[0].name} not found", ) try: trans = buff.lookup_transform( @@ -217,9 +211,7 @@ def from_urdf( joints.append(joint.name) if limit != -1 and joint.limit.effort != limit: raise Exception( - "Thruster {} had a different limit, cannot proceed".format( - joint.name, - ), + f"Thruster {joint.name} had a different limit, cannot proceed", ) limit = joint.limit.effort limit_tuple = (limit, -limit) diff --git a/NaviGator/mission_control/navigator_alarm/navigator_alarm_handlers/kill.py b/NaviGator/mission_control/navigator_alarm/navigator_alarm_handlers/kill.py index a3f7a612b..547c56fa8 100644 --- a/NaviGator/mission_control/navigator_alarm/navigator_alarm_handlers/kill.py +++ b/NaviGator/mission_control/navigator_alarm/navigator_alarm_handlers/kill.py @@ -28,10 +28,7 @@ def _online_bagger_cb(self, status, result): rospy.loginfo(f"KILL BAG WRITTEN TO {result.filename}") else: rospy.logwarn( - "KILL BAG {}, status: {}".format( - TerminalState.to_string(status), - result.status, - ), + f"KILL BAG {TerminalState.to_string(status)}, status: {result.status}", ) def _kill_task_cb(self, status, result): @@ -39,10 +36,7 @@ def _kill_task_cb(self, status, result): rospy.loginfo("Killed task success!") return rospy.logwarn( - "Killed task failed ({}): {}".format( - TerminalState.to_string(status), - result.result, - ), + f"Killed task failed ({TerminalState.to_string(status)}): {result.result}", ) def raised(self, alarm): diff --git a/NaviGator/mission_control/navigator_alarm/navigator_alarm_handlers/thruster_fault.py b/NaviGator/mission_control/navigator_alarm/navigator_alarm_handlers/thruster_fault.py index a89612632..ce89ac3a0 100644 --- a/NaviGator/mission_control/navigator_alarm/navigator_alarm_handlers/thruster_fault.py +++ b/NaviGator/mission_control/navigator_alarm/navigator_alarm_handlers/thruster_fault.py @@ -65,9 +65,7 @@ def _check_faults(self, msg, topic): return self.broadcaster.raise_alarm( severity=5, - problem_description="{} thrusters have faults".format( - len(self._raised_alarms), - ), + problem_description=f"{len(self._raised_alarms)} thrusters have faults", parameters={ t: self._get_fault_codes(k) for t, k in self._raised_alarms.items() }, diff --git a/NaviGator/mission_control/navigator_missions/navigator_missions/detect_deliver_2016.py b/NaviGator/mission_control/navigator_missions/navigator_missions/detect_deliver_2016.py index bc95612b0..28c3ac4d9 100644 --- a/NaviGator/mission_control/navigator_missions/navigator_missions/detect_deliver_2016.py +++ b/NaviGator/mission_control/navigator_missions/navigator_missions/detect_deliver_2016.py @@ -240,10 +240,7 @@ async def search_sides(self, moves): self.shape_pose = found_pose return fprint( - "Saw (Shape={}, Color={}) on this side".format( - shape_color[0], - shape_color[1], - ), + f"Saw (Shape={shape_color[0]}, Color={shape_color[1]}) on this side", title="DETECT DELIVER", msg_color="green", ) @@ -287,10 +284,7 @@ def select_backup_shape(self): self.shape_pose = point_normal if self.Shape == shape or self.Color == color: fprint( - "Correct shape not found, resorting to shape={} color={}".format( - shape, - color, - ), + f"Correct shape not found, resorting to shape={shape} color={color}", title="DETECT DELIVER", msg_color="yellow", ) @@ -430,9 +424,7 @@ async def shoot_and_align_forest(self): move = await self.align_to_target() if move.failure_reason != "": fprint( - "Error Aligning with target = {}. Ending mission :(".format( - move.failure_reason, - ), + f"Error Aligning with target = {move.failure_reason}. Ending mission :(", title="DETECT DELIVER", msg_color="red", ) @@ -479,9 +471,7 @@ async def shoot_and_align(self): move = await self.align_to_target() if move.failure_reason != "": fprint( - "Error Aligning with target = {}. Ending mission :(".format( - move.failure_reason, - ), + f"Error Aligning with target = {move.failure_reason}. Ending mission :(", title="DETECT DELIVER", msg_color="red", ) diff --git a/NaviGator/mission_control/navigator_missions/navigator_missions/identify_dock.py b/NaviGator/mission_control/navigator_missions/navigator_missions/identify_dock.py index 28419f6b6..6d2203826 100644 --- a/NaviGator/mission_control/navigator_missions/navigator_missions/identify_dock.py +++ b/NaviGator/mission_control/navigator_missions/navigator_missions/identify_dock.py @@ -150,10 +150,7 @@ async def check_bays(self): def update_shape(self, shape_res, normal_res, tf): print_good( - "Found (Shape={}, Color={} in a bay".format( - shape_res.Shape, - shape_res.Color, - ), + f"Found (Shape={shape_res.Shape}, Color={shape_res.Color} in a bay", ) self.identified_shapes[(shape_res.Shape, shape_res.Color)] = self.get_shape_pos( normal_res, diff --git a/NaviGator/mission_control/navigator_missions/navigator_missions/move.py b/NaviGator/mission_control/navigator_missions/navigator_missions/move.py index 03017c09b..8ae428057 100755 --- a/NaviGator/mission_control/navigator_missions/navigator_missions/move.py +++ b/NaviGator/mission_control/navigator_missions/navigator_missions/move.py @@ -149,9 +149,7 @@ async def run(self, args): "yl": "yaw_left", "yr": "yaw_right", } - command = ( - command if command not in shorthand.keys() else shorthand[command] - ) + command = command if command not in shorthand else shorthand[command] movement = getattr(self.move, command) trans_move = command[:3] != "yaw" diff --git a/NaviGator/mission_control/navigator_missions/navigator_missions/navigation.py b/NaviGator/mission_control/navigator_missions/navigator_missions/navigation.py index 9b977b0bf..7812e5108 100644 --- a/NaviGator/mission_control/navigator_missions/navigator_missions/navigation.py +++ b/NaviGator/mission_control/navigator_missions/navigator_missions/navigation.py @@ -187,9 +187,7 @@ async def explore_closest_until(self, is_done, filter_and_sort): ) if classification_index != -1: self.send_feedback( - "{} identified. Canceling investigation".format( - move_id_tuple[1], - ), + f"{move_id_tuple[1]} identified. Canceling investigation", ) move_task.cancel() diff --git a/NaviGator/mission_control/navigator_missions/vrx_missions/vrx_navigation.py b/NaviGator/mission_control/navigator_missions/vrx_missions/vrx_navigation.py index 3086e39b5..7ef98c064 100644 --- a/NaviGator/mission_control/navigator_missions/vrx_missions/vrx_navigation.py +++ b/NaviGator/mission_control/navigator_missions/vrx_missions/vrx_navigation.py @@ -151,9 +151,7 @@ async def explore_closest_until(self, is_done, filter_and_sort): ) if classification_index != -1: self.send_feedback( - "{} identified. Canceling investigation".format( - move_id_tuple[1], - ), + f"{move_id_tuple[1]} identified. Canceling investigation", ) move_id_tuple[0].cancel() await self.nh.sleep(1.0) diff --git a/NaviGator/mission_control/navigator_missions/vrx_missions/vrx_perception.py b/NaviGator/mission_control/navigator_missions/vrx_missions/vrx_perception.py index f1483416f..2b1ef6d50 100644 --- a/NaviGator/mission_control/navigator_missions/vrx_missions/vrx_perception.py +++ b/NaviGator/mission_control/navigator_missions/vrx_missions/vrx_perception.py @@ -79,11 +79,7 @@ async def run(self, parameters): ) elif objects[key] != new_objects[key]: self.send_feedback( - "{} changed from {} to {}".format( - key, - objects[key], - new_objects[key], - ), + f"{key} changed from {objects[key]} to {new_objects[key]}", ) await self.announce_object( key, diff --git a/NaviGator/simulation/navigator_gazebo/test/test_sim_integration.py b/NaviGator/simulation/navigator_gazebo/test/test_sim_integration.py index 3cb7ab8a7..46e20b856 100755 --- a/NaviGator/simulation/navigator_gazebo/test/test_sim_integration.py +++ b/NaviGator/simulation/navigator_gazebo/test/test_sim_integration.py @@ -71,10 +71,7 @@ def test_odom(self): rospy.sleep(0.1) self.assertTrue( len(self.odom_pos_msg) == 3 and len(self.odom_ori_msg) == 4, - msg="POS, ORI: {}, {}".format( - len(self.odom_pos_msg), - len(self.odom_ori_msg), - ), + msg=f"POS, ORI: {len(self.odom_pos_msg)}, {len(self.odom_ori_msg)}", ) initial_pos = [-1.2319, 0.0, 0.0] initial_ori = [0.0, 0.0, 0.0, 1.0] @@ -100,10 +97,7 @@ def test_absodom(self): rospy.sleep(0.1) self.assertTrue( len(self.absodom_pos_msg) == 3 and len(self.absodom_ori_msg) == 4, - msg="POS, ORI: {}, {}".format( - len(self.absodom_pos_msg), - len(self.absodom_ori_msg), - ), + msg=f"POS, ORI: {len(self.absodom_pos_msg)}, {len(self.absodom_ori_msg)}", ) initial_pos = [743789.637462, -5503821.36715, 3125622.10477] initial_ori = [0.0, 0.0, 0.0, 1.0] @@ -122,25 +116,13 @@ def verify_pos_ori(self, pos, initial_pos, ori, initial_ori, topic): actual, initial, places=0, - msg=( - "Error: {} position is: {} should be {}".format( - topic, - actual, - initial, - ) - ), + msg=(f"Error: {topic} position is: {actual} should be {initial}"), ) for actual, initial in zip(ori, initial_ori): self.assertEqual( actual, initial, - msg=( - "Error: {} orientation is: {} should be {}".format( - topic, - actual, - initial, - ) - ), + msg=(f"Error: {topic} orientation is: {actual} should be {initial}"), ) def cam_info_right_cb(self, msg): @@ -229,10 +211,7 @@ def verify_not_empty(self, data_lists, num_topics): self.assertEqual( len(data_lists), num_topics, - msg="Number of topics is {}, should be {}".format( - len(data_lists), - num_topics, - ), + msg=f"Number of topics is {len(data_lists)}, should be {num_topics}", ) for data_list in data_lists: self.assertNotEqual(len(data_list), 0, msg="data is empty") @@ -243,13 +222,7 @@ def verify_info(self, res_info, initial_info, topic): self.assertNotEqual( actual_dim, initial_dim, - msg=( - "Error: {} is: {} shouldn't be {}".format( - topic, - actual_dim, - initial_dim, - ) - ), + msg=(f"Error: {topic} is: {actual_dim} shouldn't be {initial_dim}"), ) diff --git a/NaviGator/utils/navigator_robotx_comms/nodes/robotx_comms_client.py b/NaviGator/utils/navigator_robotx_comms/nodes/robotx_comms_client.py index d8583f010..59e6139e3 100755 --- a/NaviGator/utils/navigator_robotx_comms/nodes/robotx_comms_client.py +++ b/NaviGator/utils/navigator_robotx_comms/nodes/robotx_comms_client.py @@ -559,10 +559,7 @@ def connect(self) -> None: """ if not self.connected: rospy.loginfo( - "Attempting Connection to TD Server at {}:{}".format( - self.tcp_ip, - self.tcp_port, - ), + f"Attempting Connection to TD Server at {self.tcp_ip}:{self.tcp_port}", ) while not self.connected and not rospy.is_shutdown(): # recreate socket diff --git a/NaviGator/utils/navigator_tools/navigator_tools/object_database_helper.py b/NaviGator/utils/navigator_tools/navigator_tools/object_database_helper.py index 0798c78e0..5cb8762cb 100644 --- a/NaviGator/utils/navigator_tools/navigator_tools/object_database_helper.py +++ b/NaviGator/utils/navigator_tools/navigator_tools/object_database_helper.py @@ -1,4 +1,3 @@ -"""Use the DBHelper class to interface with the Database without having to deal with ROS things.""" import asyncio import sys import time @@ -14,10 +13,29 @@ class DBHelper: - """DBHelper class.""" + """ + Use the DBHelper class to interface with the Database without having to deal with ROS things. + + Attributes: + found (bool): checks whether the object is what the user has been looking for. + nh (axros.NodeHandle): processes database requests. + position (int): the current position of the object. + rot (): one of the dimensional values. + new_object_subscriber (): an object that is being called from the database. + ensuring_objects (bool): a variable state for the object. + ensuring_object_dep (): a list of objects being used for storage. + ensuring_object_cb (): this is considered when it is an "ensuring_objects". + looking_for (): a setter variable to which a name is being assigned. + is_found (bool): determines whether the object is found or not. + """ def __init__(self, nh): - """Initialize the DB helper class.""" + """ + Initialize the DB helper class. + + Args: + nh(NodeHandle): NodeHandle object + """ self.found = set() self.nh = nh self.position = None @@ -30,7 +48,12 @@ def __init__(self, nh): self.is_found = False async def init_(self, navigator=None): - """Initialize the axros parts of the DBHelper.""" + """ + Initialize the axros parts of the DBHelper. + + Args: + navigator (NaviGator | None): Base NaviGator object + """ # self._sub_database = yield self.nh.subscribe('/database/objects', PerceptionObjectArray, self.object_cb) self._database = self.nh.get_service_client("/database/requests", ObjectDBQuery) self.navigator = navigator @@ -43,21 +66,37 @@ async def init_(self, navigator=None): return self def _odom_cb(self, odom): + """ + Sets the position and the rot values. + + Args: + odom (object): Odom + """ self.position, self.rot = nt.odometry_to_numpy(odom)[0] async def get_object_by_id(self, my_id): + """ + Retrieves the object with the associated "my_id". + + Args: + my_id (int): an "id" of the object is passed in. + + Returns: + An individual object is being returned with a unique id. + """ print(my_id) req = ObjectDBQueryRequest() req.name = "all" resp = await self._database(req) - ans = [obj for obj in resp.objects if obj.id == my_id][0] + ans = next(obj for obj in resp.objects if obj.id == my_id) return ans async def begin_observing(self, cb): """ Get notified when a new object is added to the database. - cb : The callback that is called when a new object is added to the database + Args: + cb : The callback that is called when a new object is added to the database. """ self.new_object_subscriber = cb req = ObjectDBQueryRequest() @@ -78,6 +117,12 @@ async def begin_observing(self, cb): self.new_object_subscriber(o) async def get_unknown_and_low_conf(self): + """ + Gets unknown objects that are of low confidence. + + Returns: + Objects that meet certain criteria. + """ req = ObjectDBQueryRequest() req.name = "all" resp = await self._database(req) @@ -91,9 +136,21 @@ async def get_unknown_and_low_conf(self): return m def set_looking_for(self, name): + """ + Setting to a name that is being looked for. + + Args: + name (string): name of the object. + """ self.looking_for = name - def is_found_func(self): + def is_found_func(self) -> bool: + """ + Determines whether the object is being found or not. + + Returns: + The existence of the object is returned. + """ if self.is_found: self.looking_for = None self.is_found = False @@ -101,7 +158,12 @@ def is_found_func(self): return False def object_cb(self, perception_objects): - """Callback for the object database.""" + """ + Callback for the object database. + + Args: + perception_objects (object): a list of objects that are passed. + """ self.total_num = len(perception_objects.objects) for o in perception_objects.objects: if o.name == self.looking_for: @@ -120,12 +182,23 @@ def object_cb(self, perception_objects): self.ensuring_object_cb(missings_objs) def remove_found(self, name): - """Remove an object that has been listed as found.""" + """ + Remove an object that has been listed as found. + + Args: + name (string): a name of the object is being passed in. + """ self.found.remove(name) def ensure_object_permanence(self, object_dep, cb): - """Ensure that all the objects in the object_dep list remain in the database. - Call the callback if this isn't true.""" + """ + Ensure that all the objects in the object_dep list remain in the database. + Call the callback if this isn't true. + + Args: + object_dep: a new object is passed in order to be set. + cb : The callback that is called when a new object is added to the database. + """ if object_dep is None or cb is None: return self.ensuring_objects = True @@ -133,10 +206,21 @@ def ensure_object_permanence(self, object_dep, cb): self.ensuring_object_dep = object_dep def stop_ensuring_object_permanence(self): - """Stop ensuring that objects remain in the database.""" + """ + Stop ensuring that objects remain in the database. + """ self.ensuring_objects = False - def _wait_for_position(self, timeout=10): + def _wait_for_position(self, timeout=10) -> bool: + """ + A possible position is being stored in a variable. + + Args: + timeout (int): time limit to retrieve something. + + Returns: + bool: Determines whether the position is found within the time limit. + """ count = 0 while self.position is None: if self.navigator is not None: @@ -148,7 +232,15 @@ def _wait_for_position(self, timeout=10): return True async def get_closest_object(self, objects): - """Get the closest mission.""" + """ + Get the closest mission. + + Args: + objects (object): a list of objects are being passed in. + + Returns: + A object with the closest mission / distance is returned. + """ pobjs = [] for obj in objects: req = ObjectDBQueryRequest() @@ -171,6 +263,15 @@ async def get_closest_object(self, objects): return min_obj async def _dist(self, x): + """ + Finds the distance of the object keeping into account its current position. + + Args: + x (object): a specific object is being passed in order to retrieve its position. + + Returns: + the distance between the two objects is being returned. + """ if self.position is None: success = asyncio.create_task(self._wait_for_position) if not success: @@ -188,7 +289,18 @@ async def get_object( thresh=50, thresh_strict=50, ): - """Get an object from the database.""" + """ + Get an object from the database. + + Args: + object_name (string): the name of the object is passed in. + volume_only (bool): determines whether it is volume-based or not. + thresh (int): a maximum distance is passed in. + thresh_strict(int): a more strict maximum distance is passed in. + + Returns: + closest_potential_object (object): returns the object that is within the closest range. + """ if volume_only: req = ObjectDBQueryRequest() req.name = object_name @@ -233,6 +345,15 @@ async def get_object( return closest_potential_object def wait_for_additional_objects(self, timeout=60): + """ + Returns true/false whether additional objects are being passed in. + + Args: + timeout (int): maximum time limit of 60 seconds to run the program. + + Returns: + (bool): determines whether additional objects are added. + """ num_items = self.num_items start = time.time() while timeout < time.time() - start: @@ -241,14 +362,36 @@ def wait_for_additional_objects(self, timeout=60): return False def set_color(self, color, name): - """Set the color of an object in the database.""" + """ + Sets the color of an object in the database. + + Args: + color: the new color of the object is passed in. + name: the name of the object is passed in. + """ raise NotImplementedError def set_fake_position(self, pos): - """Set the position of a fake perception object.""" + """ + Sets the position of a fake perception object. + + Args: + pos (int): the new position of the object is passed in. + """ raise NotImplementedError async def get_objects_in_radius(self, pos, radius, objects="all"): + """ + Retrieves the objects that are present within the radius. + + Args: + pos (int): the position of the object is passed in. + radius (int): the radius of the object is passed in. + objects (object): this is a list of all the objects that needs to be compared. + + Returns: + ans (object): returns the objects that are present within the radius. + """ req = ObjectDBQueryRequest() req.name = "all" resp = await self._database(req) diff --git a/NaviGator/utils/navigator_tools/nodes/estimated_object_setter.py b/NaviGator/utils/navigator_tools/nodes/estimated_object_setter.py index 99845b004..57d4b7c01 100755 --- a/NaviGator/utils/navigator_tools/nodes/estimated_object_setter.py +++ b/NaviGator/utils/navigator_tools/nodes/estimated_object_setter.py @@ -24,7 +24,7 @@ async def main(name, lla): name = "_".join(txt.title() for txt in name.split("_")) point = await convert.request(CoordinateConversionRequest(frame="lla", point=lla)) - await db(ObjectDBQueryRequest(cmd="{}={p[0]}, {p[1]}".format(name, p=point.enu))) + await db(ObjectDBQueryRequest(cmd=f"{name}={point.enu[0]}, {point.enu[1]}")) if __name__ == "__main__": diff --git a/SubjuGator/command/subjugator_alarm/alarm_handlers/kill.py b/SubjuGator/command/subjugator_alarm/alarm_handlers/kill.py index 4781a760a..4055694d8 100644 --- a/SubjuGator/command/subjugator_alarm/alarm_handlers/kill.py +++ b/SubjuGator/command/subjugator_alarm/alarm_handlers/kill.py @@ -101,10 +101,7 @@ def _bag_done_cb(self, status, result): rospy.loginfo(f"KILL BAG WRITTEN TO {result.filename}") else: rospy.logwarn( - "KILL BAG {}, status: {}".format( - TerminalState.to_string(status), - result.status, - ), + f"KILL BAG {TerminalState.to_string(status)}, status: {result.status}", ) def bagger_dump(self): diff --git a/SubjuGator/command/subjugator_alarm/alarm_handlers/odom_kill.py b/SubjuGator/command/subjugator_alarm/alarm_handlers/odom_kill.py index 4a7638909..90a8e817b 100644 --- a/SubjuGator/command/subjugator_alarm/alarm_handlers/odom_kill.py +++ b/SubjuGator/command/subjugator_alarm/alarm_handlers/odom_kill.py @@ -101,9 +101,7 @@ def check_continuity(self, new_odom_msg: Odometry): # True if 'continuous' if jump > self.MAX_JUMP: rospy.logerr("ODOM DISCONTINUITY DETECTED") self.ab.raise_alarm( - problem_description="ODOM DISCONTINUITY DETECTED JUMPED {} METERS".format( - jump, - ), + problem_description=f"ODOM DISCONTINUITY DETECTED JUMPED {jump} METERS", severity=5, ) self.odom_discontinuity = True @@ -120,9 +118,7 @@ def need_kill(self): if odom_loss: rospy.logerr_throttle( 1, - "LOST ODOM FOR {} SECONDS".format( - (rospy.Time.now() - self.last_time).to_sec(), - ), + f"LOST ODOM FOR {(rospy.Time.now() - self.last_time).to_sec()} SECONDS", ) self.ab.raise_alarm( problem_description="LOST ODOM FOR {} SECONDS".format( diff --git a/SubjuGator/command/subjugator_keyboard_control/teleop_twist_keyboard.py b/SubjuGator/command/subjugator_keyboard_control/teleop_twist_keyboard.py index ea5c7bedb..870df6ac7 100755 --- a/SubjuGator/command/subjugator_keyboard_control/teleop_twist_keyboard.py +++ b/SubjuGator/command/subjugator_keyboard_control/teleop_twist_keyboard.py @@ -141,9 +141,7 @@ def wait_for_subscribers(self): while not rospy.is_shutdown() and self.publisher.get_num_connections() == 0: if i == 4: print( - "Waiting for subscriber to connect to {}".format( - self.publisher.name, - ), + f"Waiting for subscriber to connect to {self.publisher.name}", ) rospy.sleep(0.5) i += 1 diff --git a/SubjuGator/command/subjugator_launch/config/adaptive_controller_gazebo.yaml b/SubjuGator/command/subjugator_launch/config/adaptive_controller_gazebo.yaml new file mode 100644 index 000000000..c6406c44d --- /dev/null +++ b/SubjuGator/command/subjugator_launch/config/adaptive_controller_gazebo.yaml @@ -0,0 +1,6 @@ +--- +kp: 1000, 1000, 1000, 5000, 5000, 5000 +kd: 150, 150, 150, 50, 100, 25 +kg: 5,5,5,5,5,5 +ki: 5,5,5,5,10,5 +use_learned: false diff --git a/SubjuGator/command/subjugator_launch/launch/sub8.launch b/SubjuGator/command/subjugator_launch/launch/sub8.launch index 178efc6bb..247fe1e62 100644 --- a/SubjuGator/command/subjugator_launch/launch/sub8.launch +++ b/SubjuGator/command/subjugator_launch/launch/sub8.launch @@ -39,7 +39,9 @@ - + + + diff --git a/SubjuGator/command/subjugator_launch/launch/subsystems/adaptive_controller.launch b/SubjuGator/command/subjugator_launch/launch/subsystems/adaptive_controller.launch index 17bd1d3ac..a758c4278 100644 --- a/SubjuGator/command/subjugator_launch/launch/subsystems/adaptive_controller.launch +++ b/SubjuGator/command/subjugator_launch/launch/subsystems/adaptive_controller.launch @@ -1,8 +1,10 @@ - + + - - + + + diff --git a/SubjuGator/command/subjugator_missions/subjugator_missions/arm_torpedos.py b/SubjuGator/command/subjugator_missions/subjugator_missions/arm_torpedos.py index aad130049..7a88e8908 100644 --- a/SubjuGator/command/subjugator_missions/subjugator_missions/arm_torpedos.py +++ b/SubjuGator/command/subjugator_missions/subjugator_missions/arm_torpedos.py @@ -170,9 +170,7 @@ async def fire(self, target: str): ) self.print_good( - "{} locked. Firing torpedoes. Hit confirmed, good job Commander.".format( - target, - ), + f"{target} locked. Firing torpedoes. Hit confirmed, good job Commander.", ) sub_pos = await self.tx_pose() print("Current Sub Position: ", sub_pos) diff --git a/SubjuGator/command/subjugator_missions/subjugator_missions/start_gate.py b/SubjuGator/command/subjugator_missions/subjugator_missions/start_gate.py index f395bd1e0..0886298e1 100644 --- a/SubjuGator/command/subjugator_missions/subjugator_missions/start_gate.py +++ b/SubjuGator/command/subjugator_missions/subjugator_missions/start_gate.py @@ -160,9 +160,7 @@ def find_gate( p2 = rosmsg_to_numpy(o2.pose.position) if distance.euclidean(p, p2) > max_distance_away: fprint( - "Poles too far away. Distance {}".format( - distance.euclidean(p, p2), - ), + f"Poles too far away. Distance {distance.euclidean(p, p2)}", ) continue if distance.euclidean(p, p2) < min_distance_away: @@ -180,10 +178,7 @@ def find_gate( continue if abs(line[0]) < 1 and abs(line[1]) < 1: fprint( - "Objects on top of one another. x {}, y {}".format( - line[0], - line[1], - ), + f"Objects on top of one another. x {line[0]}, y {line[1]}", ) continue return (p, p2) diff --git a/SubjuGator/command/subjugator_missions/subjugator_missions/sub_singleton.py b/SubjuGator/command/subjugator_missions/subjugator_missions/sub_singleton.py index 693fc3bd7..8e498fc4d 100644 --- a/SubjuGator/command/subjugator_missions/subjugator_missions/sub_singleton.py +++ b/SubjuGator/command/subjugator_missions/subjugator_missions/sub_singleton.py @@ -599,7 +599,7 @@ async def start_search_in_cone( distance_tol: float = 10, speed: float = 0.5, clear: bool = False, - c_func: Callable = None, + c_func: Callable | None = None, ): if clear: print("SONAR_OBJECTS: clearing pointcloud") diff --git a/SubjuGator/drivers/magnetic_compensation/sub8_magnetic_hardsoft_compensation/scripts/generate_config b/SubjuGator/drivers/magnetic_compensation/sub8_magnetic_hardsoft_compensation/scripts/generate_config index 86ce1a09a..4b65395af 100755 --- a/SubjuGator/drivers/magnetic_compensation/sub8_magnetic_hardsoft_compensation/scripts/generate_config +++ b/SubjuGator/drivers/magnetic_compensation/sub8_magnetic_hardsoft_compensation/scripts/generate_config @@ -111,9 +111,7 @@ def test(): assert numpy.allclose( shift2, shift, - ), "Magnetic Hardsoft Compenstion self-test failed, shifts: {}".format( - (shift2, shift), - ) + ), f"Magnetic Hardsoft Compenstion self-test failed, shifts: {(shift2, shift)}" assert ( error < 1e-5 ), f"Magnetic Hardsoft Compenstion self-test failed, error: {error}" diff --git a/SubjuGator/drivers/sub8_thrust_and_kill_board/sub8_thrust_and_kill_board/handle.py b/SubjuGator/drivers/sub8_thrust_and_kill_board/sub8_thrust_and_kill_board/handle.py index c62f5732b..19df02c33 100644 --- a/SubjuGator/drivers/sub8_thrust_and_kill_board/sub8_thrust_and_kill_board/handle.py +++ b/SubjuGator/drivers/sub8_thrust_and_kill_board/sub8_thrust_and_kill_board/handle.py @@ -109,9 +109,7 @@ def on_command(self, msg: Thrust) -> None: # If we don't have a mapping for this thruster, ignore it if cmd.name not in self.thrusters: rospy.logwarn( - "Command received for {}, but this is not a thruster.".format( - cmd.name, - ), + f"Command received for {cmd.name}, but this is not a thruster.", ) continue # Map commanded thrust (in newetons) to effort value (-1 to 1) diff --git a/SubjuGator/drivers/sub8_thrust_and_kill_board/sub8_thrust_and_kill_board/packets.py b/SubjuGator/drivers/sub8_thrust_and_kill_board/sub8_thrust_and_kill_board/packets.py index 8cc3a0f27..dcd1aaa0b 100644 --- a/SubjuGator/drivers/sub8_thrust_and_kill_board/sub8_thrust_and_kill_board/packets.py +++ b/SubjuGator/drivers/sub8_thrust_and_kill_board/sub8_thrust_and_kill_board/packets.py @@ -287,7 +287,4 @@ def command(self) -> float: return struct.unpack("f", self.payload[1:])[0] def __str__(self): - return "ThrustPacket(thruster_id={}, command={})".format( - self.thruster_id, - self.command, - ) + return f"ThrustPacket(thruster_id={self.thruster_id}, command={self.command})" diff --git a/SubjuGator/perception/subjugator_perception/ml_classifiers/path_marker/path_localizer.py b/SubjuGator/perception/subjugator_perception/ml_classifiers/path_marker/path_localizer.py index 29665f12a..c70e5e258 100755 --- a/SubjuGator/perception/subjugator_perception/ml_classifiers/path_marker/path_localizer.py +++ b/SubjuGator/perception/subjugator_perception/ml_classifiers/path_marker/path_localizer.py @@ -17,7 +17,7 @@ rospack.get_path("subjugator_perception") + "/ml_classifiers/path_marker/utils", ) -from utils import detector_utils # noqa: manually appending sys.path +from utils import detector_utils # noqa class classifier: diff --git a/docs/images/gazebo/Bottom_Toolbar.png b/docs/images/gazebo/Bottom_Toolbar.png new file mode 100644 index 000000000..de1fb6d98 Binary files /dev/null and b/docs/images/gazebo/Bottom_Toolbar.png differ diff --git a/docs/images/gazebo/Interface.png b/docs/images/gazebo/Interface.png new file mode 100644 index 000000000..b6f5018c4 Binary files /dev/null and b/docs/images/gazebo/Interface.png differ diff --git a/docs/images/gazebo/Labeled_Interface.png b/docs/images/gazebo/Labeled_Interface.png new file mode 100644 index 000000000..871ddca52 Binary files /dev/null and b/docs/images/gazebo/Labeled_Interface.png differ diff --git a/docs/images/gazebo/Menus.png b/docs/images/gazebo/Menus.png new file mode 100644 index 000000000..d6c39d62e Binary files /dev/null and b/docs/images/gazebo/Menus.png differ diff --git a/docs/images/gazebo/Model_Editor.png b/docs/images/gazebo/Model_Editor.png new file mode 100644 index 000000000..362f7cb16 Binary files /dev/null and b/docs/images/gazebo/Model_Editor.png differ diff --git a/docs/images/gazebo/Mouse.png b/docs/images/gazebo/Mouse.png new file mode 100644 index 000000000..728c8fc8f Binary files /dev/null and b/docs/images/gazebo/Mouse.png differ diff --git a/docs/images/gazebo/TrackPad.png b/docs/images/gazebo/TrackPad.png new file mode 100644 index 000000000..1231be57b Binary files /dev/null and b/docs/images/gazebo/TrackPad.png differ diff --git a/docs/images/gazebo/Upper_Toolbar.png b/docs/images/gazebo/Upper_Toolbar.png new file mode 100644 index 000000000..68e36e69a Binary files /dev/null and b/docs/images/gazebo/Upper_Toolbar.png differ diff --git a/docs/software/gazebo_guide.md b/docs/software/gazebo_guide.md new file mode 100644 index 000000000..752147b10 --- /dev/null +++ b/docs/software/gazebo_guide.md @@ -0,0 +1,327 @@ +# Gazebo Guide + +This is a guide on how to use Gazebo, including launching the sub and +viewing it inside Gazebo, how Gazebo can be controlled and manipulated, how to +use Gazebo to make world files, and where you can find more info on Gazebo. + +## Brief Introduction + +Gazebo is an open-source 3D dynamic simulator, that allows us to design, model, +and test robots and their behavior in a virtual world. Similar to game engines +like Unity, Gazebo takes in scripts and models and performs physics simulations +on them. While it is similar, Gazebo offers physics simulations at a higher level +of fidelity, a suite of sensors, and interfaces for both users and programs. + +There are many versions of Gazebo, but this guide was written for **Gazebo +Classic 11** as this is the version of Gazebo currently being used at MIL. + +### Running Gazebo + +There are many ways run Gazebo. + +* Click the "Show Applications" button on Ubuntu (the apps button located in + the bottom left corner). Then search for the Gazebo Icon and press that icon + to open Gazebo. + +* You can press Alt + F2 (or Alt+Fn+F2) to bring up the "Run a Command" window. + Then type "gazebo" and press Enter to open Gazebo. + +* You can also open a terminal and type "gazebo" and it will open Gazebo. + +To launch Gazebo will all the necessary files for simulating Subjugator, +follow these steps: + +1. Open a terminal window and execute the following command. This command uses + ROS to start all the relevant ROS nodes and to load the world file for + subjugator. This also starts a Gazebo Sever. + + ```bash + roslaunch subjugator_launch gazebo.launch --screen + ``` + + :::{note} + `--screen` forces all ROS node output to the screen. It is used for debugging. + ::: + +1. Then in another terminal window run this command to start the Gazebo + graphical client, which connects to the Gazebo Sever. + + ```bash + gazebogui + ``` + +1. Then in another terminal window run this command and then press Shift-C to + unkill the sub to allow movement. + + ```bash + amonitor kill + ``` + +1. Execute the following command to start a specific mission, replacing + "StartGate2022" with the name of the desired mission: + + ```bash + mission run StartGate2022 + ``` + +## How to use Gazebo + +### User Interface + +When you launch Gazebo you will be greeted by its user interface. + +![Gazebo Interface](/images/gazebo/Interface.png) + +The Gazebo interface consists of three main sections: The **Left Panel**, the +**Scene**, and the **Right Panel**. By default the Right Panel is hidden. This +is because we do not have anything selected. To show the right panel you can +always Click and drag the bar on the right to open it. + +![Gazebo Labeled Interface](/images/gazebo/Labeled_Interface.png) + +#### Left Panel + +The Left Panel has three tabs, each with different features. You can see +these tabs at the top of the Left Panel. You can click on them to switch +between them. The tabs are: + +##### World Tab + +The World Tab displays the models that are currently in the scene. Within this +tab, you can view and modify various model parameters, like their pose (their +position and rotation). Additionally, you can expand the GUI option to adjust +the camera view angle by modifying the camera pose. + +##### Insert Tab + +The Insert Tab allows you to add new models (objects) to the Gazebo simulation. +Here, you will find a list of file paths where your models are saved. To view +the model list, click on the arrow located on the left side of each path to +expand the folder. Select the desired model and click again in the scene to +place it. + +##### Layers Tab + +The Layers tab organizes and displays different visualization groups within +the simulation. Layers can contain one or more models, and enabling or disabling +a layer will show or hide all the models within it. While not mandatory, layers +can be helpful for organizing your simulation. Note that this tab may be empty +if no layers are defined. + +To define a layer, you will need to edit a model's SDF file. To add an +object's visuals to a layer you will need to add a `` tag for information +and then a `` tag with the layer number under each `` tag. Below +is an example: + +```xml + + + 0 + + ... + +``` + +#### Scene + +The Scene is the main window where objects are animated, and you interact with +the environment. Two toolbars are available: + +##### The Upper Toolbar + +The Upper Toolbar consists of various buttons that allow you to select, move, +rotate, and scale objects. It also provides options to create simple shapes, +as well as copy and paste objects. + +![Gazebo Upper Toolbar](/images/gazebo/Upper_Toolbar.png) + +##### The Bottom Toolbar + +The Bottom Toolbar displays information about the simulation time and its +relationship to real time. It helps you track the progress of your simulation. + +![Gazebo Bottom Toolbar](/images/gazebo/Bottom_Toolbar.png) + +#### Right Panel + +The Right Panel is used to interact with the mobile parts (joints) of a +selected model. It provides controls and settings specific to manipulating +the joints of a model. + +#### Menus (File, Edit, Camera, View, Window, Help) + +Most Linux apps have menus. These menus are usually tabs (file, edit, ...) at +the top left of an application. If you don't see it move your cursor to the +top of the application window and the menus should appear. Below describes +the features of each menu that Gazebo has. + +![Gazebo Menus](/images/gazebo/Menus.png) + +#### Mouse + +It is recommended that you use a mouse when using Gazebo. Below is a diagram +showing all the mouse controls. + +![Gazebo Mouse Controls](/images/gazebo/Mouse.png) + +However, if you want to use a trackpad you can. Below are the controls for +the trackpad: + +![Gazebo Mouse Controls](/images/gazebo/TrackPad.png) + +## How to Create Models + +The structure for most models in Gazebo is that the model is a folder that +contains a .config file, .SDF file(s), and .dae or .stl file(s). The config +file contains meta information about the model. The .SDF file contains +important simulation information like model definitions, the model's +positioning, its physical properties, etc. The .dae or .stl files contain 3D +mesh information. When creating a model it's recommended that you have all +these components. + +### Model Editor + +You can use the **Model Editor** to create simple models all within Gazebo, +but for more complex models you will want to create/write your own SDF files +and .dae files. + +To enter the **Model Editor**, click on Edit in the menu bar and select Model Editor. + +The Model Editor Interface looks similar to the regular Gazebo UI with some +slight changes. The left panel and the top toolbar have been changed to +contain only buttons and features for editing and creating parts of a model. +The bottom toolbar is now hidden as the simulation is paused. + +![Gazebo Model Editor](/images/gazebo/Model_Editor.png) + +When entering the Model Editor all other models will turn white. This can make +it hard to see the model you are currently working on if you have a lot of +models in your scene. So it may be easier to open a blank Gazebo world and +create the model using the Model Editor there. Then when you exit the Model +Editor it will ask you to save the model. This will save the model as a folder +on your computer. Then you can go back to the original world and insert +this model, by going to the insert tab (note this is the regular insert tab, +not the one in the model editor) and adding that model folder's file path. + +:::{note} +When inserting a model, make sure that the file path you pick is the path to +the parent directory. This directory contains the model folder you want to +insert. Do not put the path to the model folder. Often this parent +directory will contain all the models you want to use. The file hierarchy +might look like this: where models is the parent directory and contains the +models model1 and buoys. + +``` + models/ + ├── model_1/ + │ ├── model.config + │ ├── model1.sdf + │ ├── model1.dae + │ └── ... + ├── buoys/ + │ ├── model.config + │ ├── green.sdf + │ ├── red.sdf + │ └── ... +``` + +::: + +#### Insert Tab + +The Insert Tab allows you to add new parts, including links and models, to +your model. You have two options for inserting shapes (links): + +* Simple Shapes: Click on the desired shape in the left panel and then click + again in the scene to place it. + +* Custom Shapes: You can add COLLADA (.dae), 3D Systems (.stl), Wavefront + (.obj), and W3C SVG (.svg) files as custom shapes. Create these shapes + using 3D modeling software like Blender. + +You can also insert other models into your model as nested models. These +models can be obtained from the Gazebo Model Database +(http://gazebosim.org/models/), which should be listed as one of your file +paths under Model Databases. For example, if you need a depth sensor, you can +add a depth sensor model from the database to your model. + +#### Model Tab + +The Model Tab displays the settings for the model you are creating. Here, you +can change the model's name and modify its basic parameters. Additionally, you +can add plugins to give your model functionality here as well. + +#### Placing Shapes + +Once you insert a shape, you can use the toolbar to move, rotate, and scale +it. For finer control, you can double-click the shape or right-click and +select "Open Link Inspector" to access the link inspector. In the link +inspector, you can modify the shape's position, rotation, and scale to +achieve the desired configuration. Make sure to adjust the scale in both the +Visual and Collision tabs. + +#### Adding Joints + +To constrain the motion between shapes, you can add joints. Follow these steps: + +* Click on the joint icon in the toolbar (a line connecting two points). + +* In the Joint Creation Window, select the parent and child links (shapes) + of the joint. + +* Select the type of joint you need in the Joint Types section near the top + of the window. + +* Select the joint axis. Some joints do not have an axis. + +* Align the link (shape). Use the align links section to align the parent + and the child with each other. + +#### Adding a Plugin + +To control your model, you need to create a plugin. You can do this in the +Model Tab by specifying the necessary details for the plugin. + +You can find more information on how to create your own custom plugins [here](https://classic.gazebosim.org/tutorials?tut=ros_gzplugins). + +#### Model Creation Workflow Example + +To illustrate the model creation process, let's consider creating a car model +using Blender: + +* Create .dae files for the wheels, chassis, and other parts in Blender. + +* Insert these shapes into the Model Editor. + +* Use the toolbar and link inspector to position each shape precisely. + +* Add joints between the shapes to enable motion constraints. + +* Finally, create a plugin to control the model's behavior. + +### World File + +A World in Gazebo is used to describe the collection of robots and objects, +and global parameters like the sky, ambient light, and physics properties. +A World is the entire virtual environment that you have been +working in. The World stores important information like where all the models +are, their properties, and important global properties. + +You can save the World file by selecting File and Save World As. + +:::{note} +When using roslaunch to start Gazebo, it is crucial to update the World file +if you make any changes to the simulation environment. At MIL, there is a +dedicated `worlds` folder where Gazebo World files are saved. When you update +a World file, ensure that you replace the old file in this folder. Failing +to do so will result in the continued use of the old World file when +launching Gazebo using roslaunch. +::: + +## More Info + +If you ever need more information on how any aspect of Gazebo works or how to +use ROS with Gazebo you can check out the official Gazebo Documentation [here](https://classic.gazebosim.org/tutorials). +Some of the images used in this guide are sourced from here and we are grateful +to the creators for their exceptional work, which has been instrumental in +writing this guide. diff --git a/docs/software/index.rst b/docs/software/index.rst index 61b036c7c..9e779a0eb 100644 --- a/docs/software/index.rst +++ b/docs/software/index.rst @@ -13,10 +13,12 @@ Various documentation related to practices followed by the MIL Software team. help zobelisk asyncio + rqt rostest Bash Style Guide C++ Style Guide Python Style Guide + Gazebo Guide Migrating to ROS Noetic Installing NVIDIA Drivers for RTX 2080 Installing Ubuntu 18.04 on an M-series Apple computer diff --git a/docs/software/rqt.md b/docs/software/rqt.md new file mode 100644 index 000000000..258c43522 --- /dev/null +++ b/docs/software/rqt.md @@ -0,0 +1,437 @@ +# Implementing GUI Based Tools Using `rqt` + +## Introduction + +RQT (ROS Qt-based GUI Framework) is a powerful tool that provides a graphical +user interface for ROS (Robot Operating System) applications. It allows users +to monitor ROS topics, interact with nodes, visualize data, and more. This +document serves as a user guide for understanding and effectively using RQT. + +### Benefits of RQT + +- **Graphical Interface**: RQT provides a user-friendly graphical interface, + making it easier to visualize data, monitor topics, and interact with nodes. + This is especially helpful for users who prefer a GUI based approach over + the command line. + +- **Centralized Tool**: RQT serves as a centralized tool for various ROS tasks, + including visualization, configuration, and debugging. This reduces the need + to switch between multiple terminals and tools, streamlining the development + process. + +- **Extensibility**: RQT's plugin architecture allows users to create custom + plugins to tailor the tool to their specific application needs. This + extensibility significantly enhances RQT's capabilities and to various + robotic systems. + +- **Integration with ROS**: RQT is tightly integrated with ROS, ensuring + seamless interaction with ROS nodes, topics, and services. This integration + simplifies the process of debugging and analyzing data in real-time. + +## Installation + +Before using RQT, make sure you have ROS installed on your system. If ROS is +not yet installed, follow the official ROS installation guide for your +operating system. Once ROS is installed, you can install RQT using the package +manager: + +```bash +sudo apt-get install ros-noetic-rqt +``` + +## Getting Started + +### Launching RQT + +To launch RQT, open a terminal and run the following command: + +```bash +rqt +``` + +This command will launch the RQT Graphical User Interface, displaying a +default layout with available plugins. + +## Monitoring Information with RQT + +RQT provides various plugins to monitor information published on specific ROS +topics. The most common plugin for this purpose is the Plot plugin. + +### Subscribing to Specific Topics + +1. Open RQT using the command mentioned in the previous section. +1. Click on the "Plugins" menu in the top toolbar and select "Topics" > + "Message Publisher." +1. A new "Message Publisher" tab will appear. Click on the "+" button to + subscribe to a specific topic. +1. Choose the desired topic from the list and click "OK." +1. Now, you can monitor the information published on the selected topic in + real-time. + +### Displaying Data with Plot Plugin + +1. Open RQT using the command mentioned earlier. +1. Click on the "Plugins" menu in the top toolbar and select + "Visualization" > "Plot." +1. A new "Plot" tab will appear. Click on the "+" button to add a plot. +1. Choose the topic you want to visualize from the list, select the message + field, and click "OK." +1. The plot will start displaying the data sent over the selected topic. + +## Configuring RQT + +RQT allows users to customize its appearance and layout to suit their preferences. + +### Theme Configuration + +1. Open RQT using the command mentioned earlier. +1. Click on the "View" menu in the top toolbar and select "Settings." +1. In the "Settings" window, go to the "Appearance" tab. +1. Here, you can change the color scheme, font size, and other visual settings. + +### Layout Configuration + +1. Open RQT using the command mentioned earlier. +1. Rearrange existing plugins by clicking on their tab and dragging them to + a new position. +1. To create a new tab, right-click on any existing tab and select "Add New Tab." +1. Drag and drop desired plugins onto the new tab. +1. Save your customized layout by going to the "View" menu and selecting + "Perspectives" > "Save Perspective." + +## Writing RQT Plugins + +RQT allows users to create their custom plugins to extend its functionality. +Writing RQT plugins is essential when the available plugins do not fully +meet your application's requirements. + +### Plugin Structure + +To write an RQT plugin, you need to follow a specific directory structure and +include Python or C++ code to implement the plugin's functionality. The +essential components of an RQT plugin are: + +- **Plugin XML (`plugin.xml`)**: This file defines the plugin and its + dependencies, specifying essential information like name, description, + and which ROS packages it depends on. + +- **Python or C++ Code**: This code contains the actual implementation of the + plugin's functionality. For Python plugins, the code should extend the + `rqt_gui_py::Plugin` class, while for C++ plugins, it should extend the + `rqt_gui_cpp::Plugin` class. + +### Implementing a Basic Plugin + +In this section, we will walk through an example of implementing a basic RQT +plugin. We will create a simple plugin to display the current time published +by a ROS topic. The plugin will consist of a label that updates with the +current time whenever a new message is received. + +#### Step 1: Create the Plugin Package + +Create a new ROS package for your plugin using the `catkin_create_pkg` command: + +```bash +catkin_create_pkg my_rqt_plugin rospy rqt_gui_py +``` + +The package dependencies `rospy` and `rqt_gui_py` are required to work with +ROS and create a Python-based RQT plugin. + +#### Step 2: Implement the Plugin Code + +Next, create a Python script for your plugin. In the `src` directory of your +package, create a file named `my_rqt_plugin.py` with the following content: + +```python +#!/usr/bin/env python + +import rospy +from rqt_gui_py.plugin import Plugin +from python_qt_binding.QtWidgets import QLabel, QVBoxLayout, QWidget +from std_msgs.msg import String +import time + +class MyRqtPlugin(Plugin): + + def __init__(self, context): + super(MyRqtPlugin, self).__init__(context) + self.setObjectName('MyRqtPlugin') + + # Create the main widget and layout + self._widget = QWidget() + layout = QVBoxLayout() + self._widget.setLayout(layout) + + # Create a label to display the current time + self._label = QLabel("Current Time: N/A") + layout.addWidget(self._label) + + # Subscribe to the topic that provides the current time + rospy.Subscriber('/current_time', String, self._update_time) + + # Add the widget to the context + context.add_widget(self._widget) + + def _update_time(self, msg): + # Update the label with the received time message + self._label.setText(f"Current Time: {msg.data}") + + def shutdown_plugin(self): + # Unregister the subscriber when the plugin is shut down + rospy.Subscriber('/current_time', String, self._update_time) + + def save_settings(self, plugin_settings, instance_settings): + # Save the plugin's settings when needed + pass + + def restore_settings(self, plugin_settings, instance_settings): + # Restore the plugin's settings when needed + pass + +``` + +In this code, we create a plugin class named `MyRqtPlugin`, which inherits +from `rqt_gui_py.Plugin`. The `__init__` method sets up the GUI elements, such +as a label, and subscribes to the `/current_time` topic to receive time updates. + +#### Step 3: Add the Plugin XML File + +Create a file named `plugin.xml` in the root of your package to define the +plugin. Add the following content to the `plugin.xml`: + +```xml + + + + A custom RQT plugin to display the current time. + + + + settings + Display the current time. + + + +``` + +This XML file defines the plugin class, its description, and the icon to be +displayed in the RQT GUI. + +#### Step 4: Build the Plugin + +Build your plugin package using `catkin_make`: + +```bash +catkin_make +``` + +#### Step 5: Launch RQT and Load the Plugin + +Launch RQT using the command mentioned earlier: + +```bash +rqt +``` + +Once RQT is open, navigate to the "Plugins" menu and select "My RQT Plugin" +from the list. The plugin will be loaded, and you should see the label +displaying the current time. + +### Adding Custom Functionality + +The example plugin we implemented is a simple demonstration of how to create +an RQT plugin. Depending on your application's requirements, you can add more +sophisticated features, such as custom data visualization, interactive +controls, and integration with other ROS elements. + +You can also improve the plugin by adding error handling, additional options +for time formatting, and the ability to pause/resume the time updates. + +## Implementing a More Advanced Plugin + +In this section, we will implement a more advanced RQT plugin that visualizes +data from multiple ROS topics using a 2D plot. This plugin will allow users to +choose which topics to plot and specify the message field to display on the X +and Y axes. Let's call this plugin "ROS Data Plotter." + +### Step 1: Create the Plugin Package + +Create a new ROS package for your plugin using the `catkin_create_pkg` command: + +```bash +catkin_create_pkg ros_data_plotter rospy rqt_gui_py matplotlib +``` + +The package dependencies `rospy`, `rqt_gui_py`, and `matplotlib` are required +to work with ROS, create a Python-based RQT plugin, and plot data, respectively. + +### Step 2: Implement the Plugin Code + +Next, create a Python script for your plugin. In the `src` directory of your +package, create a file named `ros_data_plotter.py` with the following content: + +```python +#!/usr/bin/env python + +import rospy +from rqt_gui_py.plugin import Plugin +from python_qt_binding.QtWidgets import QWidget, QVBoxLayout, QComboBox +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +from std_msgs.msg import Float32 # Modify this import based on your data type + +class ROSDataPlotter(Plugin): + + def __init__(self, context): + super(ROSDataPlotter, self).__init__(context) + self.setObjectName('ROSDataPlotter') + + # Create the main widget and layout + self._widget = QWidget() + layout = QVBoxLayout() + self._widget.setLayout(layout) + + # Create a combo box to select topics and message fields + self._topic_field_selector = QComboBox() + self._topic_field_selector.currentIndexChanged.connect(self._update_plot) + layout.addWidget(self._topic_field_selector) + + # Create the matplotlib figure and plot + self._figure = Figure() + self._canvas = FigureCanvas(self._figure) + layout.addWidget(self._canvas) + + # Initialize the plot + self._update_plot() + + # Add the widget to the context + context.add_widget(self._widget) + + def _update_plot(self): + # Clear the previous plot data + self._figure.clear() + axes = self._figure.add_subplot(111) + + # Get the selected topic and field from the combo box + selected_topic_field = self._topic_field_selector.currentText() + topic, field = selected_topic_field.split(' - ') + + # Subscribe to the selected topic + rospy.Subscriber(topic, Float32, self._plot_data) # Modify this based on your data type + + def _plot_data(self, msg): + # Get the selected topic and field from the combo box + selected_topic_field = self._topic_field_selector.currentText() + topic, field = selected_topic_field.split(' - ') + + # Plot the data on the graph + # Modify this part based on your data type and plot requirements + x_data = rospy.Time.now().to_sec() # Use timestamp as X-axis + y_data = getattr(msg, field) # Use the specified field from the message as Y-axis data + axes = self._figure.add_subplot(111) + axes.plot(x_data, y_data) + + # Refresh the plot + self._canvas.draw() + + def shutdown_plugin(self): + # Unsubscribe from the topic when the plugin is shut down + self._topic_field_selector.clear() + rospy.Subscriber(topic, Float32, self._plot_data) + + def save_settings(self, plugin_settings, instance_settings): + # Save the plugin's settings when needed + pass + + def restore_settings(self, plugin_settings, instance_settings): + # Restore the plugin's settings when needed + pass + +``` + +In this code, we create a plugin class named `ROSDataPlotter`, which inherits +from `rqt_gui_py.Plugin`. The `__init__` method sets up the GUI elements, +such as a combo box to select topics and message fields, and the matplotlib +figure to plot the data. The `_update_plot` method updates the plot whenever +a new topic or message field is selected from the combo box. The `_plot_data` +method is called when new messages are received on the selected topic, and it +plots the data on the graph. + +### Step 3: Add the Plugin XML File + +Create a file named `plugin.xml` in the root of your package to define the +plugin. Add the following content to the `plugin.xml`: + +```xml + + + + An advanced RQT plugin to plot data from multiple ROS topics. + + + + settings + Plot data from selected ROS topics. + + + +``` + +This XML file defines the plugin class, its description, and the icon +to be displayed in the RQT GUI. + +### Step 4: Build the Plugin + +Build your plugin package using `catkin_make`: + +```bash +catkin_make +``` + +### Step 5: Launch RQT and Load the Plugin + +Launch RQT + + using the command mentioned earlier: + +```bash +rqt +``` + +Once RQT is open, navigate to the "Plugins" menu and select "ROS Data Plotter" +from the list. The plugin will be loaded, and you should see a combo box to +select topics and fields, and a graph to display the plotted data. + +### Adding Custom Functionality + +In this more advanced example, we created an RQT plugin that allows users to +plot data from multiple ROS topics. The example focused on plotting Float32 +messages, but you can modify the `_plot_data` method to handle different +message types or plot multiple data series on the same graph. + +You can further enhance the plugin by adding features like legends, axis +labels, custom plot styles, data smoothing, and real-time updates. +Additionally, you can provide options to save the plotted data as CSV +or images for further analysis. + +With custom plugins, you have the freedom to tailor RQT to suit your +specific application's visualization needs, making it a versatile tool +for ROS developers. + +## External Resources + +- [ROS wiki page for rqt](https://wiki.ros.org/rqt) +- [GeorgiaTech RoboJacket's rqt tutorial](https://www.youtube.com/watch?v=o90IaCRje2I) +- [Creating Custom rqt Plugins](https://github.com/ChoKasem/rqt_tut) + +## Conclusion + +Congratulations! You have now learned the basics of using RQT, including +monitoring information sent over specific topics, configuring RQT to your +preferences, writing your custom plugins, and understanding the importance +of using RQT in ROS development. RQT provides a user-friendly and powerful +graphical interface for ROS applications, making it an essential tool for +developers and researchers working with ROS-based robotic systems. By +leveraging RQT's capabilities, you can streamline your development process +and gain valuable insights into your robot's behavior. diff --git a/docs/subjugator/index.rst b/docs/subjugator/index.rst index 7a9416713..e52976cde 100644 --- a/docs/subjugator/index.rst +++ b/docs/subjugator/index.rst @@ -18,7 +18,7 @@ SubjuGator 8 PID Controller Nav Tube Watercooling - + electrical RoboSub diff --git a/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub8/sub8_driver.py b/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub8/sub8_driver.py index 8236940ac..c03b318d2 100755 --- a/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub8/sub8_driver.py +++ b/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub8/sub8_driver.py @@ -85,9 +85,7 @@ def read_packet(self) -> bool: self.handles[packet.device].on_data(packet.data) else: rospy.logwarn( - "Message received for device {}, but no handle registered".format( - packet.device, - ), + f"Message received for device {packet.device}, but no handle registered", ) return True diff --git a/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub8/utils.py b/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub8/utils.py index 2d2558f57..0ef0cf9a1 100644 --- a/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub8/utils.py +++ b/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub8/utils.py @@ -26,10 +26,7 @@ class ChecksumException(USB2CANException): def __init__(self, calculated, expected): super().__init__( - "Checksum was calculated as {} but reported as {}".format( - calculated, - expected, - ), + f"Checksum was calculated as {calculated} but reported as {expected}", ) diff --git a/mil_common/mil_missions/mil_missions_gui/dashboard.py b/mil_common/mil_missions/mil_missions_gui/dashboard.py index f3f224243..440dc85b9 100755 --- a/mil_common/mil_missions/mil_missions_gui/dashboard.py +++ b/mil_common/mil_missions/mil_missions_gui/dashboard.py @@ -195,11 +195,10 @@ def ui_log(self, string): """ self.lock.acquire() date_time = datetime.datetime.fromtimestamp(rospy.Time.now().to_time()) - time_str = "{}:{}:{}".format( - date_time.hour, - date_time.minute, - date_time.second, - ).ljust(12, " ") + time_str = f"{date_time.hour}:{date_time.minute}:{date_time.second}".ljust( + 12, + " ", + ) formatted = time_str + string self.feedback_list.addItem(formatted) self.lock.release() @@ -242,9 +241,7 @@ def transition_cb(self, goal, handler): self.current_mission_status = terminal_state self.current_mission_status_label.setText(self.current_mission_status) self.ui_log( - "FINISHED: mission finished ({})".format( - self.current_mission_status, - ), + f"FINISHED: mission finished ({self.current_mission_status})", ) def reload_available_missions(self, _): diff --git a/mil_common/mil_missions/nodes/mission_client b/mil_common/mil_missions/nodes/mission_client index 2ff81d944..230547f8e 100755 --- a/mil_common/mil_missions/nodes/mission_client +++ b/mil_common/mil_missions/nodes/mission_client @@ -30,9 +30,7 @@ class MissionClientCli: missions = MissionClient.available_missions() if not missions: print( - "{} param not set. Is mission runner running?".format( - MissionClient.LIST_PARAM, - ), + f"{MissionClient.LIST_PARAM} param not set. Is mission runner running?", ) return print("Available Missions:") diff --git a/mil_common/mil_missions/test/test_import_missions.py b/mil_common/mil_missions/test/test_import_missions.py index e03fb7e82..c09c8f13b 100755 --- a/mil_common/mil_missions/test/test_import_missions.py +++ b/mil_common/mil_missions/test/test_import_missions.py @@ -21,10 +21,7 @@ def test_import_missions(self): if not hasattr(mission_module, self.base_class): self.fail( - msg="{} doesn't have base mission {}".format( - self.module, - self.base_class, - ), + msg=f"{self.module} doesn't have base mission {self.base_class}", ) base_mission = getattr(mission_module, self.base_class) diff --git a/mil_common/perception/mil_vision/mil_vision_tools/cv_tools.py b/mil_common/perception/mil_vision/mil_vision_tools/cv_tools.py index dcfb694a1..42b6112b2 100644 --- a/mil_common/perception/mil_vision/mil_vision_tools/cv_tools.py +++ b/mil_common/perception/mil_vision/mil_vision_tools/cv_tools.py @@ -79,7 +79,7 @@ def __init__( self.conversion_code = conversion_code @classmethod - def from_dict(cls, d, in_space: str = "BGR", thresh_space: str = None): + def from_dict(cls, d, in_space: str = "BGR", thresh_space: Optional[str] = None): """ Loads thresholds from a dictionary. See examples for valid dictionaries. @@ -114,9 +114,7 @@ def from_dict(cls, d, in_space: str = "BGR", thresh_space: str = None): d.keys(), ), ) - assert thresh_space in d, "{} color space not in dictionary".format( - thresh_space, - ) + assert thresh_space in d, f"{thresh_space} color space not in dictionary" inner = d[thresh_space] if "low" in inner and "high" in inner: return cls( diff --git a/mil_common/utils/mil_tools/mil_ros_tools/extract_bag_images.py b/mil_common/utils/mil_tools/mil_ros_tools/extract_bag_images.py index c243ae950..22e52987c 100755 --- a/mil_common/utils/mil_tools/mil_ros_tools/extract_bag_images.py +++ b/mil_common/utils/mil_tools/mil_ros_tools/extract_bag_images.py @@ -150,17 +150,11 @@ def get_camera_model(bag: str, topic: str): _, msg, _ = next(bag.read_messages(topics=camera_info_topic)) except StopIteration: raise Exception( - "no camera info messages found on topic {} in {}".format( - camera_info_topic, - bag, - ), + f"no camera info messages found on topic {camera_info_topic} in {bag}", ) if msg._type != "sensor_msgs/CameraInfo": raise Exception( - "msg on topic {} are not camera info in bag {}".format( - camera_info_topic, - bag, - ), + f"msg on topic {camera_info_topic} are not camera info in bag {bag}", ) model = PinholeCameraModel() model.fromCameraInfo(msg) @@ -218,10 +212,7 @@ def extract_images(self, source_dir=".", image_dir=".", verbose=False): """ if verbose: print( - "\tExtracting images from topic {} in {}".format( - self.topic, - self.filename, - ), + f"\tExtracting images from topic {self.topic} in {self.filename}", ) filename = os.path.join(source_dir, self.filename) b = rosbag.Bag(filename) diff --git a/mil_common/utils/mil_tools/mil_ros_tools/label_me_bag.py b/mil_common/utils/mil_tools/mil_ros_tools/label_me_bag.py index d3a98fa67..4e6c7231c 100755 --- a/mil_common/utils/mil_tools/mil_ros_tools/label_me_bag.py +++ b/mil_common/utils/mil_tools/mil_ros_tools/label_me_bag.py @@ -263,10 +263,7 @@ def completion_report(self): total_img_count += i if total_img_count == 0: print( - "{}/{} TOTAL images labeled (0%)".format( - total_xml_count, - total_img_count, - ), + f"{total_xml_count}/{total_img_count} TOTAL images labeled (0%)", ) else: print( diff --git a/mil_common/utils/mil_tools/mil_ros_tools/msg_helpers.py b/mil_common/utils/mil_tools/mil_ros_tools/msg_helpers.py index 9ffa9b2d9..8ab315818 100644 --- a/mil_common/utils/mil_tools/mil_ros_tools/msg_helpers.py +++ b/mil_common/utils/mil_tools/mil_ros_tools/msg_helpers.py @@ -58,10 +58,7 @@ def rosmsg_to_numpy(rosmsg, keys=None): break assert ( len(output_array) != 0 - ), "Input type {} has none of these attributes {}.".format( - type(rosmsg).__name__, - keys, - ) + ), f"Input type {type(rosmsg).__name__} has none of these attributes {keys}." return np.array(output_array).astype(np.float32) else: diff --git a/mil_common/utils/mil_tools/nodes/online_bagger.py b/mil_common/utils/mil_tools/nodes/online_bagger.py index 2580f5cf2..5a0c6bb32 100755 --- a/mil_common/utils/mil_tools/nodes/online_bagger.py +++ b/mil_common/utils/mil_tools/nodes/online_bagger.py @@ -71,10 +71,7 @@ def get_subscriber_list(self, status): sub_list = "" for topic in self.subscriber_list: if self.subscriber_list[topic][1] == status: - sub_list = sub_list + "\n{:13}, {}".format( - self.subscriber_list[topic], - topic, - ) + sub_list = sub_list + f"\n{self.subscriber_list[topic]:13}, {topic}" return sub_list def get_params(self): diff --git a/mil_common/utils/mil_tools/nodes/tf_fudger.py b/mil_common/utils/mil_tools/nodes/tf_fudger.py index 58a647757..0ae508c98 100755 --- a/mil_common/utils/mil_tools/nodes/tf_fudger.py +++ b/mil_common/utils/mil_tools/nodes/tf_fudger.py @@ -45,10 +45,7 @@ ang_res = ang_range / ang_max print( - "Distance resolution: {} meters\nAngular resolution: {} radians".format( - x_res, - ang_res, - ), + f"Distance resolution: {x_res} meters\nAngular resolution: {ang_res} radians", ) cv2.createTrackbar("x", "tf", 0, x_max, lambda x: x) diff --git a/mil_common/utils/mil_tools/nodes/tf_to_gazebo.py b/mil_common/utils/mil_tools/nodes/tf_to_gazebo.py index b587dfb88..9363d3c4f 100755 --- a/mil_common/utils/mil_tools/nodes/tf_to_gazebo.py +++ b/mil_common/utils/mil_tools/nodes/tf_to_gazebo.py @@ -25,12 +25,7 @@ def main(): if do_cam_fix: rot = tf.transformations.quaternion_multiply(rot, cam_fix_quat) print( - "(qx={}, qy={} , qz={}, qw={})".format( - rot[0], - rot[1], - rot[2], - rot[3], - ), + f"(qx={rot[0]}, qy={rot[1]} , qz={rot[2]}, qw={rot[3]})", ) print(f"(x={trans[0]}, y={trans[1]}, z={trans[2]})") euler = tf.transformations.euler_from_quaternion(rot) diff --git a/requirements.txt b/requirements.txt index ae0ac3d2e..90237fb22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pandas==1.4.1 numpy==1.22.3 # Machine Learning -scipy==1.7.3 +scipy==1.10.0 scikit-learn==1.1.1 # Computer Vision @@ -22,7 +22,7 @@ PyYAML==6.0 urdf-parser-py==0.0.4 # Internet -aiohttp==3.8.1 +aiohttp==3.8.5 aiohttp-xmlrpc==1.5.0 # Documentation diff --git a/ruff.toml b/ruff.toml index 69b8787be..0ec9c21f1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -35,6 +35,7 @@ ignore = [ "D212", # we use second line! "D200", # one line docstrings are not shortened! "SIM115", # too many for now... + "RUF012", # too many for now, adds obvious ClassVar statements ] [per-file-ignores]