From f5508a172fe59d517af11c498491e683e8271bce Mon Sep 17 00:00:00 2001
From: Cruiz102
Date: Mon, 11 Aug 2025 23:35:29 -0400
Subject: [PATCH 1/3] ros2 package init and decouple shared interfaces.
---
autonomy/CMakeLists.txt | 57 ++++++++-
autonomy2/CMakeLists.txt | 71 +++++++++++
autonomy2/README.md | 9 ++
autonomy2/colcon.pkg | 1 +
autonomy2/package.xml | 24 ++++
autonomy2/src/cv_node_py/CMakeLists.txt | 5 +
autonomy2/src/cv_node_py/README.md | 12 ++
autonomy2/src/cv_node_py/colcon.pkg | 1 +
.../src/cv_node_py/cv_node_py/__init__.py | 0
autonomy2/src/cv_node_py/cv_node_py/node.py | 116 +++++++++++++++++
.../src/cv_node_py/launch/cv_node.launch.py | 24 ++++
autonomy2/src/cv_node_py/package.xml | 23 ++++
autonomy2/src/cv_node_py/pyproject.toml | 3 +
autonomy2/src/cv_node_py/resource/cv_node_py | 0
autonomy2/src/cv_node_py/setup.cfg | 4 +
autonomy2/src/cv_node_py/setup.py | 26 ++++
autonomy_interfaces/README.md | 14 ++
.../action/NavigateToWaypoint.action | 10 ++
autonomy_interfaces/msg/Detection.msg | 4 +
autonomy_interfaces/msg/Detections.msg | 2 +
autonomy_interfaces/srv/FireTorpedo.srv | 4 +
autonomy_interfaces/srv/SetColorFilter.srv | 8 ++
autonomy_interfaces/srv/SetYoloModel.srv | 4 +
hydrus_software_stack.egg-info/PKG-INFO | 120 ------------------
hydrus_software_stack.egg-info/SOURCES.txt | 45 -------
.../dependency_links.txt | 1 -
.../entry_points.txt | 2 -
hydrus_software_stack.egg-info/not-zip-safe | 1 -
hydrus_software_stack.egg-info/requires.txt | 33 -----
hydrus_software_stack.egg-info/top_level.txt | 1 -
30 files changed, 416 insertions(+), 209 deletions(-)
create mode 100644 autonomy2/CMakeLists.txt
create mode 100644 autonomy2/README.md
create mode 100644 autonomy2/colcon.pkg
create mode 100644 autonomy2/package.xml
create mode 100644 autonomy2/src/cv_node_py/CMakeLists.txt
create mode 100644 autonomy2/src/cv_node_py/README.md
create mode 100644 autonomy2/src/cv_node_py/colcon.pkg
create mode 100644 autonomy2/src/cv_node_py/cv_node_py/__init__.py
create mode 100644 autonomy2/src/cv_node_py/cv_node_py/node.py
create mode 100644 autonomy2/src/cv_node_py/launch/cv_node.launch.py
create mode 100644 autonomy2/src/cv_node_py/package.xml
create mode 100644 autonomy2/src/cv_node_py/pyproject.toml
create mode 100644 autonomy2/src/cv_node_py/resource/cv_node_py
create mode 100644 autonomy2/src/cv_node_py/setup.cfg
create mode 100644 autonomy2/src/cv_node_py/setup.py
create mode 100644 autonomy_interfaces/README.md
create mode 100644 autonomy_interfaces/action/NavigateToWaypoint.action
create mode 100644 autonomy_interfaces/msg/Detection.msg
create mode 100644 autonomy_interfaces/msg/Detections.msg
create mode 100644 autonomy_interfaces/srv/FireTorpedo.srv
create mode 100644 autonomy_interfaces/srv/SetColorFilter.srv
create mode 100644 autonomy_interfaces/srv/SetYoloModel.srv
delete mode 100644 hydrus_software_stack.egg-info/PKG-INFO
delete mode 100644 hydrus_software_stack.egg-info/SOURCES.txt
delete mode 100644 hydrus_software_stack.egg-info/dependency_links.txt
delete mode 100644 hydrus_software_stack.egg-info/entry_points.txt
delete mode 100644 hydrus_software_stack.egg-info/not-zip-safe
delete mode 100644 hydrus_software_stack.egg-info/requires.txt
delete mode 100644 hydrus_software_stack.egg-info/top_level.txt
diff --git a/autonomy/CMakeLists.txt b/autonomy/CMakeLists.txt
index 5581c17..634ea4b 100644
--- a/autonomy/CMakeLists.txt
+++ b/autonomy/CMakeLists.txt
@@ -26,25 +26,70 @@ if (CATKIN_ENABLE_TESTING)
find_package(rostest REQUIRED)
endif()
+# --- Begin: Copy shared interface definitions into this package ---
+# Location of shared interface source files (relative to this package)
+set(SHARED_INTERFACES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../shared_interfaces")
+
+# Ensure local interface directories exist
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/msg")
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/srv")
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/action")
+
+# Gather shared files
+file(GLOB SHARED_MSGS "${SHARED_INTERFACES_DIR}/msg/*.msg")
+file(GLOB SHARED_SRVS "${SHARED_INTERFACES_DIR}/srv/*.srv")
+file(GLOB SHARED_ACTIONS "${SHARED_INTERFACES_DIR}/action/*.action")
+
+# Copy shared files into this package so catkin can find them
+foreach(f ${SHARED_MSGS})
+ get_filename_component(_name "${f}" NAME)
+ configure_file("${f}" "${CMAKE_CURRENT_SOURCE_DIR}/msg/${_name}" COPYONLY)
+endforeach()
+foreach(f ${SHARED_SRVS})
+ get_filename_component(_name "${f}" NAME)
+ configure_file("${f}" "${CMAKE_CURRENT_SOURCE_DIR}/srv/${_name}" COPYONLY)
+endforeach()
+foreach(f ${SHARED_ACTIONS})
+ get_filename_component(_name "${f}" NAME)
+ configure_file("${f}" "${CMAKE_CURRENT_SOURCE_DIR}/action/${_name}" COPYONLY)
+endforeach()
+
+# Build lists of filenames relative to package dirs
+set(MSG_FILES)
+foreach(f ${SHARED_MSGS})
+ get_filename_component(_name "${f}" NAME)
+ list(APPEND MSG_FILES ${_name})
+endforeach()
+
+set(SRV_FILES)
+foreach(f ${SHARED_SRVS})
+ get_filename_component(_name "${f}" NAME)
+ list(APPEND SRV_FILES ${_name})
+endforeach()
+
+set(ACTION_FILES)
+foreach(f ${SHARED_ACTIONS})
+ get_filename_component(_name "${f}" NAME)
+ list(APPEND ACTION_FILES ${_name})
+endforeach()
+# --- End: Copy shared interface definitions ---
+
# Add message files
add_message_files(
FILES
- Detection.msg
- Detections.msg
+ ${MSG_FILES}
)
add_action_files(
FILES
- NavigateToWaypoint.action
+ ${ACTION_FILES}
)
# Add service files
add_service_files(
FILES
- SetColorFilter.srv
- SetYoloModel.srv
- FireTorpedo.srv
+ ${SRV_FILES}
)
# Generate added messages and services
diff --git a/autonomy2/CMakeLists.txt b/autonomy2/CMakeLists.txt
new file mode 100644
index 0000000..f41b40b
--- /dev/null
+++ b/autonomy2/CMakeLists.txt
@@ -0,0 +1,71 @@
+cmake_minimum_required(VERSION 3.8)
+project(autonomy)
+
+if(NOT CMAKE_CXX_STANDARD)
+ set(CMAKE_CXX_STANDARD 17)
+endif()
+
+find_package(ament_cmake REQUIRED)
+find_package(rclcpp REQUIRED)
+find_package(sensor_msgs REQUIRED)
+find_package(geometry_msgs REQUIRED)
+find_package(rosidl_default_generators REQUIRED)
+
+# --- Begin: Copy shared interface definitions into this package ---
+# Location of shared interface source files (relative to this package)
+set(SHARED_INTERFACES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../shared_interfaces")
+
+# Ensure local interface directories exist
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/msg")
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/srv")
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/action")
+
+# Gather shared files
+file(GLOB SHARED_MSGS "${SHARED_INTERFACES_DIR}/msg/*.msg")
+file(GLOB SHARED_SRVS "${SHARED_INTERFACES_DIR}/srv/*.srv")
+file(GLOB SHARED_ACTIONS "${SHARED_INTERFACES_DIR}/action/*.action")
+
+# Copy shared files into this package so rosidl can find them
+foreach(f ${SHARED_MSGS})
+ get_filename_component(_name "${f}" NAME)
+ configure_file("${f}" "${CMAKE_CURRENT_SOURCE_DIR}/msg/${_name}" COPYONLY)
+endforeach()
+foreach(f ${SHARED_SRVS})
+ get_filename_component(_name "${f}" NAME)
+ configure_file("${f}" "${CMAKE_CURRENT_SOURCE_DIR}/srv/${_name}" COPYONLY)
+endforeach()
+foreach(f ${SHARED_ACTIONS})
+ get_filename_component(_name "${f}" NAME)
+ configure_file("${f}" "${CMAKE_CURRENT_SOURCE_DIR}/action/${_name}" COPYONLY)
+endforeach()
+
+# Build lists of filenames relative to package dirs
+set(MSG_FILES)
+foreach(f ${SHARED_MSGS})
+ get_filename_component(_name "${f}" NAME)
+ list(APPEND MSG_FILES "msg/${_name}")
+endforeach()
+
+set(SRV_FILES)
+foreach(f ${SHARED_SRVS})
+ get_filename_component(_name "${f}" NAME)
+ list(APPEND SRV_FILES "srv/${_name}")
+endforeach()
+
+set(ACTION_FILES)
+foreach(f ${SHARED_ACTIONS})
+ get_filename_component(_name "${f}" NAME)
+ list(APPEND ACTION_FILES "action/${_name}")
+endforeach()
+# --- End: Copy shared interface definitions ---
+
+rosidl_generate_interfaces(${PROJECT_NAME}
+ ${MSG_FILES}
+ ${SRV_FILES}
+ ${ACTION_FILES}
+ DEPENDENCIES geometry_msgs sensor_msgs
+)
+
+ament_export_dependencies(rosidl_default_runtime)
+
+ament_package()
diff --git a/autonomy2/README.md b/autonomy2/README.md
new file mode 100644
index 0000000..13d1a08
--- /dev/null
+++ b/autonomy2/README.md
@@ -0,0 +1,9 @@
+# ROS 2 messages for Hydrus autonomy
+
+This package defines ROS 2 versions of:
+- `autonomy/Detection`
+- `autonomy/Detections`
+
+They mirror the fields from the ROS 1 messages.
+
+Build with colcon from the workspace root.
diff --git a/autonomy2/colcon.pkg b/autonomy2/colcon.pkg
new file mode 100644
index 0000000..cd1d2a9
--- /dev/null
+++ b/autonomy2/colcon.pkg
@@ -0,0 +1 @@
+{ "name": "autonomy" }
diff --git a/autonomy2/package.xml b/autonomy2/package.xml
new file mode 100644
index 0000000..934b8a1
--- /dev/null
+++ b/autonomy2/package.xml
@@ -0,0 +1,24 @@
+
+
+ autonomy
+ 0.1.0
+ ROS2 port of Hydrus computer vision node and custom messages.
+ Your Name
+ MIT
+
+ ament_cmake
+
+ rclcpp
+ sensor_msgs
+ geometry_msgs
+ rosidl_default_generators
+ action_msgs
+
+ rclcpp
+ sensor_msgs
+ geometry_msgs
+ rosidl_default_runtime
+ action_msgs
+
+ rosidl_interface_packages
+
diff --git a/autonomy2/src/cv_node_py/CMakeLists.txt b/autonomy2/src/cv_node_py/CMakeLists.txt
new file mode 100644
index 0000000..755f811
--- /dev/null
+++ b/autonomy2/src/cv_node_py/CMakeLists.txt
@@ -0,0 +1,5 @@
+cmake_minimum_required(VERSION 3.8)
+project(cv_node_py)
+
+find_package(ament_cmake REQUIRED)
+ament_package()
diff --git a/autonomy2/src/cv_node_py/README.md b/autonomy2/src/cv_node_py/README.md
new file mode 100644
index 0000000..23b859e
--- /dev/null
+++ b/autonomy2/src/cv_node_py/README.md
@@ -0,0 +1,12 @@
+# cv_node_py
+
+ROS 2 Python node wrapping existing `src/computer_vision/detection_core.py`.
+
+## Build
+
+```bash
+# In workspace root (ros2_ws)
+colcon build --symlink-install
+. install/setup.bash
+ros2 launch cv_node_py cv_node.launch.py
+```
diff --git a/autonomy2/src/cv_node_py/colcon.pkg b/autonomy2/src/cv_node_py/colcon.pkg
new file mode 100644
index 0000000..a06ee9d
--- /dev/null
+++ b/autonomy2/src/cv_node_py/colcon.pkg
@@ -0,0 +1 @@
+{ "name": "cv_node_py" }
diff --git a/autonomy2/src/cv_node_py/cv_node_py/__init__.py b/autonomy2/src/cv_node_py/cv_node_py/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/autonomy2/src/cv_node_py/cv_node_py/node.py b/autonomy2/src/cv_node_py/cv_node_py/node.py
new file mode 100644
index 0000000..89789a5
--- /dev/null
+++ b/autonomy2/src/cv_node_py/cv_node_py/node.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+import os
+
+# Reuse existing detection core utilities
+import sys
+from typing import List, Tuple
+
+import rclpy
+from geometry_msgs.msg import Point
+from rclpy.node import Node
+from rclpy.qos import qos_profile_sensor_data
+from sensor_msgs.msg import Image, RegionOfInterest
+
+sys.path.append(
+ os.path.abspath(
+ os.path.join(os.path.dirname(__file__), "../../../../src/computer_vision")
+ )
+)
+from detection_core import ColorFilterConfig, DetectionPipelineManager, ros_img_to_cv2
+
+from autonomy.msg import Detection as DetectionMsg
+from autonomy.msg import Detections as DetectionsMsg
+
+
+class CvNode(Node):
+ def __init__(self):
+ super().__init__("cv_node")
+ # Parameters
+ self.declare_parameter("yolo_model", "yolo11n.pt")
+ self.declare_parameter("color_target_rgb", [255, 0, 0])
+ self.declare_parameter("color_tolerance", 0.4)
+ self.declare_parameter("color_min_confidence", 0.3)
+ self.declare_parameter("color_min_area", 0.2)
+ self.declare_parameter("image_topic", "/camera/image_raw")
+ self.declare_parameter("detections_topic", "/detections")
+
+ # Build pipeline
+ self.pipeline = DetectionPipelineManager()
+ model = self.get_parameter("yolo_model").get_parameter_value().string_value
+ try:
+ self.pipeline.load_yolo_model(model)
+ except Exception as e:
+ self.get_logger().warn(f"YOLO model load failed: {e}")
+
+ rgb = (
+ self.get_parameter("color_target_rgb")
+ .get_parameter_value()
+ .integer_array_value
+ )
+ tol = float(self.get_parameter("color_tolerance").value)
+ min_conf = float(self.get_parameter("color_min_confidence").value)
+ min_area = float(self.get_parameter("color_min_area").value)
+ self.pipeline.update_color_filter_config(
+ ColorFilterConfig(
+ tolerance=tol,
+ min_confidence=min_conf,
+ min_area=min_area,
+ rgb_range=tuple(int(x) for x in rgb),
+ )
+ )
+
+ # Pub/Sub
+ image_topic = self.get_parameter("image_topic").value
+ self.sub = self.create_subscription(
+ Image, image_topic, self.on_image, qos_profile_sensor_data
+ )
+ det_topic = self.get_parameter("detections_topic").value
+ self.pub = self.create_publisher(DetectionsMsg, det_topic, 10)
+
+ self.get_logger().info(
+ f"cv_node started. Subscribing to {image_topic}, publishing {det_topic}"
+ )
+
+ def on_image(self, msg: Image):
+ try:
+ img = ros_img_to_cv2(msg, encoding=msg.encoding if msg.encoding else "bgr8")
+ except Exception:
+ # fallback assume bgr8
+ img = ros_img_to_cv2(msg, encoding="bgr8")
+
+ results: List[Tuple[str, list]] = self.pipeline.run_detections(img)
+ for name, detections in results:
+ out = DetectionsMsg()
+ out.detector_name = name
+ out.detections = []
+ for d in detections:
+ dm = DetectionMsg()
+ dm.cls = int(d.cls)
+ dm.confidence = float(d.conf)
+ # point may be None
+ if getattr(d, "point", None) is not None:
+ dm.point = Point(
+ x=float(d.point.x), y=float(d.point.y), z=float(d.point.z)
+ )
+ else:
+ dm.point = Point()
+ roi = RegionOfInterest()
+ roi.x_offset = int(d.x1)
+ roi.y_offset = int(d.y1)
+ roi.width = int(d.x2 - d.x1)
+ roi.height = int(d.y2 - d.y1)
+ dm.bounding_box = roi
+ out.detections.append(dm)
+ self.pub.publish(out)
+
+
+def main():
+ rclpy.init()
+ node = CvNode()
+ rclpy.spin(node)
+ node.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/autonomy2/src/cv_node_py/launch/cv_node.launch.py b/autonomy2/src/cv_node_py/launch/cv_node.launch.py
new file mode 100644
index 0000000..6b291aa
--- /dev/null
+++ b/autonomy2/src/cv_node_py/launch/cv_node.launch.py
@@ -0,0 +1,24 @@
+from launch import LaunchDescription
+from launch_ros.actions import Node
+
+
+def generate_launch_description():
+ return LaunchDescription(
+ [
+ Node(
+ package="cv_node_py",
+ executable="cv_node",
+ name="cv_node",
+ output="screen",
+ parameters=[
+ {"yolo_model": "yolo11n.pt"},
+ {"color_target_rgb": [255, 0, 0]},
+ {"color_tolerance": 0.4},
+ {"color_min_confidence": 0.3},
+ {"color_min_area": 0.2},
+ {"image_topic": "/camera/image_raw"},
+ {"detections_topic": "/detections"},
+ ],
+ )
+ ]
+ )
diff --git a/autonomy2/src/cv_node_py/package.xml b/autonomy2/src/cv_node_py/package.xml
new file mode 100644
index 0000000..343e633
--- /dev/null
+++ b/autonomy2/src/cv_node_py/package.xml
@@ -0,0 +1,23 @@
+
+
+ cv_node_py
+ 0.1.0
+ ROS2 Python CV node that reuses detection_core.py
+ Your Name
+ MIT
+
+ ament_python
+
+ rclpy
+ sensor_msgs
+ geometry_msgs
+ autonomy
+
+ ultralytics
+ opencv-python
+ numpy
+
+
+ ament_python
+
+
diff --git a/autonomy2/src/cv_node_py/pyproject.toml b/autonomy2/src/cv_node_py/pyproject.toml
new file mode 100644
index 0000000..9787c3b
--- /dev/null
+++ b/autonomy2/src/cv_node_py/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/autonomy2/src/cv_node_py/resource/cv_node_py b/autonomy2/src/cv_node_py/resource/cv_node_py
new file mode 100644
index 0000000..e69de29
diff --git a/autonomy2/src/cv_node_py/setup.cfg b/autonomy2/src/cv_node_py/setup.cfg
new file mode 100644
index 0000000..6c72b4f
--- /dev/null
+++ b/autonomy2/src/cv_node_py/setup.cfg
@@ -0,0 +1,4 @@
+[develop]
+script-dir=$base/lib/cv_node_py
+[install]
+install-scripts=$base/lib/cv_node_py
diff --git a/autonomy2/src/cv_node_py/setup.py b/autonomy2/src/cv_node_py/setup.py
new file mode 100644
index 0000000..9cd3130
--- /dev/null
+++ b/autonomy2/src/cv_node_py/setup.py
@@ -0,0 +1,26 @@
+from setuptools import setup
+
+package_name = "cv_node_py"
+
+setup(
+ name=package_name,
+ version="0.1.0",
+ packages=[package_name],
+ data_files=[
+ ("share/ament_index/resource_index/packages", ["resource/" + package_name]),
+ ("share/" + package_name, ["package.xml"]),
+ ("share/" + package_name + "/launch", ["launch/cv_node.launch.py"]),
+ ],
+ install_requires=["setuptools"],
+ zip_safe=True,
+ maintainer="Your Name",
+ maintainer_email="you@example.com",
+ description="ROS2 Python CV node using detection_core.py",
+ license="MIT",
+ tests_require=["pytest"],
+ entry_points={
+ "console_scripts": [
+ "cv_node = cv_node_py.node:main",
+ ],
+ },
+)
diff --git a/autonomy_interfaces/README.md b/autonomy_interfaces/README.md
new file mode 100644
index 0000000..cdeda1e
--- /dev/null
+++ b/autonomy_interfaces/README.md
@@ -0,0 +1,14 @@
+# Shared Interfaces (ROS 1 and ROS 2)
+
+This folder contains the single source of truth for Hydrus message, service, and action interface definitions.
+
+Structure:
+- `msg/` β `.msg` message files
+- `srv/` β `.srv` service files
+- `action/` β `.action` action files
+
+Both ROS 1 (`autonomy_ros`) and ROS 2 (`autonomy_ros2`) packages copy these files at CMake configure-time into their local `msg/`, `srv/`, and `action/` folders and generate code from them. Edit files here only, then rebuild the ROS 1/ROS 2 workspaces.
+
+To add a new interface:
+1. Add the `.msg` / `.srv` / `.action` file in this folder hierarchy.
+2. Rebuild your ROS 1 and/or ROS 2 workspaces as usual.
diff --git a/autonomy_interfaces/action/NavigateToWaypoint.action b/autonomy_interfaces/action/NavigateToWaypoint.action
new file mode 100644
index 0000000..6d5e2ec
--- /dev/null
+++ b/autonomy_interfaces/action/NavigateToWaypoint.action
@@ -0,0 +1,10 @@
+# Goal
+
+geometry_msgs/Point target_point
+---
+# Feedback
+float64 distance_to_target # Current distance to the target
+
+---
+# Result
+bool success # True if the target was reached successfully, False otherwise
diff --git a/autonomy_interfaces/msg/Detection.msg b/autonomy_interfaces/msg/Detection.msg
new file mode 100644
index 0000000..38e226a
--- /dev/null
+++ b/autonomy_interfaces/msg/Detection.msg
@@ -0,0 +1,4 @@
+int32 cls
+float32 confidence
+geometry_msgs/Point point
+sensor_msgs/RegionOfInterest bounding_box
diff --git a/autonomy_interfaces/msg/Detections.msg b/autonomy_interfaces/msg/Detections.msg
new file mode 100644
index 0000000..80b3a13
--- /dev/null
+++ b/autonomy_interfaces/msg/Detections.msg
@@ -0,0 +1,2 @@
+autonomy/Detection[] detections
+string detector_name
diff --git a/autonomy_interfaces/srv/FireTorpedo.srv b/autonomy_interfaces/srv/FireTorpedo.srv
new file mode 100644
index 0000000..d1472cd
--- /dev/null
+++ b/autonomy_interfaces/srv/FireTorpedo.srv
@@ -0,0 +1,4 @@
+int8 torpedo_number # Which torpedo to fire (1 or 2)
+---
+bool success # Whether the torpedo was fired successfully
+string message # Additional information or error message
diff --git a/autonomy_interfaces/srv/SetColorFilter.srv b/autonomy_interfaces/srv/SetColorFilter.srv
new file mode 100644
index 0000000..02395fc
--- /dev/null
+++ b/autonomy_interfaces/srv/SetColorFilter.srv
@@ -0,0 +1,8 @@
+# SetColorFilter.srv
+float32 tolerance
+float32 min_confidence
+float32 min_area
+int32[] rgb_range
+---
+bool success
+string message
diff --git a/autonomy_interfaces/srv/SetYoloModel.srv b/autonomy_interfaces/srv/SetYoloModel.srv
new file mode 100644
index 0000000..040daa4
--- /dev/null
+++ b/autonomy_interfaces/srv/SetYoloModel.srv
@@ -0,0 +1,4 @@
+string model_name
+---
+bool success
+string message
diff --git a/hydrus_software_stack.egg-info/PKG-INFO b/hydrus_software_stack.egg-info/PKG-INFO
deleted file mode 100644
index 93c0108..0000000
--- a/hydrus_software_stack.egg-info/PKG-INFO
+++ /dev/null
@@ -1,120 +0,0 @@
-Metadata-Version: 2.4
-Name: hydrus-software-stack
-Version: 0.1.0
-Summary: Autonomous underwater vehicle software stack with computer vision and navigation capabilities
-Home-page: https://github.com/your-username/hydrus-software-stack
-Author: Cesar
-Author-email: cesar@example.com
-Project-URL: Bug Reports, https://github.com/your-username/hydrus-software-stack/issues
-Project-URL: Source, https://github.com/your-username/hydrus-software-stack
-Classifier: Development Status :: 3 - Alpha
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Operating System :: OS Independent
-Requires-Python: >=3.8
-Description-Content-Type: text/markdown
-License-File: LICENSE
-Requires-Dist: opencv-python
-Requires-Dist: numpy
-Requires-Dist: matplotlib
-Requires-Dist: requests
-Requires-Dist: colorama
-Requires-Dist: pyserial
-Requires-Dist: fastapi
-Requires-Dist: Flask
-Requires-Dist: black>=23.0.0
-Requires-Dist: isort>=5.12.0
-Requires-Dist: flake8>=6.0.0
-Requires-Dist: pre-commit>=3.0.0
-Requires-Dist: mypy>=1.0.0
-Provides-Extra: depth-estimation
-Requires-Dist: onnx; extra == "depth-estimation"
-Requires-Dist: onnxruntime-gpu; extra == "depth-estimation"
-Requires-Dist: onnxscript; extra == "depth-estimation"
-Requires-Dist: onnxslim; extra == "depth-estimation"
-Requires-Dist: torch; extra == "depth-estimation"
-Requires-Dist: torchvision; extra == "depth-estimation"
-Requires-Dist: tqdm; extra == "depth-estimation"
-Requires-Dist: typer; extra == "depth-estimation"
-Provides-Extra: all
-Requires-Dist: onnx; extra == "all"
-Requires-Dist: onnxruntime-gpu; extra == "all"
-Requires-Dist: onnxscript; extra == "all"
-Requires-Dist: onnxslim; extra == "all"
-Requires-Dist: torch; extra == "all"
-Requires-Dist: torchvision; extra == "all"
-Requires-Dist: tqdm; extra == "all"
-Requires-Dist: typer; extra == "all"
-Dynamic: author
-Dynamic: author-email
-Dynamic: classifier
-Dynamic: description
-Dynamic: description-content-type
-Dynamic: home-page
-Dynamic: license-file
-Dynamic: project-url
-Dynamic: provides-extra
-Dynamic: requires-dist
-Dynamic: requires-python
-Dynamic: summary
-
-
-
-
π Hydrus Software Stack
-
-
- A comprehensive ROS-based toolkit for autonomous underwater vehicles.
-
- Built for RobSub competitions with maintainability and usability at its core.
-
-
-
-
-Quick start: `./doctor.sh && ./docker/hydrus-docker/hocker`
-
-[](./run_tests.sh)
-[](LICENSE)
-[](http://wiki.ros.org/melodic)
-[](docker/README.md)
-
-
-
-
-To start developing Hydrus
-------
-
-Hydrus is developed by [Rumarino Team](https://github.com/Rumarino-Team). We welcome both pull requests and issues on [GitHub](https://github.com/Rumarino-Team/hydrus-software-stack).
-
-* Read our [Philosophy & Contributing Guide](PHILOSOPHY.md)
-* Check out the [autonomy system documentation](autonomy/README.md)
-* Explore the [Docker deployment options](docker/README.md)
-* Join discussions in [GitHub Issues](../../issues)
-
-Want to contribute? Check our [open issues](../../issues) and follow our [contribution guidelines](PHILOSOPHY.md).
-
-
-MIT Licensed
-
-Hydrus is released under the MIT license. Some parts of the software are released under other licenses as specified.
-
-Any user of this software shall indemnify and hold harmless the Rumarino Team and its members from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses which arise out of, relate to or result from any use of this software by user.
-
-**THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH AND COMPETITION PURPOSES ONLY. THIS IS NOT A PRODUCT.
-YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS.
-NO WARRANTY EXPRESSED OR IMPLIED.**
-
diff --git a/hydrus_software_stack.egg-info/SOURCES.txt b/hydrus_software_stack.egg-info/SOURCES.txt
deleted file mode 100644
index c0797ff..0000000
--- a/hydrus_software_stack.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,45 +0,0 @@
-LICENSE
-README.md
-pyproject.toml
-setup.py
-autonomy/__init__.py
-autonomy/arduino_simulator.py
-autonomy/setup.py
-autonomy/test_controller.py
-autonomy/scripts/__init__.py
-autonomy/scripts/profiler/__init__.py
-autonomy/scripts/profiler/profiling_decorators.py
-autonomy/scripts/profiler/profiling_test.py
-autonomy/scripts/profiler/ros_profiler.py
-autonomy/scripts/profiler/ros_profiler_demo.py
-autonomy/scripts/web/__init__.py
-autonomy/scripts/web/detection_viewer.py
-autonomy/src/__init__.py
-autonomy/src/api_server.py
-autonomy/src/controllers.py
-autonomy/src/custom_types.py
-autonomy/src/cv_publishers.py
-autonomy/src/mission_planner/__init__.py
-autonomy/src/mission_planner/base_mission.py
-autonomy/src/mission_planner/gate_mission.py
-autonomy/src/mission_planner/gate_mission_tester.py
-autonomy/src/mission_planner/gate_tester_visualizer.py
-autonomy/src/mission_planner/mission_manager.py
-autonomy/src/mission_planner/mission_tree.py
-autonomy/src/mission_planner/prequalification_mission.py
-autonomy/src/mission_planner/slalom_mission.py
-autonomy/src/mission_planner/slalom_visualizer.py
-autonomy/src/mission_planner/tagging_mission.py
-autonomy/src/mission_planner/tagging_mission_test.py
-autonomy/src/mission_planner/task_parameters.py
-autonomy/src/mission_planner/test_gate_mission.py
-autonomy/src/mission_planner/test_slalom.py
-autonomy/src/mission_planner/test_slalom_integration.py
-autonomy/src/mission_planner/types.py
-hydrus_software_stack.egg-info/PKG-INFO
-hydrus_software_stack.egg-info/SOURCES.txt
-hydrus_software_stack.egg-info/dependency_links.txt
-hydrus_software_stack.egg-info/entry_points.txt
-hydrus_software_stack.egg-info/not-zip-safe
-hydrus_software_stack.egg-info/requires.txt
-hydrus_software_stack.egg-info/top_level.txt
\ No newline at end of file
diff --git a/hydrus_software_stack.egg-info/dependency_links.txt b/hydrus_software_stack.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/hydrus_software_stack.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/hydrus_software_stack.egg-info/entry_points.txt b/hydrus_software_stack.egg-info/entry_points.txt
deleted file mode 100644
index f06db19..0000000
--- a/hydrus_software_stack.egg-info/entry_points.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[console_scripts]
-hydrus-depth-estimator = autonomy.src.computer_vision.depth_estimation:main
diff --git a/hydrus_software_stack.egg-info/not-zip-safe b/hydrus_software_stack.egg-info/not-zip-safe
deleted file mode 100644
index 8b13789..0000000
--- a/hydrus_software_stack.egg-info/not-zip-safe
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/hydrus_software_stack.egg-info/requires.txt b/hydrus_software_stack.egg-info/requires.txt
deleted file mode 100644
index 3af6dfa..0000000
--- a/hydrus_software_stack.egg-info/requires.txt
+++ /dev/null
@@ -1,33 +0,0 @@
-opencv-python
-numpy
-matplotlib
-requests
-colorama
-pyserial
-fastapi
-Flask
-black>=23.0.0
-isort>=5.12.0
-flake8>=6.0.0
-pre-commit>=3.0.0
-mypy>=1.0.0
-
-[all]
-onnx
-onnxruntime-gpu
-onnxscript
-onnxslim
-torch
-torchvision
-tqdm
-typer
-
-[depth-estimation]
-onnx
-onnxruntime-gpu
-onnxscript
-onnxslim
-torch
-torchvision
-tqdm
-typer
diff --git a/hydrus_software_stack.egg-info/top_level.txt b/hydrus_software_stack.egg-info/top_level.txt
deleted file mode 100644
index 342156a..0000000
--- a/hydrus_software_stack.egg-info/top_level.txt
+++ /dev/null
@@ -1 +0,0 @@
-autonomy
From e309b8acffdf30308545458294f0b56fc925c9f5 Mon Sep 17 00:00:00 2001
From: Cruiz102
Date: Sat, 23 Aug 2025 23:12:10 -0400
Subject: [PATCH 2/3] delete clutter and added new structure tot he dockers.
---
FORMATTING.md | 101 --
README.md | 2 +-
autonomy/.python-version | 1 +
autonomy/README.md | 6 +
autonomy/action/NavigateToWaypoint.action | 10 -
autonomy/launch/depth_estimation.launch | 22 -
autonomy/launch/godot_bridge.launch | 47 -
autonomy/launch/profiler.launch | 23 -
autonomy/launch/simulation.launch | 37 -
autonomy/main.py | 6 +
autonomy/msg/Detection.msg | 4 -
autonomy/msg/Detections.msg | 2 -
autonomy/pyproject.toml | 16 +
autonomy/scripts/cv/depth_estimation_node.py | 135 --
.../scripts/profiler/profiling_decorators.py | 276 ----
autonomy/scripts/profiler/profiling_test.py | 114 --
autonomy/srv/FireTorpedo.srv | 4 -
autonomy/srv/SetColorFilter.srv | 8 -
autonomy/srv/SetYoloModel.srv | 4 -
autonomy2/package.xml | 24 -
autonomy2/src/cv_node_py/package.xml | 23 -
docker/amd64/{cuda => }/camera.Dockerfile | 0
docker/amd64/cpu/hydrus.Dockerfile | 109 --
docker/amd64/cuda/hydrus.Dockerfile | 100 --
docker/amd64/foxy.Dockerfile | 50 +
docker/amd64/noetic.Dockerfile | 46 +
docker/amd64/pkgmanagers.Dockerfile | 39 +
docker/docker-compose-amd64-cpu.yaml | 27 +-
docker/docker-compose-jetson-tx2.yaml | 66 -
docker/run_docker.py | 36 -
hocker | 2 +-
hydrus-cli | 3 -
pyproject.toml | 50 +-
scripts/__init__.py | 1 -
scripts/activate_formatters.sh | 10 -
scripts/commands/dev.py | 4 -
scripts/commands/ros.py | 567 -------
scripts/commands/runner.py | 2 -
scripts/commands/test/__init__.py | 13 -
scripts/commands/test/test_cache.py | 0
scripts/format.sh | 44 -
scripts/format_with_venv.sh | 26 -
scripts/install_ros2_jassy.sh | 39 -
scripts/jetson.py | 383 -----
scripts/setup_formatting.sh | 45 -
scripts/setup_venv_formatters.sh | 120 --
scripts/tests.hss | 36 -
src/computer_vision/.python-version | 1 +
src/computer_vision/README.md | 4 +
.../examples}/color_filters.py | 0
.../computer_vision/examples}/distance.py | 0
.../examples}/object_detector.py | 0
.../computer_vision/examples}/orbs.py | 0
.../examples}/plane_detection.py | 0
.../examples}/slam_visuation.py | 0
src/computer_vision/examples/yolo11n.pt | Bin 0 -> 5613764 bytes
src/computer_vision/main.py | 6 +
src/computer_vision/pyproject.toml | 11 +
src/godot_sim/.editorconfig | 4 -
src/godot_sim/.gitattributes | 2 -
src/godot_sim/.gitignore | 3 -
src/godot_sim/icon.svg | 1 -
src/godot_sim/icon.svg.import | 37 -
src/godot_sim/project.godot | 15 -
src/godot_sim/scripts/depth_camera.gd | 100 --
src/godot_sim/scripts/depth_camera.gd.uid | 1 -
src/godot_sim/scripts/detection_target.gd | 92 --
src/godot_sim/scripts/detection_target.gd.uid | 1 -
src/godot_sim/scripts/main_scene.gd | 99 --
src/godot_sim/scripts/main_scene.gd.uid | 1 -
src/godot_sim/scripts/submarine_controller.gd | 330 ----
.../scripts/submarine_controller.gd.uid | 1 -
src/godot_sim/scripts/water_environment.gd | 173 ---
.../scripts/water_environment.gd.uid | 1 -
tools/hydrus_cli/.python-version | 1 +
.../cv_node_py => tools/hydrus_cli/README.md | 0
.../commands => tools/hydrus_cli}/__init__.py | 0
.../commands => tools/hydrus_cli}/arduino.py | 16 +-
.../commands => tools/hydrus_cli}/autonomy.py | 0
.../commands => tools/hydrus_cli}/build.py | 0
{scripts => tools/hydrus_cli}/cli.py | 14 +-
tools/hydrus_cli/main.py | 6 +
tools/hydrus_cli/pyproject.toml | 9 +
.../test => tools/hydrus_cli}/test.py | 0
.../commands => tools/hydrus_cli}/tmux.py | 0
.../commands => tools/hydrus_cli}/todo.py | 0
.../commands => tools/hydrus_cli}/utils.py | 0
tools/test_lib/README.md | 1 +
.../test => tools/test_lib}/test_cases.py | 0
.../test_lib}/test_config_loader.py | 0
.../test => tools/test_lib}/test_logger.py | 0
.../test => tools/test_lib}/test_manager.py | 0
uv.lock | 1368 +++++++++++++++++
93 files changed, 1604 insertions(+), 3377 deletions(-)
delete mode 100644 FORMATTING.md
create mode 100644 autonomy/.python-version
delete mode 100644 autonomy/action/NavigateToWaypoint.action
delete mode 100644 autonomy/launch/depth_estimation.launch
delete mode 100644 autonomy/launch/godot_bridge.launch
delete mode 100644 autonomy/launch/profiler.launch
delete mode 100644 autonomy/launch/simulation.launch
create mode 100644 autonomy/main.py
delete mode 100644 autonomy/msg/Detection.msg
delete mode 100644 autonomy/msg/Detections.msg
create mode 100644 autonomy/pyproject.toml
delete mode 100644 autonomy/scripts/cv/depth_estimation_node.py
delete mode 100644 autonomy/scripts/profiler/profiling_decorators.py
delete mode 100644 autonomy/scripts/profiler/profiling_test.py
delete mode 100644 autonomy/srv/FireTorpedo.srv
delete mode 100644 autonomy/srv/SetColorFilter.srv
delete mode 100644 autonomy/srv/SetYoloModel.srv
delete mode 100644 autonomy2/package.xml
delete mode 100644 autonomy2/src/cv_node_py/package.xml
rename docker/amd64/{cuda => }/camera.Dockerfile (100%)
delete mode 100644 docker/amd64/cpu/hydrus.Dockerfile
delete mode 100644 docker/amd64/cuda/hydrus.Dockerfile
create mode 100644 docker/amd64/foxy.Dockerfile
create mode 100644 docker/amd64/noetic.Dockerfile
create mode 100644 docker/amd64/pkgmanagers.Dockerfile
delete mode 100644 docker/docker-compose-jetson-tx2.yaml
delete mode 100755 docker/run_docker.py
delete mode 100644 scripts/__init__.py
delete mode 100755 scripts/activate_formatters.sh
delete mode 100644 scripts/commands/dev.py
delete mode 100644 scripts/commands/ros.py
delete mode 100644 scripts/commands/runner.py
delete mode 100644 scripts/commands/test/__init__.py
delete mode 100644 scripts/commands/test/test_cache.py
delete mode 100755 scripts/format.sh
delete mode 100755 scripts/format_with_venv.sh
delete mode 100644 scripts/install_ros2_jassy.sh
delete mode 100755 scripts/jetson.py
delete mode 100755 scripts/setup_formatting.sh
delete mode 100755 scripts/setup_venv_formatters.sh
delete mode 100644 scripts/tests.hss
create mode 100644 src/computer_vision/.python-version
create mode 100644 src/computer_vision/README.md
rename {autonomy/scripts/cv => src/computer_vision/examples}/color_filters.py (100%)
rename {autonomy/scripts/cv => src/computer_vision/examples}/distance.py (100%)
rename {autonomy/scripts/cv => src/computer_vision/examples}/object_detector.py (100%)
rename {autonomy/scripts/cv => src/computer_vision/examples}/orbs.py (100%)
rename {autonomy/scripts/cv => src/computer_vision/examples}/plane_detection.py (100%)
rename {autonomy/scripts/cv => src/computer_vision/examples}/slam_visuation.py (100%)
create mode 100644 src/computer_vision/examples/yolo11n.pt
create mode 100644 src/computer_vision/main.py
create mode 100644 src/computer_vision/pyproject.toml
delete mode 100644 src/godot_sim/.editorconfig
delete mode 100644 src/godot_sim/.gitattributes
delete mode 100644 src/godot_sim/.gitignore
delete mode 100644 src/godot_sim/icon.svg
delete mode 100644 src/godot_sim/icon.svg.import
delete mode 100644 src/godot_sim/project.godot
delete mode 100644 src/godot_sim/scripts/depth_camera.gd
delete mode 100644 src/godot_sim/scripts/depth_camera.gd.uid
delete mode 100644 src/godot_sim/scripts/detection_target.gd
delete mode 100644 src/godot_sim/scripts/detection_target.gd.uid
delete mode 100644 src/godot_sim/scripts/main_scene.gd
delete mode 100644 src/godot_sim/scripts/main_scene.gd.uid
delete mode 100644 src/godot_sim/scripts/submarine_controller.gd
delete mode 100644 src/godot_sim/scripts/submarine_controller.gd.uid
delete mode 100644 src/godot_sim/scripts/water_environment.gd
delete mode 100644 src/godot_sim/scripts/water_environment.gd.uid
create mode 100644 tools/hydrus_cli/.python-version
rename autonomy2/src/cv_node_py/resource/cv_node_py => tools/hydrus_cli/README.md (100%)
rename {scripts/commands => tools/hydrus_cli}/__init__.py (100%)
rename {scripts/commands => tools/hydrus_cli}/arduino.py (95%)
rename {scripts/commands => tools/hydrus_cli}/autonomy.py (100%)
rename {scripts/commands => tools/hydrus_cli}/build.py (100%)
rename {scripts => tools/hydrus_cli}/cli.py (69%)
create mode 100644 tools/hydrus_cli/main.py
create mode 100644 tools/hydrus_cli/pyproject.toml
rename {scripts/commands/test => tools/hydrus_cli}/test.py (100%)
rename {scripts/commands => tools/hydrus_cli}/tmux.py (100%)
rename {scripts/commands => tools/hydrus_cli}/todo.py (100%)
rename {scripts/commands => tools/hydrus_cli}/utils.py (100%)
create mode 100644 tools/test_lib/README.md
rename {scripts/commands/test => tools/test_lib}/test_cases.py (100%)
rename {scripts/commands/test => tools/test_lib}/test_config_loader.py (100%)
rename {scripts/commands/test => tools/test_lib}/test_logger.py (100%)
rename {scripts/commands/test => tools/test_lib}/test_manager.py (100%)
create mode 100644 uv.lock
diff --git a/FORMATTING.md b/FORMATTING.md
deleted file mode 100644
index 299aae8..0000000
--- a/FORMATTING.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# Python Code Formatting Guide
-
-This project uses automated Python code formatting to ensure consistent code style across the entire codebase.
-
-## π Quick Start
-
-### Option 1: Use the Virtual Environment (Recommended)
-```bash
-# One-time setup
-./setup_venv_formatters.sh
-
-# Format your code
-./format_with_venv.sh
-
-# Or activate and use manually
-source activate_formatters.sh
-black . --line-length 88 --target-version py38
-isort . --profile black
-flake8 .
-```
-
-### Option 2: Use Existing Environment
-```bash
-# If you already have the tools installed
-./format.sh
-```
-
-## π§ Tools Used
-
-- **Black** (24.10.0+): Automatic Python code formatter
-- **isort** (6.0.1+): Import statement organizer
-- **flake8** (7.2.0+): Code linter and style checker
-- **pre-commit** (3.8.0+): Git hooks for automatic formatting
-
-## π Files in This Setup
-
-| File | Purpose |
-|------|---------|
-| `setup_venv_formatters.sh` | Creates virtual environment with formatters |
-| `activate_formatters.sh` | Quick activation script |
-| `format_with_venv.sh` | Formats code using virtual environment |
-| `format.sh` | Legacy formatting script |
-| `pyproject.toml` | Tool configuration |
-| `.flake8` | Flake8-specific configuration |
-| `.pre-commit-config.yaml` | Git hooks configuration |
-
-## π CI/CD Integration
-
-The CI pipeline automatically checks code formatting on every pull request:
-
-1. **Black formatting check** - Ensures consistent code style
-2. **Import sorting check** - Validates import organization
-3. **Flake8 linting** - Checks for code quality issues
-
-If any check fails, the CI will block the PR with clear instructions to run the formatter.
-
-## π οΈ Configuration
-
-### Black Settings
-- Line length: 88 characters
-- Target Python version: 3.8
-- Profile: Compatible with isort
-
-### Flake8 Settings
-- Ignores common style issues that don't affect functionality
-- Focuses on important code quality checks
-- Uses same line length as Black
-
-## π‘ Usage Tips
-
-1. **Before committing**: Pre-commit hooks will automatically format your code
-2. **If CI fails**: Run `./format_with_venv.sh` to fix all issues
-3. **For specific files**: Use tools directly after activating the environment
-
-## π Troubleshooting
-
-### Virtual Environment Issues
-```bash
-# If the environment is corrupted, recreate it
-rm -rf .venv-formatters
-./setup_venv_formatters.sh
-```
-
-### CI Version Conflicts
-The CI uses flexible version ranges to ensure compatibility with the latest available public versions of the formatting tools.
-
-### Pre-commit Hook Issues
-```bash
-# Reinstall hooks if needed
-source activate_formatters.sh
-pre-commit install --overwrite
-```
-
-## π Version Compatibility
-
-This setup is designed to work with the latest publicly available versions:
-- Black: `>=24.0.0,<25.0.0`
-- isort: `>=5.12.0,<7.0.0`
-- flake8: `>=6.0.0,<8.0.0`
-
-The virtual environment approach ensures consistent versions across all developers and CI environments.
diff --git a/README.md b/README.md
index 6ffc34f..1e06c41 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
- Philosophy
+ Philosophy
Β·
Autonomy Docs
Β·
diff --git a/autonomy/.python-version b/autonomy/.python-version
new file mode 100644
index 0000000..24ee5b1
--- /dev/null
+++ b/autonomy/.python-version
@@ -0,0 +1 @@
+3.13
diff --git a/autonomy/README.md b/autonomy/README.md
index 31b780a..733a67c 100644
--- a/autonomy/README.md
+++ b/autonomy/README.md
@@ -1,5 +1,11 @@
# Hydrus Autonomy System
+
+This module is going to be deprecated for the following reasons:
+
+
+
+
The Hydrus Autonomy System is a comprehensive ROS-based underwater vehicle control and mission management framework. This README documents all available scripts, their purpose, and usage instructions.
> β οΈ **Warning**: The mission API is currently under development. Some features may not work as expected and are subject to change.
diff --git a/autonomy/action/NavigateToWaypoint.action b/autonomy/action/NavigateToWaypoint.action
deleted file mode 100644
index 6d5e2ec..0000000
--- a/autonomy/action/NavigateToWaypoint.action
+++ /dev/null
@@ -1,10 +0,0 @@
-# Goal
-
-geometry_msgs/Point target_point
----
-# Feedback
-float64 distance_to_target # Current distance to the target
-
----
-# Result
-bool success # True if the target was reached successfully, False otherwise
diff --git a/autonomy/launch/depth_estimation.launch b/autonomy/launch/depth_estimation.launch
deleted file mode 100644
index 5e05225..0000000
--- a/autonomy/launch/depth_estimation.launch
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/autonomy/launch/godot_bridge.launch b/autonomy/launch/godot_bridge.launch
deleted file mode 100644
index b91975a..0000000
--- a/autonomy/launch/godot_bridge.launch
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/autonomy/launch/profiler.launch b/autonomy/launch/profiler.launch
deleted file mode 100644
index a7677e1..0000000
--- a/autonomy/launch/profiler.launch
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- $(arg target_nodes)
-
-
-
diff --git a/autonomy/launch/simulation.launch b/autonomy/launch/simulation.launch
deleted file mode 100644
index 3e8c19e..0000000
--- a/autonomy/launch/simulation.launch
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/autonomy/main.py b/autonomy/main.py
new file mode 100644
index 0000000..428c106
--- /dev/null
+++ b/autonomy/main.py
@@ -0,0 +1,6 @@
+def main():
+ print("Hello from autonomy!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/autonomy/msg/Detection.msg b/autonomy/msg/Detection.msg
deleted file mode 100644
index 38e226a..0000000
--- a/autonomy/msg/Detection.msg
+++ /dev/null
@@ -1,4 +0,0 @@
-int32 cls
-float32 confidence
-geometry_msgs/Point point
-sensor_msgs/RegionOfInterest bounding_box
diff --git a/autonomy/msg/Detections.msg b/autonomy/msg/Detections.msg
deleted file mode 100644
index 80b3a13..0000000
--- a/autonomy/msg/Detections.msg
+++ /dev/null
@@ -1,2 +0,0 @@
-autonomy/Detection[] detections
-string detector_name
diff --git a/autonomy/pyproject.toml b/autonomy/pyproject.toml
new file mode 100644
index 0000000..5294fc3
--- /dev/null
+++ b/autonomy/pyproject.toml
@@ -0,0 +1,16 @@
+[project]
+name = "autonomy"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "colorama>=0.4.6",
+ "fastapi>=0.116.1",
+ "matplotlib>=3.10.5",
+ "opencv-python>=4.11.0.86",
+ "pyserial>=3.5",
+ "requests>=2.32.5",
+ "termcolor>=3.1.0",
+ "uvicorn>=0.35.0",
+]
diff --git a/autonomy/scripts/cv/depth_estimation_node.py b/autonomy/scripts/cv/depth_estimation_node.py
deleted file mode 100644
index 463a612..0000000
--- a/autonomy/scripts/cv/depth_estimation_node.py
+++ /dev/null
@@ -1,135 +0,0 @@
-#!/usr/bin/env python3
-"""
-ROS node for depth estimation using Depth-Anything-ONNX
-Publishes depth maps and provides depth estimation services
-"""
-
-import os
-import sys
-
-import cv2
-import numpy as np
-import rospy
-from cv_bridge import CvBridge
-from sensor_msgs.msg import CompressedImage, Image
-from std_msgs.msg import Float32
-
-# Add the computer vision module to path
-sys.path.append(os.path.dirname(__file__))
-
-try:
- from computer_vision.depth_estimation import DepthEstimator
-
- DEPTH_ESTIMATION_AVAILABLE = True
-except ImportError as e:
- DEPTH_ESTIMATION_AVAILABLE = False
- rospy.logwarn(f"Depth estimation dependencies not available: {e}")
- rospy.logwarn("Install with: pip install -e .[depth-estimation]")
-
-
-class DepthEstimationNode:
- """ROS node for depth estimation"""
-
- def __init__(self):
- rospy.init_node("depth_estimation_node")
-
- # Check if depth estimation is available
- if not DEPTH_ESTIMATION_AVAILABLE:
- rospy.logerr("Depth estimation dependencies not installed!")
- rospy.logerr("Install with: pip install -e .[depth-estimation]")
- rospy.signal_shutdown("Missing dependencies")
- return
-
- # Parameters
- self.encoder = rospy.get_param("~encoder", "vitb")
- self.model_path = rospy.get_param("~model_path", None)
- self.input_topic = rospy.get_param("~input_topic", "/camera/image_raw")
- self.use_compressed = rospy.get_param("~use_compressed", False)
- self.publish_colored = rospy.get_param("~publish_colored", True)
-
- # Initialize components
- self.bridge = CvBridge()
-
- try:
- self.depth_estimator = DepthEstimator(
- model_path=self.model_path, encoder=self.encoder
- )
- except ImportError as e:
- rospy.logerr(f"Failed to initialize depth estimator: {e}")
- rospy.signal_shutdown("Depth estimator initialization failed")
- return
-
- # Publishers
- self.depth_pub = rospy.Publisher("~depth_map", Image, queue_size=1)
- self.depth_colored_pub = rospy.Publisher("~depth_colored", Image, queue_size=1)
- self.center_depth_pub = rospy.Publisher("~center_depth", Float32, queue_size=1)
-
- # Subscribers
- if self.use_compressed:
- self.image_sub = rospy.Subscriber(
- self.input_topic, CompressedImage, self.compressed_image_callback
- )
- else:
- self.image_sub = rospy.Subscriber(
- self.input_topic, Image, self.image_callback
- )
-
- rospy.loginfo(f"Depth estimation node started with encoder: {self.encoder}")
- rospy.loginfo(f"Subscribing to: {self.input_topic}")
-
- def image_callback(self, msg):
- """Process uncompressed image messages"""
- try:
- # Convert ROS image to OpenCV
- cv_image = self.bridge.imgmsg_to_cv2(msg, "bgr8")
- self.process_image(cv_image, msg.header)
- except Exception as e:
- rospy.logerr(f"Error processing image: {e}")
-
- def compressed_image_callback(self, msg):
- """Process compressed image messages"""
- try:
- # Convert compressed image to OpenCV
- np_arr = np.frombuffer(msg.data, np.uint8)
- cv_image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
- self.process_image(cv_image, msg.header)
- except Exception as e:
- rospy.logerr(f"Error processing compressed image: {e}")
-
- def process_image(self, cv_image, header):
- """Process image and publish depth information"""
- try:
- # Estimate depth
- raw_depth, viz_depth = self.depth_estimator.estimate_depth(cv_image)
-
- # Publish raw depth map
- depth_msg = self.bridge.cv2_to_imgmsg(raw_depth.astype(np.float32), "32FC1")
- depth_msg.header = header
- self.depth_pub.publish(depth_msg)
-
- # Publish colored depth map if enabled
- if self.publish_colored:
- colored_depth = self.depth_estimator.create_colored_depth_map(viz_depth)
- colored_msg = self.bridge.cv2_to_imgmsg(colored_depth, "bgr8")
- colored_msg.header = header
- self.depth_colored_pub.publish(colored_msg)
-
- # Publish center depth value
- h, w = cv_image.shape[:2]
- center_depth = self.depth_estimator.get_depth_at_point(
- raw_depth, w // 2, h // 2
- )
- center_msg = Float32()
- center_msg.data = center_depth
- self.center_depth_pub.publish(center_msg)
-
- except Exception as e:
- rospy.logerr(f"Error in depth processing: {e}")
-
-
-if __name__ == "__main__":
- try:
- node = DepthEstimationNode()
- rospy.spin()
- except rospy.ROSInterruptException:
- pass
diff --git a/autonomy/scripts/profiler/profiling_decorators.py b/autonomy/scripts/profiler/profiling_decorators.py
deleted file mode 100644
index a155adb..0000000
--- a/autonomy/scripts/profiler/profiling_decorators.py
+++ /dev/null
@@ -1,276 +0,0 @@
-#!/usr/bin/env python3
-"""
-Function Profiling Decorators
-
-Decorators and utilities for profiling function execution times in ROS nodes.
-Use these to instrument critical functions for detailed performance analysis.
-
-Usage:
- from autonomy.scripts.services.profiling_decorators import profile_function, ProfileManager
-
- # Decorate functions you want to profile
- @profile_function("cv_processing")
- def process_image(self, image):
- # Your function code here
- pass
-
- # Get profiling results
- ProfileManager.get_stats()
-"""
-
-import functools
-import json
-import threading
-import time
-from collections import defaultdict, deque
-from typing import Any, Callable, Dict, List, Optional
-
-
-class ProfileManager:
- """Global manager for function profiling data"""
-
- _instance = None
- _lock = threading.Lock()
-
- def __new__(cls):
- if cls._instance is None:
- with cls._lock:
- if cls._instance is None:
- cls._instance = super().__new__(cls)
- cls._instance._initialized = False
- return cls._instance
-
- def __init__(self):
- if self._initialized:
- return
-
- self.function_stats = defaultdict(
- lambda: {
- "call_count": 0,
- "total_time": 0.0,
- "min_time": float("inf"),
- "max_time": 0.0,
- "recent_times": deque(maxlen=100), # Last 100 calls
- "avg_time": 0.0,
- "calls_per_second": 0.0,
- "last_call": 0.0,
- }
- )
- self.node_name = "unknown"
- self._initialized = True
-
- def set_node_name(self, name: str):
- """Set the name of the current node"""
- self.node_name = name
-
- def record_function_call(self, function_name: str, execution_time: float):
- """Record a function call and its execution time"""
- with self._lock:
- stats = self.function_stats[function_name]
- current_time = time.time()
-
- # Update basic stats
- stats["call_count"] += 1
- stats["total_time"] += execution_time
- stats["min_time"] = min(stats["min_time"], execution_time)
- stats["max_time"] = max(stats["max_time"], execution_time)
- stats["recent_times"].append(execution_time)
- stats["last_call"] = current_time
-
- # Calculate averages
- stats["avg_time"] = stats["total_time"] / stats["call_count"]
-
- # Calculate calls per second (based on recent calls)
- if len(stats["recent_times"]) > 1:
- time_span = current_time - (current_time - len(stats["recent_times"]))
- if time_span > 0:
- stats["calls_per_second"] = len(stats["recent_times"]) / time_span
-
- def get_stats(self) -> Dict[str, Any]:
- """Get current profiling statistics"""
- with self._lock:
- return {"node_name": self.node_name, "functions": dict(self.function_stats)}
-
- def get_top_functions(
- self, metric: str = "total_time", limit: int = 10
- ) -> List[tuple]:
- """Get top functions by specified metric"""
- with self._lock:
- functions = []
- for func_name, stats in self.function_stats.items():
- if metric in stats:
- functions.append((func_name, stats[metric]))
-
- functions.sort(key=lambda x: x[1], reverse=True)
- return functions[:limit]
-
- def reset_stats(self):
- """Reset all profiling statistics"""
- with self._lock:
- self.function_stats.clear()
-
- @classmethod
- def get_instance(cls):
- """Get the singleton instance"""
- return cls()
-
-
-def profile_function(name: Optional[str] = None, category: str = "general"):
- """
- Decorator to profile function execution time
-
- Args:
- name: Custom name for the function (defaults to function name)
- category: Category for grouping related functions
- """
-
- def decorator(func: Callable) -> Callable:
- function_name = name or f"{category}.{func.__name__}"
-
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- start_time = time.perf_counter()
- try:
- result = func(*args, **kwargs)
- return result
- finally:
- end_time = time.perf_counter()
- execution_time = end_time - start_time
- ProfileManager.get_instance().record_function_call(
- function_name, execution_time
- )
-
- return wrapper
-
- return decorator
-
-
-def profile_method(name: Optional[str] = None, category: str = "methods"):
- """
- Decorator specifically for class methods
-
- Args:
- name: Custom name for the method (defaults to class.method)
- category: Category for grouping related methods
- """
-
- def decorator(func: Callable) -> Callable:
- @functools.wraps(func)
- def wrapper(self, *args, **kwargs):
- if name:
- function_name = name
- else:
- class_name = self.__class__.__name__
- function_name = f"{category}.{class_name}.{func.__name__}"
-
- start_time = time.perf_counter()
- try:
- result = func(self, *args, **kwargs)
- return result
- finally:
- end_time = time.perf_counter()
- execution_time = end_time - start_time
- ProfileManager.get_instance().record_function_call(
- function_name, execution_time
- )
-
- return wrapper
-
- return decorator
-
-
-class FunctionProfiler:
- """Context manager for profiling code blocks"""
-
- def __init__(self, name: str):
- self.name = name
- self.start_time = None
-
- def __enter__(self):
- self.start_time = time.perf_counter()
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- if self.start_time:
- execution_time = time.perf_counter() - self.start_time
- ProfileManager.get_instance().record_function_call(
- self.name, execution_time
- )
-
-
-def profile_ros_callback(topic_name: str):
- """
- Decorator specifically for ROS callback functions
-
- Args:
- topic_name: Name of the ROS topic for identification
- """
-
- def decorator(func: Callable) -> Callable:
- function_name = f"ros_callback.{topic_name}.{func.__name__}"
-
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- start_time = time.perf_counter()
- try:
- result = func(*args, **kwargs)
- return result
- finally:
- end_time = time.perf_counter()
- execution_time = end_time - start_time
- ProfileManager.get_instance().record_function_call(
- function_name, execution_time
- )
-
- return wrapper
-
- return decorator
-
-
-def export_profiling_data(filename: str):
- """Export profiling data to JSON file"""
- stats = ProfileManager.get_instance().get_stats()
-
- # Convert deque objects to lists for JSON serialization
- for func_stats in stats["functions"].values():
- if "recent_times" in func_stats:
- func_stats["recent_times"] = list(func_stats["recent_times"])
-
- with open(filename, "w") as f:
- json.dump(stats, f, indent=2, default=str)
-
-
-# Example usage and testing
-if __name__ == "__main__":
- # Example of how to use the profiling decorators
-
- @profile_function("test_function", "testing")
- def slow_function():
- time.sleep(0.1)
- return "done"
-
- @profile_method("test_method", "testing")
- class TestClass:
- def slow_method(self):
- time.sleep(0.05)
- return "method done"
-
- # Test the profiling
- ProfileManager.get_instance().set_node_name("test_node")
-
- # Call functions multiple times
- for i in range(5):
- slow_function()
-
- test_obj = TestClass()
- for i in range(3):
- test_obj.slow_method()
-
- # Use context manager
- with FunctionProfiler("manual_timing"):
- time.sleep(0.02)
-
- # Print results
- stats = ProfileManager.get_instance().get_stats()
- print("Profiling Results:")
- print(json.dumps(stats, indent=2, default=str))
diff --git a/autonomy/scripts/profiler/profiling_test.py b/autonomy/scripts/profiler/profiling_test.py
deleted file mode 100644
index 60a9bc8..0000000
--- a/autonomy/scripts/profiler/profiling_test.py
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/env python3
-"""
-Simple test of the profiling decorators without ROS dependencies
-This allows testing the profiling functionality independently
-"""
-
-import os
-import sys
-import time
-
-import numpy as np
-
-# Add the current directory to path
-sys.path.append(os.path.dirname(__file__))
-
-from profiling_decorators import (
- FunctionProfiler,
- ProfileManager,
- export_profiling_data,
- profile_function,
- profile_method,
-)
-
-# Initialize profiling
-ProfileManager.get_instance().set_node_name("profiling_test")
-
-
-@profile_function("image_processing", "cv")
-def process_image(image):
- """Mock image processing function"""
- with FunctionProfiler("cv.blur"):
- # Simulate Gaussian blur
- time.sleep(0.01) # Simulate processing time
-
- with FunctionProfiler("cv.threshold"):
- # Simulate thresholding
- time.sleep(0.005)
-
- return True
-
-
-@profile_function("detection", "cv")
-def detect_objects(image):
- """Mock object detection"""
- with FunctionProfiler("cv.inference"):
- time.sleep(0.02) # Simulate model inference
-
- with FunctionProfiler("cv.postprocess"):
- time.sleep(0.003) # Simulate post-processing
-
- return [{"bbox": [100, 100, 50, 50], "confidence": 0.8}]
-
-
-class MockProcessor:
- @profile_method("process_frame", "pipeline")
- def process_frame(self, frame):
- """Mock frame processing pipeline"""
- process_image(frame)
- detections = detect_objects(frame)
- return detections
-
-
-def main():
- print("π§ Testing Profiling Decorators")
- print("=" * 40)
-
- # Create mock data
- mock_image = np.zeros((480, 640, 3), dtype=np.uint8)
- processor = MockProcessor()
-
- # Run multiple iterations to collect data
- print("Running profiling test iterations...")
- for i in range(20):
- processor.process_frame(mock_image)
- if i % 5 == 0:
- print(f"Completed {i+1}/20 iterations")
-
- # Get and display results
- stats = ProfileManager.get_instance().get_stats()
- print(f"\nπ Profiling Results:")
- print(f"Node: {stats['node_name']}")
- print(f"Functions profiled: {len(stats['functions'])}")
- print()
-
- # Sort functions by total time
- sorted_functions = sorted(
- stats["functions"].items(), key=lambda x: x[1]["total_time"], reverse=True
- )
-
- print("Function Performance (sorted by total time):")
- print("-" * 60)
- print(f"{'Function':<25} {'Calls':<8} {'Avg (ms)':<10} {'Total (ms)':<12}")
- print("-" * 60)
-
- for func_name, func_stats in sorted_functions:
- avg_time = func_stats["avg_time"] * 1000
- total_time = func_stats["total_time"] * 1000
- call_count = func_stats["call_count"]
- print(f"{func_name:<25} {call_count:<8} {avg_time:<10.2f} {total_time:<12.2f}")
-
- # Export data
- export_filename = "profiling_test_results.json"
- export_profiling_data(export_filename)
- print(f"\nβ
Detailed results exported to {export_filename}")
-
- # Show top slowest functions
- print(f"\nπ Top 3 slowest functions (by average time):")
- top_slow = ProfileManager.get_instance().get_top_functions("avg_time", 3)
- for i, (func_name, avg_time) in enumerate(top_slow, 1):
- print(f" {i}. {func_name}: {avg_time*1000:.2f}ms avg")
-
-
-if __name__ == "__main__":
- main()
diff --git a/autonomy/srv/FireTorpedo.srv b/autonomy/srv/FireTorpedo.srv
deleted file mode 100644
index d1472cd..0000000
--- a/autonomy/srv/FireTorpedo.srv
+++ /dev/null
@@ -1,4 +0,0 @@
-int8 torpedo_number # Which torpedo to fire (1 or 2)
----
-bool success # Whether the torpedo was fired successfully
-string message # Additional information or error message
diff --git a/autonomy/srv/SetColorFilter.srv b/autonomy/srv/SetColorFilter.srv
deleted file mode 100644
index 02395fc..0000000
--- a/autonomy/srv/SetColorFilter.srv
+++ /dev/null
@@ -1,8 +0,0 @@
-# SetColorFilter.srv
-float32 tolerance
-float32 min_confidence
-float32 min_area
-int32[] rgb_range
----
-bool success
-string message
diff --git a/autonomy/srv/SetYoloModel.srv b/autonomy/srv/SetYoloModel.srv
deleted file mode 100644
index 040daa4..0000000
--- a/autonomy/srv/SetYoloModel.srv
+++ /dev/null
@@ -1,4 +0,0 @@
-string model_name
----
-bool success
-string message
diff --git a/autonomy2/package.xml b/autonomy2/package.xml
deleted file mode 100644
index 934b8a1..0000000
--- a/autonomy2/package.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
- autonomy
- 0.1.0
- ROS2 port of Hydrus computer vision node and custom messages.
- Your Name
- MIT
-
- ament_cmake
-
- rclcpp
- sensor_msgs
- geometry_msgs
- rosidl_default_generators
- action_msgs
-
- rclcpp
- sensor_msgs
- geometry_msgs
- rosidl_default_runtime
- action_msgs
-
- rosidl_interface_packages
-
diff --git a/autonomy2/src/cv_node_py/package.xml b/autonomy2/src/cv_node_py/package.xml
deleted file mode 100644
index 343e633..0000000
--- a/autonomy2/src/cv_node_py/package.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
- cv_node_py
- 0.1.0
- ROS2 Python CV node that reuses detection_core.py
- Your Name
- MIT
-
- ament_python
-
- rclpy
- sensor_msgs
- geometry_msgs
- autonomy
-
- ultralytics
- opencv-python
- numpy
-
-
- ament_python
-
-
diff --git a/docker/amd64/cuda/camera.Dockerfile b/docker/amd64/camera.Dockerfile
similarity index 100%
rename from docker/amd64/cuda/camera.Dockerfile
rename to docker/amd64/camera.Dockerfile
diff --git a/docker/amd64/cpu/hydrus.Dockerfile b/docker/amd64/cpu/hydrus.Dockerfile
deleted file mode 100644
index ac7e556..0000000
--- a/docker/amd64/cpu/hydrus.Dockerfile
+++ /dev/null
@@ -1,109 +0,0 @@
-# Use Ubuntu 20.04 as base image
-FROM ubuntu:20.04
-
-ARG DEBIAN_FRONTEND=noninteractive
-# Update package list
-RUN apt-get update && apt-get install -y lsb-release gnupg curl software-properties-common
-
-# Setup ROS repositories
-RUN sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
-RUN curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | apt-key add -
-
-# Install ROS Noetic base
-RUN apt-get update && apt-get install -y ros-noetic-ros-base
-
-# Add the deadsnakes PPA and install Python 3.8
-RUN add-apt-repository -y ppa:deadsnakes/ppa && \
- apt-get update && \
- apt-get install -y python3.8 python3.8-distutils python3.8-venv
-
-# Set Python 3.8 as the default
-RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1
-
-# Install pip for Python 3.8
-RUN curl -sS https://bootstrap.pypa.io/pip/3.8/get-pip.py | python3.8
-
-#Add kisak-mesa PPA (for latest graphics drivers)
-RUN add-apt-repository -y ppa:kisak/kisak-mesa
-
-# Camera and Computer Vision Dependencies Python-3
-RUN apt-get update && apt-get install -y \
- python3-pip \
- python3-numpy\
- python3-opencv \
- libgl1-mesa-glx \
- ros-noetic-cv-bridge \
- ros-noetic-vision-opencv\
- libbullet-dev \
- python3-empy
-
-
-RUN apt-get update && apt-get install -y\
- ros-noetic-tf2-geometry-msgs\
- python3-tf2-kdl
-
-RUN sudo sh -c \
- 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list' &&\
- curl https://packages.osrfoundation.org/gazebo.key | sudo apt-key add - &&\
- apt-get update && apt-get install -y ignition-fortress ros-noetic-ros-ign &&\
- echo "export GZ_VERSION=fortress" >> /root/.bashrc
-
-
-# Embedded Node Dependencies
-RUN apt-get install -y --no-install-recommends \
- gcc \
- curl \
- git
-
-# ROS setup
-RUN /bin/bash -c 'source /opt/ros/noetic/setup.bash && \
- mkdir -p /home/catkin_ws/src && \
- cd /home/catkin_ws/ && \
- catkin_make'
-RUN echo "source /opt/ros/noetic/setup.bash" >> /root/.bashrc
-
-# Install Arduino CLI and libraries
-WORKDIR /usr/local/
-RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh && \
- arduino-cli core update-index && \
- arduino-cli core install arduino:avr
-RUN arduino-cli lib install "Servo@1.2.1" && \
- arduino-cli lib install "BlueRobotics MS5837 Library@1.1.1"
-
-
-# Copy embedded Arduino code in the Arduino libraries folder
-
-# Copy dvl embedded driver
-COPY ./devices/DVL/Wayfinder /opt/Wayfinder
-WORKDIR /opt/Wayfinder
-
-# Ultralytics with NO GPU
-RUN python3 -m pip install --extra-index-url https://download.pytorch.org/whl/cpu ultralytics
-# Copy the Python Dependencies and Install them
-COPY ./requirements.txt /requirements.txt
-RUN python3 -m pip install --ignore-installed -r /requirements.txt
-
-# Install Default models for YOLO
-RUN echo "export MESA_GL_VERSION_OVERRIDE=3.3" >> /root/.bashrc
-
-# Install additional dependencies for the embedded node
-# Install tmux, vim, git, and htop in a single RUN command
-RUN apt-get update && apt-get install -y tmux vim git htop socat
-
-RUN apt-get update && apt-get install -y \
- ros-noetic-rviz \
- ros-noetic-rqt \
- ros-noetic-rosbag \
- ros-noetic-image-view \
- ros-noetic-tf \
- ros-noetic-tf2-ros \
- ros-noetic-image-transport \
- ros-noetic-laser-proc
-
-COPY ./devices/Arduino/HydrusModule /root/Arduino/libraries/HydrusModule
-
-COPY ./ /catkin_ws/src/hydrus-software-stack
-WORKDIR /catkin_ws/src/hydrus-software-stack
-
-
-CMD ["/bin/bash", "-c", "sleep infinity"]
diff --git a/docker/amd64/cuda/hydrus.Dockerfile b/docker/amd64/cuda/hydrus.Dockerfile
deleted file mode 100644
index 3d4cf0d..0000000
--- a/docker/amd64/cuda/hydrus.Dockerfile
+++ /dev/null
@@ -1,100 +0,0 @@
-# Use Ubuntu 20.04 as base image
-FROM ubuntu:20.04
-
-# Update package list
-ARG DEBIAN_FRONTEND=noninteractive
-RUN apt-get update && apt-get install -y lsb-release gnupg curl software-properties-common
-
-# Setup ROS repositories
-RUN sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
-RUN curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | apt-key add -
-
-# Install ROS Noetic base and catkin build tools
-RUN apt-get update && apt-get install -y ros-noetic-ros-base python3-catkin-tools ros-noetic-catkin
-
-# Add the deadsnakes PPA and install Python 3.8
-RUN add-apt-repository -y ppa:deadsnakes/ppa && \
- apt-get update && \
- apt-get install -y python3.8 python3.8-distutils python3.8-venv
-
-# Set Python 3.8 as the default
-RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1
-
-# Install pip for Python 3.8
-RUN curl -sS https://bootstrap.pypa.io/pip/3.8/get-pip.py | python3.8
-
-#Add kisak-mesa PPA (for latest graphics drivers)
-RUN add-apt-repository -y ppa:kisak/kisak-mesa
-
-
-# Camera and Computer Vision Dependencies Python-3
-RUN apt-get update && apt-get install -y \
- python3-pip \
- python3-numpy\
- python3-opencv \
- libgl1-mesa-glx \
- ros-noetic-cv-bridge \
- ros-noetic-vision-opencv\
- libbullet-dev \
- python3-empy
-
-
-RUN apt-get update && apt-get install -y\
- ros-noetic-tf2-geometry-msgs\
- python3-tf2-kdl
-
-
-RUN sudo sh -c \
- 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list' &&\
- curl https://packages.osrfoundation.org/gazebo.key | sudo apt-key add - &&\
- apt-get update && apt-get install -y ignition-fortress ros-noetic-ros-ign &&\
- echo "export GZ_VERSION=fortress" >> /root/.bashrc
-
-
-# Embedded Node Dependencies
-RUN apt-get install -y --no-install-recommends \
- gcc \
- curl \
- git
-
-# ROS setup
-RUN /bin/bash -c 'source /opt/ros/noetic/setup.bash && \
- mkdir -p /home/catkin_ws/src && \
- cd /home/catkin_ws/ && \
- catkin_make'
-RUN echo "source /opt/ros/noetic/setup.bash" >> /root/.bashrc
-
-# Install Arduino CLI and libraries
-WORKDIR /usr/local/
-RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh && \
- arduino-cli core update-index && \
- arduino-cli core install arduino:avr &&\
- arduino-cli lib install "Servo@1.2.1" && \
- arduino-cli lib install "BlueRobotics MS5837 Library@1.1.1"
-
-
-# Copy the Python Dependencies and Install them
-COPY ./requirements.txt /requirements.txt
-
-# Ultralytics with GPU
-RUN python3 -m pip install ultralytics
-RUN python3 -m pip install -r /requirements.txt
-
-# Install Default models for YOLO
-RUN curl -Lo /yolov8n.pt https://github.com/ultralytics/assets/releases/latest/download/yolov8n.pt
-RUN curl -Lo /yolov8s-world.pt https://github.com/ultralytics/assets/releases/latest/download/yolov8s-world.pt
-
-RUN apt-get install -y libeigen3-dev python3-tf2-kdl
-RUN apt-get update && apt-get install -y ros-noetic-tf2-geometry-msgs
-RUN echo "export MESA_GL_VERSION_OVERRIDE=3.3" >> /root/.bashrc
-
-# Install additional dependencies for the embedded node
-# Install tmux, vim, git, and htop in a single RUN command
-RUN apt-get update && apt-get install -y tmux vim git htop socat
-
-# Copy embedded Arduino code in the Arduino libraries folder
-COPY ./devices/Arduino/HydrusModule /root/Arduino/libraries/HydrusModule
-
-COPY ./ /catkin_ws/src/hydrus-software-stack
-WORKDIR /catkin_ws/src/hydrus-software-stack
-CMD ["/bin/bash", "-c", "sleep infinity"]
diff --git a/docker/amd64/foxy.Dockerfile b/docker/amd64/foxy.Dockerfile
new file mode 100644
index 0000000..04f8a9b
--- /dev/null
+++ b/docker/amd64/foxy.Dockerfile
@@ -0,0 +1,50 @@
+# Hydrus ROS 2 (Foxy) layer built on shared base
+FROM ubuntu:20.04
+
+ENV DEBIAN_FRONTEND=noninteractive \
+ LANG=en_US.UTF-8 \
+ LC_ALL=en_US.UTF-8
+
+# Common packages for all Hydrus images
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ gcc \
+ cmake \
+ git \
+ wget \
+ curl \
+ lsb-release \
+ gnupg \
+ software-properties-common \
+ ca-certificates \
+ tmux \
+ vim \
+ htop \
+ socat \
+ locales \
+ && locale-gen en_US en_US.UTF-8 \
+ && update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 \
+ && rm -rf /var/lib/apt/lists/*
+
+
+ENV ROS_DISTRO=foxy \
+ LANG=en_US.UTF-8 \
+ LC_ALL=en_US.UTF-8
+
+# Configure locale and add ROS 2 apt repository; install core + dev tools
+RUN locale-gen en_US en_US.UTF-8 && update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 \
+ && echo "deb [arch=$(dpkg --print-architecture)] http://packages.ros.org/ros2/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros2.list \
+ && curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key | apt-key add - \
+ && apt-get update \
+ && apt-get install -y --no-install-recommends \
+ ros-foxy-ros-base \
+ ros-foxy-tf2-ros \
+ ros-foxy-tf2-geometry-msgs \
+ ros-foxy-image-transport \
+ ros-foxy-rviz2 \
+ ros-foxy-rqt \
+ ros-foxy-rosbag2 \
+ ros-foxy-ros2bag \
+ ros-foxy-rqt-image-view \
+ python3-colcon-common-extensions \
+ && rm -rf /var/lib/apt/lists/*
diff --git a/docker/amd64/noetic.Dockerfile b/docker/amd64/noetic.Dockerfile
new file mode 100644
index 0000000..fca2418
--- /dev/null
+++ b/docker/amd64/noetic.Dockerfile
@@ -0,0 +1,46 @@
+# Hydrus ROS 1 (Noetic) layer built on shared base
+FROM ubuntu:20.04
+
+ENV DEBIAN_FRONTEND=noninteractive \
+ LANG=en_US.UTF-8 \
+ LC_ALL=en_US.UTF-8
+
+# Common packages for all Hydrus images
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ gcc \
+ cmake \
+ git \
+ wget \
+ curl \
+ lsb-release \
+ gnupg \
+ software-properties-common \
+ ca-certificates \
+ tmux \
+ vim \
+ htop \
+ socat \
+ locales \
+ && locale-gen en_US en_US.UTF-8 \
+ && update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 \
+ && rm -rf /var/lib/apt/lists/*
+
+
+# Add ROS 1 repo and install core + commonly used packages
+RUN echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list \
+ && curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | apt-key add - \
+ && apt-get update \
+ && apt-get install -y --no-install-recommends \
+ ros-noetic-ros-base \
+ ros-noetic-tf2-geometry-msgs \
+ python3-tf2-kdl \
+ ros-noetic-rviz \
+ ros-noetic-rqt \
+ ros-noetic-rosbag \
+ ros-noetic-image-view \
+ ros-noetic-tf \
+ ros-noetic-tf2-ros \
+ ros-noetic-image-transport \
+ ros-noetic-laser-proc \
+ && rm -rf /var/lib/apt/lists/*
diff --git a/docker/amd64/pkgmanagers.Dockerfile b/docker/amd64/pkgmanagers.Dockerfile
new file mode 100644
index 0000000..e724c09
--- /dev/null
+++ b/docker/amd64/pkgmanagers.Dockerfile
@@ -0,0 +1,39 @@
+# Hydrus package managers layer: Python uv, Arduino CLI, Cargo
+# Inherit from either the base or a ROS layer depending on use case
+ARG PARENT_IMAGE
+FROM ${PARENT_IMAGE}
+
+# Ensure common env is present if base is used
+ENV PATH="/root/.local/bin:${PATH}"
+
+# Python and venv utilities
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ python3 \
+ python3-venv \
+ python3-pip \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install uv (https://astral.sh/uv)
+RUN curl -LsSf https://astral.sh/uv/install.sh | sh
+
+# Install Arduino CLI (package manager for Arduino cores/libs)
+RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
+
+# Install Cargo (Rust package manager) without baking toolchains/deps
+RUN apt-get update && apt-get install -y --no-install-recommends cargo && rm -rf /var/lib/apt/lists/*
+
+# Set cache locations to bind-mounted repo by default; can be overridden at runtime
+ENV WORKDIR_PATH=/home/catkin_ws/src/hydrus-software-stack \
+ UV_CACHE_DIR=/home/catkin_ws/src/hydrus-software-stack/.uv-cache \
+ PIP_CACHE_DIR=/home/catkin_ws/src/hydrus-software-stack/.pip-cache \
+ ARDUINO_DATA_DIR=/home/catkin_ws/src/hydrus-software-stack/.arduino15 \
+ ARDUINO_SKETCHBOOK_DIR=/home/catkin_ws/src/hydrus-software-stack/Arduino \
+ CARGO_HOME=/home/catkin_ws/src/hydrus-software-stack/.cargo \
+ RUSTUP_HOME=/home/catkin_ws/src/hydrus-software-stack/.rustup \
+ PATH="/root/.local/bin:${CARGO_HOME}/bin:${PATH}"
+
+# Prepare directories and symlinks for caches
+RUN mkdir -p "$ARDUINO_DATA_DIR" "$ARDUINO_SKETCHBOOK_DIR/libraries" "$CARGO_HOME/bin" "$RUSTUP_HOME" \
+ && rm -rf /root/.arduino15 /root/Arduino \
+ && ln -s "$ARDUINO_DATA_DIR" /root/.arduino15 \
+ && ln -s "$ARDUINO_SKETCHBOOK_DIR" /root/Arduino
diff --git a/docker/docker-compose-amd64-cpu.yaml b/docker/docker-compose-amd64-cpu.yaml
index 46150f7..32927e5 100644
--- a/docker/docker-compose-amd64-cpu.yaml
+++ b/docker/docker-compose-amd64-cpu.yaml
@@ -1,5 +1,3 @@
-version: '3.8'
-
services:
ros-master:
image: ros:noetic-ros-core
@@ -14,36 +12,21 @@ services:
hydrus_cpu:
build:
context: ../../hydrus-software-stack
- dockerfile: docker/amd64/cpu/hydrus.Dockerfile
+ args:
+ PARENT_IMAGE: "noetic:latest"
+ dockerfile: docker/amd64/pkgmanagers.Dockerfile
privileged: true
stdin_open: true
tty: true
ports:
- "8000:8000"
- - "5000:5000" # Added port for Flask detection viewer
- devices:
- - "/dev/ttyACM0:/dev/ttyACM0"
- - "/dev/ttyACM1:/dev/ttyACM1" # Added ttyACM1 mapping
- - "/dev/ttyUSB0:/dev/ttyUSB0" # Added ttyUSB0 mapping
- - "/dev/dri:/dev/dri"
+ - "5000:5000"
environment:
- ROS_MASTER_URI=http://ros-master:11311
- - ARDUINO_BOARD=arduino:avr:uno
- - VOLUME
- - ROSBAG_PLAYBACK
- - DEBUG_ARDUINO
- - DISPLAY=${DISPLAY} # Pass the DISPLAY variable from the host
- - RVIZ
- - TEST
- - NO_BUILD
- - TMUX_SESSIONS
- - ARDUINO_COMPILE
- - VIRTUAL_ARDUINO
+ - DISPLAY=${DISPLAY}
volumes:
- "../:/home/catkin_ws/src/hydrus-software-stack"
- "/tmp/.X11-unix:/tmp/.X11-unix"
- - "../rosbags:/rosbags"
- - "../yolo_models:/yolo_models" # Volume for YOLO models
command: /bin/bash -c "sleep infinity"
depends_on:
- ros-master
diff --git a/docker/docker-compose-jetson-tx2.yaml b/docker/docker-compose-jetson-tx2.yaml
deleted file mode 100644
index 17f4367..0000000
--- a/docker/docker-compose-jetson-tx2.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-version: '3.8'
-
-services:
- ros-master:
- image: ros:noetic-ros-core
- command: stdbuf -o L roscore
- ports:
- - "11311:11311"
-
- zed-camera:
- build:
- context: ../
- dockerfile: jetson/camera.Dockerfile
- privileged: true
- deploy:
- resources:
- reservations:
- devices:
- - driver: nvidia
- count: all
- capabilities: [gpu]
- environment:
- - ROS_MASTER_URI=http://ros-master:11311
- depends_on:
- - ros-master
-
- hydrus_jetson:
- build:
- context: ../
- dockerfile: jetson/hydrus.Dockerfile
- privileged: true
- deploy:
- resources:
- reservations:
- devices:
- - driver: nvidia
- count: all
- capabilities: [gpu]
- stdin_open: true
- tty: true
- volumes:
- - "../:/home/catkin_ws/src"
- - "/tmp/.X11-unix:/tmp/.X11-unix"
- - "../rosbags:/rosbags"
- - "../yolo_models:/yolo_models" # Volume for YOLO models
- ports:
- - "8000:8000"
- devices:
- - "/dev/ttyACM0:/dev/ttyACM0"
- environment:
- - ROS_MASTER_URI=http://ros-master:11311
- - ARDUINO_BOARD=arduino:avr:uno
- - VOLUME
- - ROSBAG_PLAYBACK
- - DEBUG_ARDUINO
- - DISPLAY=${DISPLAY}
- - RVIZ
- - TEST
- - NO_BUILD
- - TMUX_SESSIONS
- - ARDUINO_COMPILE
- - VIRTUAL_ARDUINO
- command: /bin/bash -c "sleep infinity"
- depends_on:
- - ros-master
- - zed-camera
diff --git a/docker/run_docker.py b/docker/run_docker.py
deleted file mode 100755
index 9ed4a77..0000000
--- a/docker/run_docker.py
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env python3
-"""
-Hydrus Docker Deployment - Main Entry Point
-Simple wrapper that calls the modular hocker script
-"""
-
-import os
-import sys
-from pathlib import Path
-
-
-def main():
- """Entry point that delegates to the hocker script"""
- hocker_script = Path(__file__).parent / "hydrus-docker" / "hocker"
-
- if not hocker_script.exists():
- print("β Error: hocker script not found in hydrus-docker folder")
- sys.exit(1)
-
- # Pass all arguments to the hocker script and replace current process
- try:
- # Use execve to replace current process with hocker
- env = os.environ.copy()
- os.execve(
- sys.executable, [sys.executable, str(hocker_script)] + sys.argv[1:], env
- )
- except KeyboardInterrupt:
- print("\nπ Operation cancelled by user")
- sys.exit(1)
- except Exception as e:
- print(f"β Error running hocker: {e}")
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/hocker b/hocker
index 20b74cc..474d715 120000
--- a/hocker
+++ b/hocker
@@ -1 +1 @@
-/home/catkin_ws/src/hydrus-software-stack/docker/hydrus-docker/hocker.py
\ No newline at end of file
+/home/cesar/Projects/hydrus-software-stack/docker/hydrus-docker/hocker.py
\ No newline at end of file
diff --git a/hydrus-cli b/hydrus-cli
index 3074f4a..03199d0 100755
--- a/hydrus-cli
+++ b/hydrus-cli
@@ -5,9 +5,6 @@
# Get the directory where this wrapper is located (should be project root)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-# Set HYDRUS_ROOT environment variable to ensure test system works correctly
-export HYDRUS_ROOT="$SCRIPT_DIR"
-
# Change to the project directory to ensure proper module resolution
cd "$SCRIPT_DIR"
diff --git a/pyproject.toml b/pyproject.toml
index e32b941..cf0a375 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,46 +1,6 @@
-[tool.black]
-line-length = 88
-target-version = ['py38']
-include = '\.pyi?$'
-extend-exclude = '''
-/(
- # directories
- \.eggs
- | \.git
- | \.hg
- | \.mypy_cache
- | \.tox
- | \.venv
- | _build
- | buck-out
- | build
- | dist
-)/
-'''
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-line_length = 88
-known_first_party = ["autonomy", "mission_planner", "computer_vision"]
-known_third_party = ["rospy", "cv2", "numpy", "matplotlib", "flask", "fastapi"]
-
-[tool.mypy]
-python_version = "3.8"
-warn_return_any = true
-warn_unused_configs = true
-disallow_untyped_defs = true
-ignore_missing_imports = true
-
-[tool.flake8]
-max-line-length = 88
-extend-ignore = ["E203", "W503"]
-exclude = [
- ".git",
- "__pycache__",
- "docs/source/conf.py",
- "old",
- "build",
- "dist",
- ".venv"
+[tool.uv.workspace]
+members = [
+ "src/computer_vision",
+ "tools/hydrus_cli",
+ "autonomy",
]
diff --git a/scripts/__init__.py b/scripts/__init__.py
deleted file mode 100644
index 7994ee4..0000000
--- a/scripts/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Scripts package
diff --git a/scripts/activate_formatters.sh b/scripts/activate_formatters.sh
deleted file mode 100755
index ed89f1c..0000000
--- a/scripts/activate_formatters.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-# Quick activation script for formatter virtual environment
-echo "π Activating formatter virtual environment..."
-source .venv-formatters/bin/activate
-echo "β
Formatter environment activated!"
-echo "π‘ Available commands:"
-echo " - black . --line-length 88 --target-version py38"
-echo " - isort . --profile black"
-echo " - flake8 ."
-echo " - pre-commit run --all-files"
diff --git a/scripts/commands/dev.py b/scripts/commands/dev.py
deleted file mode 100644
index 7761bba..0000000
--- a/scripts/commands/dev.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# All the dev tools should be in here.
-# For example formatting tools, linters, etc.
-# I would like to have an internall interactive tool that lets you have
-# multiple devs tools enabled accessible from the cli.
diff --git a/scripts/commands/ros.py b/scripts/commands/ros.py
deleted file mode 100644
index dc22164..0000000
--- a/scripts/commands/ros.py
+++ /dev/null
@@ -1,567 +0,0 @@
-"""
-ROS command module for Hydrus CLI
-Handles ROS workspace operations, camera management, and rosbag utilities
-"""
-
-import os
-import re
-import subprocess
-import sys
-from pathlib import Path
-from typing import Dict, List, Optional
-
-import typer
-
-from .utils import get_building_path
-
-ros_app = typer.Typer()
-
-
-class HydrusRosManager:
- def __init__(self, volume: bool = False):
- self.volume = volume
- self.workspace_dir = get_building_path(self.volume)
- self.rosbags_dir = self.workspace_dir / "src/hydrus-software-stack/rosbags"
-
- # Create rosbags directory if it doesn't exist
- self.rosbags_dir.mkdir(parents=True, exist_ok=True)
-
- # Constants for download
- self.DEFAULT_ROSBAG_URL = "https://drive.google.com/file/d/16Lr-CbW1rW6rKh8_mWClTQMIjm2u0y8X/view?usp=drive_link"
-
- def _run_command(
- self,
- cmd: List[str],
- check: bool = True,
- capture_output: bool = False,
- env: Optional[Dict] = None,
- cwd: Optional[Path] = None,
- ) -> subprocess.CompletedProcess:
- """Run a command with proper error handling"""
- if env is None:
- env = os.environ.copy()
-
- cmd_str = " ".join(cmd)
- print(f"π§ Executing: {cmd_str}")
-
- try:
- return subprocess.run(
- cmd,
- check=check,
- capture_output=capture_output,
- env=env,
- cwd=cwd,
- text=True,
- )
- except subprocess.CalledProcessError as e:
- if check:
- print(f"β Command failed: {cmd_str}")
- print(f" Exit code: {e.returncode}")
- if capture_output and e.stderr:
- print(f" Error: {e.stderr}")
- raise
- except FileNotFoundError:
- print(f"β Command not found: {cmd_str}")
- raise
-
- def _setup_environment(self) -> Dict[str, str]:
- """Setup ROS environment"""
- env = os.environ.copy()
-
- # Source devel/setup.bash equivalent
- setup_file = self.workspace_dir / "devel/setup.bash"
- if setup_file.exists():
- # Add workspace paths to environment
- env[
- "ROS_PACKAGE_PATH"
- ] = f"{self.workspace_dir}/src:{env.get('ROS_PACKAGE_PATH', '')}"
- env[
- "CMAKE_PREFIX_PATH"
- ] = f"{self.workspace_dir}/devel:{env.get('CMAKE_PREFIX_PATH', '')}"
- env[
- "LD_LIBRARY_PATH"
- ] = f"{self.workspace_dir}/devel/lib:{env.get('LD_LIBRARY_PATH', '')}"
- env[
- "PYTHONPATH"
- ] = f"{self.workspace_dir}/devel/lib/python3/dist-packages:{env.get('PYTHONPATH', '')}"
-
- return env
-
- def build_workspace(self, clean: bool = False):
- """Build the catkin workspace."""
- print("π¨ Building catkin workspace...")
-
- if not self.workspace_dir.exists():
- print(f"β Workspace directory not found: {self.workspace_dir}")
- return False
-
- env = self._setup_environment()
-
- # Clean if requested
- if clean:
- print("π§Ή Cleaning workspace...")
- clean_cmd = ["catkin_make", "clean"]
- try:
- self._run_command(
- clean_cmd, cwd=self.workspace_dir, env=env, check=False
- )
- except subprocess.CalledProcessError:
- print("Warning: Clean failed, but continuing...")
-
- # Build command that sources ROS environment first
- ros_distro = env.get("ROS_DISTRO", "noetic")
- cmake_command = f"source /opt/ros/{ros_distro}/setup.bash && catkin_make --cmake-args -DCMAKE_BUILD_TYPE=Release -DPYTHON_EXECUTABLE=/usr/bin/python3 -DPYTHON_INCLUDE_DIR=/usr/include/python3.9 -DPYTHON_LIBRARY=/usr/lib/aarch64-linux-gnu/libpython3.9.so"
-
- cmake_args = ["bash", "-c", cmake_command]
-
- try:
- self._run_command(cmake_args, cwd=self.workspace_dir, env=env)
- print("β
Workspace built successfully")
- return True
- except subprocess.CalledProcessError as e:
- print(f"β Build failed: {e}")
- return False
-
- def run_camera_entrypoint(self, zed_option: bool = False):
- """Run the camera entrypoint."""
- print("π· Starting ZED camera entrypoint...")
- print(f"ZED_OPTION: {zed_option}")
-
- env = self._setup_environment()
- env["ZED_OPTION"] = "true" if zed_option else "false"
-
- try:
- # Build workspace first
- if not self.build_workspace():
- print("β Failed to build workspace")
- return False
-
- # Launch ZED camera
- if zed_option:
- print("Launching ZED2i camera with RViz display...")
- launch_cmd = ["roslaunch", "zed_display_rviz", "display_zed2i.launch"]
- else:
- print("Launching ZED2i camera...")
- launch_cmd = ["roslaunch", "--wait", "zed_wrapper", "zed2i.launch"]
-
- self._run_command(launch_cmd, env=env, cwd=self.workspace_dir)
- return True
-
- except subprocess.CalledProcessError as e:
- print(f"β Failed to launch ZED camera: {e}")
- return False
- except KeyboardInterrupt:
- print("\nπ Camera entrypoint stopped by user")
- return True
- except Exception as e:
- print(f"β Unexpected error: {e}")
- return False
-
- def _extract_file_id(self, url: str) -> Optional[str]:
- """Extract the file ID from a Google Drive URL"""
- pattern = r"drive\.google\.com/file/d/([a-zA-Z0-9_-]+)"
- match = re.search(pattern, url)
- if match:
- return match.group(1)
- return None
-
- def _install_download_dependencies(self) -> bool:
- """Install required dependencies for downloading"""
- try:
- print("Installing gdown and requests...")
- self._run_command(
- [sys.executable, "-m", "pip", "install", "gdown", "requests"]
- )
- print("β
Dependencies installed successfully!")
- return True
- except subprocess.CalledProcessError:
- print("β Failed to install dependencies. Please install them manually:")
- print("pip install gdown requests")
- print("\nOr if you have permission issues, try:")
- print("pip install --user gdown requests")
- return False
-
- def _check_rosbags_exist(self) -> bool:
- """Check if any .bag files exist in the rosbags directory"""
- bag_files = list(self.rosbags_dir.glob("*.bag"))
- return len(bag_files) > 0
-
- def download_ros_bags(self, url: Optional[str] = None, force: bool = False):
- """Download ROS bags."""
- print("π₯ Checking for ROS bag files...")
-
- if not force and self._check_rosbags_exist():
- print("β
ROS bag files already exist in the rosbags directory.")
- existing_bags = list(self.rosbags_dir.glob("*.bag"))
- print("Existing bag files:")
- for bag in existing_bags:
- print(f" β’ {bag.name}")
- return True
-
- if not force:
- print("No ROS bag files found.")
-
- download_url = url or self.DEFAULT_ROSBAG_URL
-
- # Install dependencies
- if not self._install_download_dependencies():
- return False
-
- print(f"π₯ Downloading rosbag from {download_url}")
- print("This may take a few minutes depending on your internet connection...")
-
- # Import gdown here to avoid import error if not installed
- try:
- import gdown
- except ImportError:
- print("β Error: gdown is not available. Please run the installation first.")
- return False
-
- file_id = self._extract_file_id(download_url)
- if not file_id:
- print("β Error: Could not extract file ID from the URL.")
- return False
-
- output_path = self.rosbags_dir / "zed2i_camera.bag"
-
- try:
- # Use gdown to download from Google Drive
- gdown.download(id=file_id, output=str(output_path), quiet=False)
-
- if output_path.exists() and output_path.stat().st_size > 0:
- print(f"β
Successfully downloaded rosbag to {output_path}")
- return True
- else:
- print("β Error: Download failed or file is empty.")
- return False
- except Exception as e:
- print(f"β Error downloading file: {e}")
- return False
-
- def video_to_ros_bag(
- self,
- video_path: str,
- output_bag_path: Optional[str] = None,
- target_fps: float = 10.0,
- include_camera_info: bool = False,
- rgb_topic: str = "/zed2i/zed_node/rgb/image_rect_color",
- camera_info_topic: str = "/zed2i/zed_node/rgb/camera_info",
- frame_id: str = "zed2i_left_camera_optical_frame",
- ):
- """Convert video files to ROS bags."""
- print("π¬ Converting video to ROS bag...")
- print(f"Input video: {video_path}")
-
- video_path_obj = Path(video_path)
- if not video_path_obj.exists():
- print(f"β Error: Video file not found: {video_path}")
- return False
-
- # Set default output path if not provided
- if output_bag_path is None:
- output_bag_path = str(self.rosbags_dir / f"{video_path_obj.stem}.bag")
-
- print(f"Output bag: {output_bag_path}")
- print(f"Target FPS: {target_fps}")
- print(f"Include camera info: {include_camera_info}")
-
- try:
- import cv2
- import rosbag
- import rospy
- except ImportError as e:
- print(f"β Error: Required dependencies not found: {e}")
- print("Please install: pip install opencv-python")
- print("And ensure ROS packages are available in the environment")
- return False
-
- # Open the video file
- cap = cv2.VideoCapture(video_path)
- if not cap.isOpened():
- print(f"β Error: Could not open video file: {video_path}")
- return False
-
- # Get video properties
- original_fps = cap.get(cv2.CAP_PROP_FPS)
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
- width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
- height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
-
- print("Video properties:")
- print(f" β’ Resolution: {width}x{height}")
- print(f" β’ Original FPS: {original_fps}")
- print(f" β’ Total frames: {total_frames}")
-
- # Calculate frame skip for target FPS
- frame_skip = max(1, int(original_fps / target_fps))
- duration_seconds = total_frames / original_fps
- expected_output_frames = int(duration_seconds * target_fps)
-
- print("Conversion settings:")
- print(f" β’ Frame skip: {frame_skip}")
- print(f" β’ Expected output frames: {expected_output_frames}")
-
- try:
- with rosbag.Bag(output_bag_path, "w") as bag:
- frame_count = 0
- written_frames = 0
-
- # Create camera info message if requested
- camera_info = None
- if include_camera_info:
- camera_info = self._create_camera_info(width, height, frame_id)
-
- while True:
- ret, frame = cap.read()
- if not ret:
- break
-
- # Skip frames to achieve target FPS
- if frame_count % frame_skip != 0:
- frame_count += 1
- continue
-
- # Calculate timestamp
- timestamp = rospy.Time.from_sec(written_frames / target_fps)
-
- # Convert OpenCV image to ROS Image message
- ros_image = self._cv2_to_ros_image(frame, "bgr8", frame_id)
- ros_image.header.stamp = timestamp
-
- # Write image message to bag
- bag.write(rgb_topic, ros_image, timestamp)
-
- # Write camera info if requested
- if camera_info:
- camera_info.header.stamp = timestamp
- bag.write(camera_info_topic, camera_info, timestamp)
-
- written_frames += 1
- frame_count += 1
-
- # Progress update
- if written_frames % 50 == 0:
- progress = (frame_count / total_frames) * 100
- print(
- f"Progress: {progress:.1f}% ({written_frames} frames written)"
- )
-
- cap.release()
-
- print("β
Conversion completed successfully!")
- print(f" β’ Output file: {output_bag_path}")
- print(f" β’ Frames written: {written_frames}")
- print(f" β’ Duration: {written_frames / target_fps:.2f} seconds")
- return True
-
- except Exception as e:
- print(f"β Error during conversion: {e}")
- cap.release()
- return False
-
- def _cv2_to_ros_image(
- self, cv_image, encoding: str = "bgr8", frame_id: str = "camera_link"
- ):
- """Convert an OpenCV image to a ROS sensor_msgs/Image message."""
- from sensor_msgs.msg import Image
-
- ros_image = Image()
- ros_image.header.frame_id = frame_id
- ros_image.height, ros_image.width = cv_image.shape[:2]
- ros_image.encoding = encoding
- ros_image.is_bigendian = False
- ros_image.step = (
- cv_image.strides[0]
- if cv_image.strides
- else cv_image.shape[1]
- * cv_image.itemsize
- * (cv_image.shape[2] if len(cv_image.shape) > 2 else 1)
- )
- ros_image.data = cv_image.tobytes()
- return ros_image
-
- def _create_camera_info(
- self, width: int, height: int, frame_id: str = "camera_link"
- ):
- """Create a basic CameraInfo message with reasonable defaults."""
- from sensor_msgs.msg import CameraInfo
-
- camera_info = CameraInfo()
- camera_info.header.frame_id = frame_id
- camera_info.width = width
- camera_info.height = height
-
- # Basic camera parameters (these are rough estimates)
- fx = fy = width * 0.8 # Rough focal length estimate
- cx = width / 2.0 # Principal point x
- cy = height / 2.0 # Principal point y
-
- camera_info.K = [fx, 0, cx, 0, fy, cy, 0, 0, 1]
- camera_info.D = [0, 0, 0, 0, 0] # No distortion
- camera_info.R = [1, 0, 0, 0, 1, 0, 0, 0, 1]
- camera_info.P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0]
- camera_info.distortion_model = "plumb_bob"
-
- return camera_info
-
-
-# === Typer Commands ===
-
-
-@ros_app.command("build")
-def build_workspace_cmd(
- volume: bool = typer.Option(False, "--volume", "-v", help="Use volume directory"),
- clean: bool = typer.Option(
- False, "--clean", "-c", help="Clean workspace before building"
- ),
-):
- """Build the catkin workspace."""
- typer.echo("π¨ Building ROS workspace...")
-
- manager = HydrusRosManager(volume=volume)
- success = manager.build_workspace(clean=clean)
-
- if success:
- typer.echo("β
Workspace built successfully!")
- raise typer.Exit(0)
- else:
- typer.echo("β Workspace build failed!")
- raise typer.Exit(1)
-
-
-@ros_app.command("camera")
-def run_camera_cmd(
- volume: bool = typer.Option(False, "--volume", "-v", help="Use volume directory"),
- rviz: bool = typer.Option(
- False, "--rviz", "-r", help="Launch camera with RViz display"
- ),
-):
- """Run the ZED camera entrypoint."""
- typer.echo("π· Starting ZED camera...")
-
- manager = HydrusRosManager(volume=volume)
- success = manager.run_camera_entrypoint(zed_option=rviz)
-
- if success:
- typer.echo("β
Camera stopped successfully!")
- raise typer.Exit(0)
- else:
- typer.echo("β Camera failed!")
- raise typer.Exit(1)
-
-
-@ros_app.command("download-bags")
-def download_bags_cmd(
- volume: bool = typer.Option(False, "--volume", "-v", help="Use volume directory"),
- url: Optional[str] = typer.Option(None, "--url", "-u", help="Custom download URL"),
- force: bool = typer.Option(
- False, "--force", "-f", help="Force download even if bags exist"
- ),
-):
- """Download ROS bag files."""
- typer.echo("π₯ Downloading ROS bags...")
-
- manager = HydrusRosManager(volume=volume)
- success = manager.download_ros_bags(url=url, force=force)
-
- if success:
- typer.echo("β
ROS bags downloaded successfully!")
- raise typer.Exit(0)
- else:
- typer.echo("β ROS bags download failed!")
- raise typer.Exit(1)
-
-
-@ros_app.command("video-to-bag")
-def video_to_bag_cmd(
- video_path: str = typer.Argument(..., help="Path to input video file"),
- output_path: Optional[str] = typer.Option(
- None, "--output", "-o", help="Output bag file path"
- ),
- volume: bool = typer.Option(False, "--volume", "-v", help="Use volume directory"),
- fps: float = typer.Option(10.0, "--fps", "-f", help="Target FPS for output bag"),
- camera_info: bool = typer.Option(
- False, "--camera-info", "-c", help="Include camera info messages"
- ),
- rgb_topic: str = typer.Option(
- "/zed2i/zed_node/rgb/image_rect_color", "--rgb-topic", help="RGB image topic"
- ),
- info_topic: str = typer.Option(
- "/zed2i/zed_node/rgb/camera_info", "--info-topic", help="Camera info topic"
- ),
- frame_id: str = typer.Option(
- "zed2i_left_camera_optical_frame", "--frame-id", help="Frame ID for messages"
- ),
-):
- """Convert video file to ROS bag."""
- typer.echo(f"π¬ Converting video {video_path} to ROS bag...")
-
- manager = HydrusRosManager(volume=volume)
- success = manager.video_to_ros_bag(
- video_path=video_path,
- output_bag_path=output_path,
- target_fps=fps,
- include_camera_info=camera_info,
- rgb_topic=rgb_topic,
- camera_info_topic=info_topic,
- frame_id=frame_id,
- )
-
- if success:
- typer.echo("β
Video converted to ROS bag successfully!")
- raise typer.Exit(0)
- else:
- typer.echo("β Video conversion failed!")
- raise typer.Exit(1)
-
-
-@ros_app.command("list-bags")
-def list_bags_cmd(
- volume: bool = typer.Option(False, "--volume", "-v", help="Use volume directory")
-):
- """List available ROS bag files."""
- manager = HydrusRosManager(volume=volume)
-
- bag_files = list(manager.rosbags_dir.glob("*.bag"))
-
- if bag_files:
- typer.echo("π Available ROS bag files:")
- for bag in bag_files:
- size_mb = bag.stat().st_size / (1024 * 1024)
- typer.echo(f" β’ {bag.name} ({size_mb:.1f} MB)")
- else:
- typer.echo("π No ROS bag files found.")
- typer.echo(f"Directory: {manager.rosbags_dir}")
-
-
-@ros_app.command("info")
-def ros_info_cmd(
- volume: bool = typer.Option(False, "--volume", "-v", help="Use volume directory")
-):
- """Show ROS workspace information."""
- manager = HydrusRosManager(volume=volume)
-
- typer.echo("π ROS Workspace Information:")
- typer.echo(f" β’ Workspace path: {manager.workspace_dir}")
- typer.echo(f" β’ ROSbags directory: {manager.rosbags_dir}")
-
- # Check if workspace exists
- if manager.workspace_dir.exists():
- typer.echo(" β’ Workspace: β
Found")
-
- # Check if built
- devel_dir = manager.workspace_dir / "devel"
- if devel_dir.exists():
- typer.echo(" β’ Built: β
Yes")
- else:
- typer.echo(" β’ Built: β No")
- else:
- typer.echo(" β’ Workspace: β Not found")
-
- # Check bag files
- bag_count = len(list(manager.rosbags_dir.glob("*.bag")))
- typer.echo(f" β’ ROS bag files: {bag_count}")
-
-
-if __name__ == "__main__":
- ros_app()
diff --git a/scripts/commands/runner.py b/scripts/commands/runner.py
deleted file mode 100644
index 1494d36..0000000
--- a/scripts/commands/runner.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# Runner should be used to run scripts that are in the repository.
-# Runner could have a smart breakpoint checker
diff --git a/scripts/commands/test/__init__.py b/scripts/commands/test/__init__.py
deleted file mode 100644
index 3ce1e0d..0000000
--- a/scripts/commands/test/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""
-Test command module for Hydrus software stack.
-
-This module contains all test-related functionality including:
-- Test execution and management
-- Test configuration loading from distributed .hss files
-- Test logging and result archiving
-- Test caching functionality
-"""
-
-from .test import test_app
-
-__all__ = ["test_app"]
diff --git a/scripts/commands/test/test_cache.py b/scripts/commands/test/test_cache.py
deleted file mode 100644
index e69de29..0000000
diff --git a/scripts/format.sh b/scripts/format.sh
deleted file mode 100755
index 66477bc..0000000
--- a/scripts/format.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/bash
-# Python Code Formatting Script for Hydrus Software Stack
-
-echo "π§ Running Python code formatters..."
-
-# Check if we're in the right directory
-if [ ! -f "requirements.txt" ]; then
- echo "β Error: Please run this script from the project root directory"
- exit 1
-fi
-
-# Activate virtual environment if it exists, otherwise try system packages
-if [ -d ".venv" ]; then
- echo "π§ Activating virtual environment..."
- source .venv/bin/activate
-else
- echo "β οΈ Virtual environment not found, trying system packages..."
- echo "π‘ Run './setup_formatting.sh' to set up the virtual environment"
-fi
-
-# Check if tools are available
-if ! command -v black &> /dev/null; then
- echo "β Black not found. Please run './setup_formatting.sh' first"
- exit 1
-fi
-
-echo "π¨ Formatting Python code with Black..."
-black . --line-length 88 --target-version py38
-
-echo "π Sorting imports with isort..."
-isort . --profile black
-
-echo "π Running flake8 linting..."
-flake8 . --max-line-length=88 --extend-ignore=E203,W503
-
-echo "β
Code formatting complete!"
-echo ""
-echo "π Summary:"
-echo " - Black: Code formatted to 88 character lines"
-echo " - isort: Import statements sorted and organized"
-echo " - flake8: Linting checks completed"
-echo ""
-echo "π‘ To set up automatic formatting on git commits, run:"
-echo " pre-commit install"
diff --git a/scripts/format_with_venv.sh b/scripts/format_with_venv.sh
deleted file mode 100755
index 19257e0..0000000
--- a/scripts/format_with_venv.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/bash
-# Format script using the dedicated virtual environment
-set -e
-
-echo "π§ Running Python formatters..."
-
-# Check if virtual environment exists
-if [ ! -d ".venv-formatters" ]; then
- echo "β Virtual environment not found. Run './setup_venv_formatters.sh' first"
- exit 1
-fi
-
-# Activate virtual environment
-source .venv-formatters/bin/activate
-
-# Run formatters
-echo "π¨ Formatting with Black..."
-black . --line-length 88 --target-version py38
-
-echo "π Sorting imports with isort..."
-isort . --profile black
-
-echo "π Running flake8 linting..."
-flake8 . --statistics
-
-echo "β
Code formatting complete!"
diff --git a/scripts/install_ros2_jassy.sh b/scripts/install_ros2_jassy.sh
deleted file mode 100644
index 6f651c2..0000000
--- a/scripts/install_ros2_jassy.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/bin/bash
-set -e
-
-ROS_DISTRO=jazzy
-UBU_CODENAME=$(lsb_release -cs)
-
-echo "π Installing ROS 2 $ROS_DISTRO on Ubuntu $UBU_CODENAME..."
-
-# Locale
-sudo apt update && sudo apt install -y locales
-sudo locale-gen en_US en_US.UTF-8
-sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
-export LANG=en_US.UTF-8
-
-# Repos & keys
-sudo apt install -y software-properties-common curl gnupg
-sudo add-apt-repository universe -y
-sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
- -o /usr/share/keyrings/ros-archive-keyring.gpg
-echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
- http://packages.ros.org/ros2/ubuntu $UBU_CODENAME main" | \
- sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
-
-# Install
-sudo apt update
-sudo apt install -y ros-$ROS_DISTRO-desktop \
- python3-colcon-common-extensions python3-pip python3-rosdep
-
-# Rosdep init
-sudo rosdep init || true
-rosdep update
-
-# Source on startup
-if ! grep -F "source /opt/ros/$ROS_DISTRO/setup.bash" ~/.bashrc >/dev/null; then
- echo "source /opt/ros/$ROS_DISTRO/setup.bash" >> ~/.bashrc
-fi
-source ~/.bashrc
-
-echo "β
ROS 2 $ROS_DISTRO installation complete!"
diff --git a/scripts/jetson.py b/scripts/jetson.py
deleted file mode 100755
index 60518ed..0000000
--- a/scripts/jetson.py
+++ /dev/null
@@ -1,383 +0,0 @@
-#!/usr/bin/env python3
-"""
-Hydrus Jetson Deployment Script
-Replaces jetson.sh with improved modularity and error handling
-"""
-
-import os
-import platform
-import subprocess
-import sys
-from pathlib import Path
-from typing import Dict, List, Optional
-
-
-class JetsonDeploymentManager:
- def __init__(self):
- self.network_name = "ros-network"
- self.ros_master_uri = "http://ros-master:11311"
- self.use_qemu = False
- self.qemu_args = []
- self.zed_option = False
- self.deploy = True
-
- # Detect architecture and setup QEMU if needed
- self._setup_architecture()
-
- def _run_command(
- self,
- cmd: List[str],
- check: bool = True,
- capture_output: bool = False,
- env: Optional[Dict] = None,
- ) -> subprocess.CompletedProcess:
- """Run a command with proper error handling"""
- if env is None:
- env = os.environ.copy()
-
- try:
- return subprocess.run(
- cmd, check=check, capture_output=capture_output, env=env, text=True
- )
- except subprocess.CalledProcessError as e:
- if check:
- print(f"Command failed: {' '.join(cmd)}")
- print(f"Exit code: {e.returncode}")
- if capture_output and e.stderr:
- print(f"Error: {e.stderr}")
- raise
-
- def _setup_architecture(self):
- """Detect system architecture and set up QEMU if needed"""
- arch = platform.machine()
-
- if arch == "x86_64":
- print("Detected x86_64 architecture. Will use QEMU for ARM emulation.")
-
- # Check if QEMU is installed
- try:
- self._run_command(
- ["qemu-system-aarch64", "--version"], capture_output=True
- )
- except (subprocess.CalledProcessError, FileNotFoundError):
- print("ERROR: QEMU is not installed. Please install it with:")
- print(
- " sudo apt-get install qemu-system-arm qemu-efi qemu-user-static"
- )
- sys.exit(1)
-
- # Register QEMU binary formats if not already done
- binfmt_file = Path("/proc/sys/fs/binfmt_misc/qemu-aarch64")
- if not binfmt_file.exists():
- print("Setting up QEMU binary formats for ARM emulation...")
- try:
- self._run_command(
- [
- "docker",
- "run",
- "--rm",
- "--privileged",
- "multiarch/qemu-user-static",
- "--reset",
- "-p",
- "yes",
- ]
- )
- except subprocess.CalledProcessError as e:
- print(f"Failed to setup QEMU binary formats: {e}")
- sys.exit(1)
-
- self.use_qemu = True
- self.qemu_args = ["--platform", "linux/arm64"]
- print("QEMU emulation enabled for ARM containers.")
-
- def _create_docker_network(self):
- """Create custom Docker network if it doesn't exist"""
- try:
- # Check if network exists
- result = self._run_command(
- ["docker", "network", "ls", "--format", "{{.Name}}"],
- capture_output=True,
- )
-
- if self.network_name not in result.stdout:
- print(f"Creating custom Docker network: {self.network_name}")
- self._run_command(["docker", "network", "create", self.network_name])
- else:
- print(f"Docker network {self.network_name} already exists")
-
- except subprocess.CalledProcessError as e:
- print(f"Failed to create Docker network: {e}")
- sys.exit(1)
-
- def _container_exists(self, container_name: str) -> bool:
- """Check if a container exists"""
- try:
- result = self._run_command(
- ["docker", "ps", "-a", "--format", "{{.Names}}"], capture_output=True
- )
- return container_name in result.stdout.split("\n")
- except subprocess.CalledProcessError:
- return False
-
- def _container_running(self, container_name: str) -> bool:
- """Check if a container is running"""
- try:
- result = self._run_command(
- ["docker", "ps", "--format", "{{.Names}}"], capture_output=True
- )
- return container_name in result.stdout.split("\n")
- except subprocess.CalledProcessError:
- return False
-
- def _start_ros_master(self):
- """Run the ROS master container"""
- print("=" * 50)
- print("Step 1: Setting up ROS master container")
- print("=" * 50)
-
- if self._container_exists("ros-master"):
- if self._container_running("ros-master"):
- print("ROS master is already running.")
- else:
- print("Starting existing ROS master container...")
- self._run_command(["docker", "start", "ros-master"])
- else:
- print("Creating and starting ROS master container...")
- cmd = [
- "docker",
- "run",
- "-d",
- "--name",
- "ros-master",
- "--network",
- self.network_name,
- "-p",
- "11311:11311",
- "ros:melodic-ros-core",
- "stdbuf",
- "-o",
- "L",
- "roscore",
- ]
- self._run_command(cmd)
-
- # Wait for ROS master to be up
- print("Waiting for ROS master to start...")
- import time
-
- time.sleep(3)
-
- def _build_and_run_zed_camera(self):
- """Build and run the ZED camera container"""
- print("=" * 50)
- print("Step 2: Setting up ZED camera container")
- print("=" * 50)
-
- dockerfile_path = "docker/jetson/camera.Dockerfile"
-
- # Build ZED camera container
- if self.use_qemu:
- print("Building ZED camera container with QEMU emulation...")
- build_cmd = (
- ["docker", "build"]
- + self.qemu_args
- + ["-t", "zed-camera", "-f", dockerfile_path, "."]
- )
- else:
- print("Building ZED camera container...")
- build_cmd = [
- "docker",
- "build",
- "-t",
- "zed-camera",
- "-f",
- dockerfile_path,
- ".",
- ]
-
- self._run_command(build_cmd)
-
- # Run ZED camera container
- if self._container_exists("zed-camera"):
- if self._container_running("zed-camera"):
- print("ZED camera container is already running.")
- else:
- print("Starting existing ZED camera container...")
- self._run_command(["docker", "start", "zed-camera"])
- else:
- print("Creating and starting ZED camera container...")
-
- env_vars = {
- "ROS_MASTER_URI": "http://ros-master:11311",
- "ZED_OPTION": str(self.zed_option).lower(),
- }
-
- cmd = [
- "docker",
- "run",
- "-d",
- "--name",
- "zed-camera",
- "--network",
- self.network_name,
- "--privileged",
- "--gpus",
- "all",
- ]
-
- # Add environment variables
- for key, value in env_vars.items():
- cmd.extend(["--env", f"{key}={value}"])
-
- # Add QEMU args if needed
- if self.use_qemu:
- cmd.extend(self.qemu_args)
-
- cmd.append("zed-camera")
- self._run_command(cmd)
-
- # Wait for ZED camera to be ready
- print("Waiting for ZED camera to start...")
- import time
-
- time.sleep(3)
-
- def _build_and_run_hydrus(self):
- """Build and run the Hydrus container"""
- print("=" * 50)
- print("Step 3: Setting up Hydrus container")
- print("=" * 50)
-
- dockerfile_path = "docker/jetson/hydrus.Dockerfile"
-
- # Check if Hydrus image exists
- try:
- result = self._run_command(
- [
- "docker",
- "images",
- "hydrus:latest",
- "--format",
- "{{.Repository}}:{{.Tag}}",
- ],
- capture_output=True,
- )
-
- if "hydrus:latest" not in result.stdout:
- print("Building Hydrus image...")
- if self.use_qemu:
- build_cmd = (
- ["docker", "build"]
- + self.qemu_args
- + ["-t", "hydrus:latest", "-f", dockerfile_path, "."]
- )
- else:
- build_cmd = [
- "docker",
- "build",
- "-t",
- "hydrus:latest",
- "-f",
- dockerfile_path,
- ".",
- ]
-
- self._run_command(build_cmd)
- else:
- print("Hydrus image already exists, skipping build.")
- except subprocess.CalledProcessError:
- print("Error checking Hydrus image, building anyway...")
- if self.use_qemu:
- build_cmd = (
- ["docker", "build"]
- + self.qemu_args
- + ["-t", "hydrus:latest", "-f", dockerfile_path, "."]
- )
- else:
- build_cmd = [
- "docker",
- "build",
- "-t",
- "hydrus:latest",
- "-f",
- dockerfile_path,
- ".",
- ]
-
- self._run_command(build_cmd)
-
- # Run Hydrus container
- if self._container_exists("hydrus"):
- if self._container_running("hydrus"):
- print("Hydrus container is already running.")
- else:
- print("Starting existing Hydrus container...")
- self._run_command(["docker", "start", "hydrus"])
- else:
- print("Creating and starting Hydrus container...")
-
- env_vars = {
- "ROS_MASTER_URI": "http://ros-master:11311",
- "ARDUINO_BOARD": "arduino:avr:mega",
- "DEPLOY": str(self.deploy).lower(),
- }
-
- cmd = [
- "docker",
- "run",
- "-d",
- "--name",
- "hydrus",
- "--network",
- self.network_name,
- "--privileged",
- "--gpus",
- "all",
- "-p",
- "8000:8000",
- "--device",
- "/dev/ttyACM0:/dev/ttyACM0",
- ]
-
- # Add environment variables
- for key, value in env_vars.items():
- cmd.extend(["--env", f"{key}={value}"])
-
- # Add QEMU args if needed
- if self.use_qemu:
- cmd.extend(self.qemu_args)
-
- cmd.extend(["-it", "hydrus:latest"])
- self._run_command(cmd)
-
- def main(self):
- """Main execution function"""
- print("Starting Jetson deployment...")
-
- # Set environment variables
- os.environ["ROS_MASTER_URI"] = self.ros_master_uri
-
- try:
- # Create Docker network
- self._create_docker_network()
-
- # Start ROS master
- self._start_ros_master()
-
- # Build and run ZED camera
- self._build_and_run_zed_camera()
-
- # Build and run Hydrus
- self._build_and_run_hydrus()
-
- print(f"Containers are up and running on network '{self.network_name}'!")
-
- except Exception as e:
- print(f"Deployment failed: {e}")
- sys.exit(1)
-
-
-if __name__ == "__main__":
- manager = JetsonDeploymentManager()
- manager.main()
diff --git a/scripts/setup_formatting.sh b/scripts/setup_formatting.sh
deleted file mode 100755
index ed048a0..0000000
--- a/scripts/setup_formatting.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/bin/bash
-# Setup Script for Python Code Formatting Tools
-
-echo "π Setting up Python code formatting for Hydrus Software Stack..."
-
-# Check if we need to install python3-venv
-if ! python3 -c "import venv" 2>/dev/null; then
- echo "π¦ Installing python3-venv package..."
- sudo apt update && sudo apt install -y python3-venv python3-pip
-fi
-
-# Create virtual environment if it doesn't exist
-if [ ! -d ".venv" ]; then
- echo "π§ Creating Python virtual environment..."
- python3 -m venv .venv
-fi
-
-# Activate virtual environment and install dependencies
-echo "π¦ Installing formatting dependencies in virtual environment..."
-source .venv/bin/activate
-pip install --upgrade pip
-pip install black isort flake8 pre-commit mypy
-
-# Setup pre-commit hooks
-echo "π Setting up pre-commit hooks..."
-pre-commit install
-
-echo "π― Running initial format on existing code..."
-# Run formatting with virtual environment
-source .venv/bin/activate && ./format.sh
-
-echo "β
Setup complete!"
-echo ""
-echo "π Your Python formatting tools are now configured:"
-echo " β’ Black: Automatic code formatting"
-echo " β’ isort: Import statement organization"
-echo " β’ flake8: Code linting and style checking"
-echo " β’ pre-commit: Automatic formatting before commits"
-echo ""
-echo "π Usage:"
-echo " β’ Run './format.sh' to format all Python files"
-echo " β’ Formatting will run automatically on git commits"
-echo " β’ Use 'source .venv/bin/activate && pre-commit run --all-files' to check all files"
-echo ""
-echo "π‘ Note: The tools are installed in a virtual environment (.venv)"
diff --git a/scripts/setup_venv_formatters.sh b/scripts/setup_venv_formatters.sh
deleted file mode 100755
index fa00f9f..0000000
--- a/scripts/setup_venv_formatters.sh
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/bin/bash
-# Virtual Environment Setup Script for Python Formatters
-# This script creates a virtual environment and installs the latest available formatting tools
-
-set -e # Exit on any error
-
-echo "π Setting up virtual environment with Python formatters..."
-
-# Script configuration
-VENV_NAME=".venv-formatters"
-PYTHON_VERSION="python3"
-
-# Check if Python is available
-if ! command -v $PYTHON_VERSION &> /dev/null; then
- echo "β Error: $PYTHON_VERSION is not installed or not in PATH"
- exit 1
-fi
-
-# Remove existing virtual environment if it exists
-if [ -d "$VENV_NAME" ]; then
- echo "ποΈ Removing existing virtual environment..."
- rm -rf "$VENV_NAME"
-fi
-
-# Create virtual environment
-echo "π§ Creating virtual environment: $VENV_NAME"
-$PYTHON_VERSION -m venv "$VENV_NAME"
-
-# Activate virtual environment
-echo "π Activating virtual environment..."
-source "$VENV_NAME/bin/activate"
-
-# Upgrade pip
-echo "π¦ Upgrading pip..."
-python -m pip install --upgrade pip
-
-# Install formatting tools with latest compatible versions
-echo "π¨ Installing Python formatting tools..."
-echo " - Installing Black (Python code formatter)..."
-pip install "black>=24.0.0,<25.0.0"
-
-echo " - Installing isort (Import sorter)..."
-pip install "isort>=5.12.0,<7.0.0"
-
-echo " - Installing flake8 (Linter)..."
-pip install "flake8>=6.0.0,<8.0.0"
-
-echo " - Installing pre-commit (Git hooks)..."
-pip install "pre-commit>=3.0.0,<4.0.0"
-
-echo " - Installing mypy (Type checker)..."
-pip install "mypy>=1.0.0,<2.0.0"
-
-# Show installed versions
-echo ""
-echo "π Installed formatter versions:"
-pip list | grep -E "(black|isort|flake8|pre-commit|mypy)" || echo "No formatters found"
-
-# Create activation script
-cat > activate_formatters.sh << 'EOF'
-#!/bin/bash
-# Quick activation script for formatter virtual environment
-echo "π Activating formatter virtual environment..."
-source .venv-formatters/bin/activate
-echo "β
Formatter environment activated!"
-echo "π‘ Available commands:"
-echo " - black . --line-length 88 --target-version py38"
-echo " - isort . --profile black"
-echo " - flake8 ."
-echo " - pre-commit run --all-files"
-EOF
-
-chmod +x activate_formatters.sh
-
-# Create a simple format script that uses this venv
-cat > format_with_venv.sh << 'EOF'
-#!/bin/bash
-# Format script using the dedicated virtual environment
-set -e
-
-echo "π§ Running Python formatters..."
-
-# Check if virtual environment exists
-if [ ! -d ".venv-formatters" ]; then
- echo "β Virtual environment not found. Run './setup_venv_formatters.sh' first"
- exit 1
-fi
-
-# Activate virtual environment
-source .venv-formatters/bin/activate
-
-# Run formatters
-echo "π¨ Formatting with Black..."
-black . --line-length 88 --target-version py38
-
-echo "π Sorting imports with isort..."
-isort . --profile black
-
-echo "π Running flake8 linting..."
-flake8 . --statistics
-
-echo "β
Code formatting complete!"
-EOF
-
-chmod +x format_with_venv.sh
-
-echo ""
-echo "β
Virtual environment setup complete!"
-echo ""
-echo "π Created files:"
-echo " - $VENV_NAME/ (virtual environment)"
-echo " - activate_formatters.sh (quick activation)"
-echo " - format_with_venv.sh (formatting script)"
-echo ""
-echo "π Usage:"
-echo " 1. To activate: source activate_formatters.sh"
-echo " 2. To format code: ./format_with_venv.sh"
-echo " 3. To install pre-commit hooks: source activate_formatters.sh && pre-commit install"
-echo ""
-echo "π‘ This environment uses the latest publicly available versions of formatting tools"
diff --git a/scripts/tests.hss b/scripts/tests.hss
deleted file mode 100644
index 8285e4a..0000000
--- a/scripts/tests.hss
+++ /dev/null
@@ -1,36 +0,0 @@
-# Script Tests Configuration
-# Tests for running script nodes indefinitely
-name: "Script Tests"
-description: "Tests for long-running script nodes"
-strategy: "script"
-
-tests:
- - name: "API Server Script"
- path: "../autonomy/src/api_server.py"
- args: []
- timeout: 2
- infinite_loop: true
- description: "API server script test"
- strategy: "ScriptTestStrategy"
- enabled: true
- test_type: "script"
-
- - name: "Controllers Script"
- path: "../autonomy/src/controllers.py"
- args: []
- timeout: 2
- infinite_loop: true
- description: "Controllers node script test"
- strategy: "ScriptTestStrategy"
- enabled: true
- test_type: "script"
-
- - name: "CV Publishers Script"
- path: "../autonomy/src/cv_publishers.py"
- args: []
- timeout: 2
- infinite_loop: true
- description: "Computer vision publishers script test"
- strategy: "ScriptTestStrategy"
- enabled: true
- test_type: "script"
diff --git a/src/computer_vision/.python-version b/src/computer_vision/.python-version
new file mode 100644
index 0000000..e4fba21
--- /dev/null
+++ b/src/computer_vision/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/src/computer_vision/README.md b/src/computer_vision/README.md
new file mode 100644
index 0000000..80bc204
--- /dev/null
+++ b/src/computer_vision/README.md
@@ -0,0 +1,4 @@
+Python Library for running the Computer Vision Algorithms.
+
+
+How to compile this proyect
\ No newline at end of file
diff --git a/autonomy/scripts/cv/color_filters.py b/src/computer_vision/examples/color_filters.py
similarity index 100%
rename from autonomy/scripts/cv/color_filters.py
rename to src/computer_vision/examples/color_filters.py
diff --git a/autonomy/scripts/cv/distance.py b/src/computer_vision/examples/distance.py
similarity index 100%
rename from autonomy/scripts/cv/distance.py
rename to src/computer_vision/examples/distance.py
diff --git a/autonomy/scripts/cv/object_detector.py b/src/computer_vision/examples/object_detector.py
similarity index 100%
rename from autonomy/scripts/cv/object_detector.py
rename to src/computer_vision/examples/object_detector.py
diff --git a/autonomy/scripts/cv/orbs.py b/src/computer_vision/examples/orbs.py
similarity index 100%
rename from autonomy/scripts/cv/orbs.py
rename to src/computer_vision/examples/orbs.py
diff --git a/autonomy/scripts/cv/plane_detection.py b/src/computer_vision/examples/plane_detection.py
similarity index 100%
rename from autonomy/scripts/cv/plane_detection.py
rename to src/computer_vision/examples/plane_detection.py
diff --git a/autonomy/scripts/cv/slam_visuation.py b/src/computer_vision/examples/slam_visuation.py
similarity index 100%
rename from autonomy/scripts/cv/slam_visuation.py
rename to src/computer_vision/examples/slam_visuation.py
diff --git a/src/computer_vision/examples/yolo11n.pt b/src/computer_vision/examples/yolo11n.pt
new file mode 100644
index 0000000000000000000000000000000000000000..45b273b46165267f2d4c90df94468b547f43cf67
GIT binary patch
literal 5613764
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