diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 36a8b6e3..198b41ad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,7 @@ "workspaceFolder": "/ws", "containerEnv": { "ROS_DOMAIN_ID": "42", - "ROS_LOCALHOST_ONLY": "1", + "ROS_LOCALHOST_ONLY": "0", "CYCLONEDDS_URI": "file:///ws/docker/cyclonedds.xml" }, "customizations": { diff --git a/.gitignore b/.gitignore index 2734fac9..a054c13f 100644 --- a/.gitignore +++ b/.gitignore @@ -176,4 +176,6 @@ cython_debug/ # Ros build ignored install log +logs +kart_logs build diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index e2530e7c..fc04eb93 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -11,7 +11,8 @@ services: environment: - ROS_DISTRO=humble - ROS_DOMAIN_ID=42 - - ROS_LOCALHOST_ONLY=1 + - ROS_LOCALHOST_ONLY=0 + - TARGET_DEVICE=jetson # set target device for build (ex: jetson, rubik) - CYCLONEDDS_URI=file:///ws/docker/cyclonedds.xml ports: - "8000:8000" diff --git a/docker/Dockerfile b/docker/Dockerfile index eca954a4..5b5141f7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,6 +20,7 @@ RUN apt-get -o Acquire::http::Pipeline-Depth="0" \ ros-${ROS_DISTRO}-rclpy ros-${ROS_DISTRO}-ros2bag \ ros-${ROS_DISTRO}-tf2-tools ros-${ROS_DISTRO}-image-transport \ ros-${ROS_DISTRO}-cv-bridge \ + openssh-client \ git curl nano \ socat rtklib i2c-tools && \ rm -rf /var/lib/apt/lists/* diff --git a/scripts/start.bash b/scripts/start.bash index af0722e6..9ca04e74 100755 --- a/scripts/start.bash +++ b/scripts/start.bash @@ -7,24 +7,25 @@ source /opt/ros/humble/setup.bash colcon build pkill -f master_api || true -# Lat/long are approx west lafayette, not important to be highly accurate mkdir -p /ws/logs -str2str -in ntrip://shayman1:shayman1@108.59.49.226:9000/MSM4_VRS -out tcpsvr://:9195 -b 1 -p 40.4376975222 -86.9444409756 200 >> /ws/logs/str2str.log 2>&1 & - -# Logs supervisor - crash tolerant -( - while true; do - RUN_NAME="run_$(date -u +%Y%m%d_%H%M%S)" - ros2 bag record -a \ - --storage mcap \ - --storage-preset-profile zstd_fast \ - --max-bag-duration 60 \ - --exclude '/camera/.*' \ - --output "/ws/logs/$RUN_NAME" \ - >> /ws/logs/recorder.log 2>&1 - echo "[$(date -u +%FT%TZ)] recorder exited, restarting in 1s" >> /ws/logs/recorder.log - sleep 1 - done -) & +if [ "$TARGET_DEVICE" == "jetson" ]; then + # Lat/long are approx west lafayette, not important to be highly accurate + str2str -in ntrip://shayman1:shayman1@108.59.49.226:9000/MSM4_VRS -out tcpsvr://:9195 -b 1 -p 40.4376975222 -86.9444409756 200 >> /ws/logs/str2str.log 2>&1 & +elif [ "$TARGET_DEVICE" == "rubik" ]; then + ( + while true; do + RUN_NAME="run_$(date -u +%Y%m%d_%H%M%S)" + ros2 bag record -a \ + --storage mcap \ + --storage-preset-profile zstd_fast \ + --max-bag-duration 60 \ + --exclude '/camera/.*' \ + --output "/ws/logs/$RUN_NAME" \ + >> /ws/logs/recorder.log 2>&1 + echo "[$(date -u +%FT%TZ)] recorder exited, restarting in 1s" >> /ws/logs/recorder.log + sleep 1 + done + ) & +fi sleep infinity \ No newline at end of file diff --git a/src/autonomous_kart/autonomous_kart/launch/bringup_pi.launch.py b/src/autonomous_kart/autonomous_kart/launch/bringup_all.launch.py similarity index 100% rename from src/autonomous_kart/autonomous_kart/launch/bringup_pi.launch.py rename to src/autonomous_kart/autonomous_kart/launch/bringup_all.launch.py diff --git a/src/autonomous_kart/autonomous_kart/launch/bringup_jetson.launch.py b/src/autonomous_kart/autonomous_kart/launch/bringup_jetson.launch.py new file mode 100644 index 00000000..b63d91cd --- /dev/null +++ b/src/autonomous_kart/autonomous_kart/launch/bringup_jetson.launch.py @@ -0,0 +1,80 @@ +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument, GroupAction +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node, SetParameter, SetParametersFromFile +from ament_index_python.packages import get_package_share_directory +import os + + +def generate_launch_description(): + pkg_share = get_package_share_directory("autonomous_kart") + + camera_yaml = os.path.join(pkg_share, "params", "camera.yaml") + gps_yaml = os.path.join(pkg_share, "params", "gps.yaml") + safety_yaml = os.path.join(pkg_share, "params", "safety.yaml") + system_yaml = os.path.join(pkg_share, "params", "system.yaml") + pathfinder_yaml = os.path.join(pkg_share, "params", "pathfinder.yaml") + localization_yaml = os.path.join(pkg_share, "params", "localization.yaml") + imu_yaml = os.path.join(pkg_share, "params", "imu.yaml") + e_comms_yaml = os.path.join(pkg_share, "params", "e_comms.yaml") + + sim_mode = LaunchConfiguration("simulation_mode") + + return LaunchDescription( + [ + DeclareLaunchArgument("simulation_mode", default_value="false"), + GroupAction( + [ + SetParametersFromFile(system_yaml), + SetParameter(name="simulation_mode", value=sim_mode), + Node( + package="autonomous_kart", + executable="camera_node", + name="camera_node", + parameters=[camera_yaml], + ), + Node( + package="autonomous_kart", + executable="pathfinder_node", + name="pathfinder_node", + parameters=[pathfinder_yaml, safety_yaml], + ), + Node( + package="autonomous_kart", + executable="opencv_pathfinder_node", + name="opencv_pathfinder_node", + parameters=[gps_yaml], + ), + Node( + package="autonomous_kart", + executable="master_api", + name="master_api", + ), + Node( + package="autonomous_kart", + executable="localization_node", + name="localization_node", + parameters=[pathfinder_yaml, localization_yaml], + ), + Node( + package="autonomous_kart", + executable="e_comms_node", + name="e_comms_node", + parameters=[e_comms_yaml], + ), + Node( + package="autonomous_kart", + executable="gps_node", + name="gps_node", + parameters=[gps_yaml], + ), + Node( + package="autonomous_kart", + executable="imu_node", + name="imu_node", + parameters=[imu_yaml], + ), + ] + ), + ] + ) diff --git a/src/autonomous_kart/autonomous_kart/launch/bringup_rubik.launch.py b/src/autonomous_kart/autonomous_kart/launch/bringup_rubik.launch.py new file mode 100644 index 00000000..3fb824b8 --- /dev/null +++ b/src/autonomous_kart/autonomous_kart/launch/bringup_rubik.launch.py @@ -0,0 +1,37 @@ +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument, GroupAction, ExecuteProcess +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node, SetParameter, SetParametersFromFile +from ament_index_python.packages import get_package_share_directory +from datetime import datetime +import os + + +def generate_launch_description(): + pkg_share = get_package_share_directory("autonomous_kart") + system_yaml = os.path.join(pkg_share, "params", "system.yaml") + sim_mode = LaunchConfiguration("simulation_mode") + + return LaunchDescription( + [ + DeclareLaunchArgument("simulation_mode", default_value="false"), + GroupAction( + [ + SetParametersFromFile(system_yaml), + SetParameter(name="simulation_mode", value=sim_mode), + + Node( + package="autonomous_kart", + executable="metrics_node", + name="metrics_node", + ), + Node( + package="autonomous_kart", + executable="master_api", + name="master_api", + parameters=[system_yaml], + ), + ] + ), + ] + ) diff --git a/src/autonomous_kart/autonomous_kart/nodes/master/master_api.py b/src/autonomous_kart/autonomous_kart/nodes/master/master_api.py index 102d5e0e..473572f4 100644 --- a/src/autonomous_kart/autonomous_kart/nodes/master/master_api.py +++ b/src/autonomous_kart/autonomous_kart/nodes/master/master_api.py @@ -191,6 +191,53 @@ def lines_endpoint(): "static": static_xy, "dynamic": master_node.get_dynamic_line(), }) + +@app.route("/jetson/hotswap", methods=["POST"]) +def jetson_hotswap(): + if not master_node: + return jsonify({"error": "not initialized"}), 500 + + master_node.hotswap_jetson() + + return jsonify({ + "message": "jetson hotswap started" + }) + + +@app.route("/jetson/restart", methods=["POST"]) +def jetson_restart(): + if not master_node: + return jsonify({"error": "not initialized"}), 500 + + master_node.restart_jetson() + + return jsonify({ + "message": "jetson restart started" + }) + + +@app.route("/jetson/update", methods=["POST"]) +def jetson_update(): + if not master_node: + return jsonify({"error": "not initialized"}), 500 + + master_node.update_jetson() + + return jsonify({ + "message": "jetson update started" + }) + + +@app.route("/jetson/rebuild", methods=["POST"]) +def jetson_rebuild(): + if not master_node: + return jsonify({"error": "not initialized"}), 500 + + master_node.rebuild_jetson() + + return jsonify({ + "message": "jetson rebuild started" + }) def start(node: MasterNode) -> None: diff --git a/src/autonomous_kart/autonomous_kart/nodes/master/master_node.py b/src/autonomous_kart/autonomous_kart/nodes/master/master_node.py index 3a1bec37..700c0b7a 100644 --- a/src/autonomous_kart/autonomous_kart/nodes/master/master_node.py +++ b/src/autonomous_kart/autonomous_kart/nodes/master/master_node.py @@ -1,6 +1,7 @@ import json import threading import time +import subprocess from enum import Enum import math @@ -35,6 +36,8 @@ def __init__(self): self.yaw_cal_tick_dt = self.get_parameter("yaw_cal_tick_dt").value self.yaw_cal_accel_floor = self.get_parameter("yaw_cal_accel_floor").value self.yaw_cal_min_samples = self.get_parameter("yaw_cal_min_samples").value + self.jetson_ip = self.get_parameter("jetson_ip").value + self.jetson_user = self.get_parameter("jetson_username").value assert self.state in [s.value for s in STATES] @@ -331,6 +334,52 @@ def get_gps_status(self): with self._lock: return dict(self.gps_status_data) + # Jetson remote management + + def _ssh_fire(self, remote_command: str, log_msg: str): + """Shared helper: open a non-blocking SSH subprocess to the Jetson.""" + ssh_command = [ + "ssh", "-t", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + f"{self.jetson_user}@{self.jetson_ip}", + remote_command, + ] + + self.logger.info(log_msg) + subprocess.Popen(ssh_command) + + def hotswap_jetson(self): + """Git-pull and rebuild inside the *running* container, then relaunch bringup_rubik. + The container is never stopped — fastest way to pick up new code mid-session.""" + self._ssh_fire( + "bash ~/run_remote.sh --hotswap", + "Hotswapping: pulling latest code and relaunching inside running container...", + ) + + def restart_jetson(self): + """Stop the container, rebuild the ROS workspace, and relaunch bringup. + No git pull, no Docker image pull — fastest way to bounce the stack.""" + self._ssh_fire( + "bash ~/run_remote.sh --restart", + "Restarting Jetson container (no code update)...", + ) + + def update_jetson(self): + """Git-pull the latest code, then restart. + Use this before a session to pick up new commits without a full image rebuild.""" + self._ssh_fire( + "bash ~/run_remote.sh --update", + "Pulling latest code and restarting Jetson...", + ) + + def rebuild_jetson(self): + """Pull the latest Docker image, git-pull code, then restart. + Use this after a Docker image has been pushed (e.g. new deps or base OS change).""" + self._ssh_fire( + "bash ~/run_remote.sh --rebuild", + "Pulling latest Docker image + code and rebuilding Jetson...", + ) def main(args=None): rclpy.init(args=args) diff --git a/src/autonomous_kart/autonomous_kart/params/system.yaml b/src/autonomous_kart/autonomous_kart/params/system.yaml index e0a7ca61..d7b29ae2 100644 --- a/src/autonomous_kart/autonomous_kart/params/system.yaml +++ b/src/autonomous_kart/autonomous_kart/params/system.yaml @@ -5,3 +5,5 @@ system_state: "IDLE" # "STOPPED", "IDLE", "MANUAL", "AUTONOMOUS" system_frequency: 60 # Speed of hot loop (Hertz) line_path: "/ws/data/racing_line/line14.csv" + jetson_ip: "evc-jetson.tail73145c.ts.net" + jetson_username: "evc" \ No newline at end of file