Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
215b044
separate launch scripts
shb-png May 11, 2026
13cbbb4
ignore logs
shb-png May 14, 2026
50476aa
move rosbags to exclusively rubik bringup
shb-png May 14, 2026
8926fdd
ROS_LOCALHOST_ONLY = 0
shb-png May 14, 2026
604fa22
idk
shb-png May 14, 2026
d5d31a9
idk
shb-png May 14, 2026
0883be8
remote jetson launch
shb-png May 14, 2026
5703db4
master api endpoints for jetson launch
shb-png May 14, 2026
1d46025
jetson tailscape addr
shb-png May 15, 2026
ddf8bf3
Add master_api node to bringup_rubik launch file
shb-png May 15, 2026
0d3ef44
pls work
shb-png May 15, 2026
0cc6916
ssh
shb-png May 15, 2026
9bc8d0d
jsonify it!!
shb-png May 15, 2026
d770149
restart and update jetston endpoint
shb-png May 15, 2026
ed2cf21
remove --symlink-install from colcon build
shb-png May 15, 2026
41813af
copilot comments
shb-png May 15, 2026
d37d013
auto accept host key
shb-png May 15, 2026
a3b3f5a
Move camera node to Rubik
LelsersLasers May 17, 2026
c962417
Don't use metrics node
LelsersLasers May 17, 2026
e322999
Don't start master api on both
LelsersLasers May 17, 2026
6207908
Docker compose service per target
LelsersLasers May 17, 2026
093309c
Update devcontainer jsons
LelsersLasers May 17, 2026
aca85d0
CycloneDDS config to work over the network
LelsersLasers May 17, 2026
8f4de00
Rename pi to rubik
LelsersLasers May 17, 2026
ddfdfd0
Hand merge the base compose
LelsersLasers May 17, 2026
ef68586
Update quickstart
LelsersLasers May 17, 2026
0ba8be0
Update docs
LelsersLasers May 17, 2026
404d2b0
camera update
LelsersLasers May 17, 2026
6aa8764
sudo
LelsersLasers May 17, 2026
e40588f
info debug print
LelsersLasers May 17, 2026
8d07380
Use system level enviorment variables to revert back to one service
LelsersLasers May 17, 2026
2368bf3
update docs
LelsersLasers May 17, 2026
101c498
now can revert devcontainer json
LelsersLasers May 17, 2026
37f16a3
master node/api on rubik
LelsersLasers May 20, 2026
7470ed1
Merge remote-tracking branch 'origin/smanor/mpc-fix' into millan/simp…
LelsersLasers May 20, 2026
fb824eb
missing e_comms params
LelsersLasers May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,6 @@ cython_debug/
# Ros build ignored
install
log
logs
kart_logs
build
19 changes: 10 additions & 9 deletions compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,27 @@ services:
dev:
image: ghcr.io/evc-purdue/ros2-humble-dev:main
container_name: ros2-dev
hostname: ros2-dev
hostname: "${TARGET_DEVICE:-jetson}-dev"
volumes:
- ../:/ws
- ros_cache:/root/.cache
- /data/kart_logs:/ws/logs # logs location
environment:
- ROS_DISTRO=humble
- ROS_DOMAIN_ID=42
- ROS_LOCALHOST_ONLY=1
- ROS_LOCALHOST_ONLY=0
- CYCLONEDDS_URI=file:///ws/docker/cyclonedds.xml
ports:
- "8000:8000"
devices:
- "/dev/ttyACM0:/dev/ttyACM0"
- "/dev/ttyTHS1:/dev/ttyTHS1" # GPS
- "/dev/i2c-7:/dev/i2c-7"
- TARGET_DEVICE=${TARGET_DEVICE:-jetson}
- ROS_NETWORK_INTERFACE=${ROS_NETWORK_INTERFACE:-enP8p1s0}
# devices:
# - "/dev/ttyACM0:/dev/ttyACM0" # canable
# - "/dev/ttyTHS1:/dev/ttyTHS1" # GPS
# - "/dev/i2c-7:/dev/i2c-7" # IMU
# - "/dev/video0:/dev/video0" # camera
stdin_open: true
tty: true
restart: always
command: bash /ws/scripts/start.bash
network_mode: host
volumes:
ros_cache:
ros_cache:
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand Down
8 changes: 8 additions & 0 deletions docker/cyclonedds.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://cdds.io/config https://raw.githubusercontent.com/eclipse-cyclonedds/cyclonedds/master/etc/cyclonedds.xsd">
<Domain Id="any">
<General>
<AllowMulticast>true</AllowMulticast>

<Interfaces>
<NetworkInterface name="${ROS_NETWORK_INTERFACE}"/>
</Interfaces>
</General>

<Discovery>
<ParticipantIndex>auto</ParticipantIndex>
<MaxAutoParticipantIndex>32</MaxAutoParticipantIndex>
Expand Down
5 changes: 3 additions & 2 deletions docs/README/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ sudo docker compose -f compose/docker-compose.yml up -d dev
```
3) Exec into container
```bash
docker exec -it <container_name> bash
docker exec -it ros2-dev bash
```
4) Setup ROS
```bash
source /opt/ros/humble/setup.bash
colcon build
source install/setup.bash
ros2 launch autonomous_kart bringup_pi.launch.py
ros2 launch autonomous_kart bringup_${TARGET_DEVICE:-jetson}.launch.py
```


```bash
ros2 bag record -s mcap -o /ws/bags/$(date +%Y%m%d_%H%M%S) /mpc/status /mpc/residual_mode /mpc/residual_revert /cmd_drive /odom /gps /imu /imu/calibration_status /e_comms/kart_speed_m_per_s /e_comms/throttle_pwm /e_comms/steering_pwm /e_comms/adcb_state /e_comms/rc_mode /system_state /track_angles /manual_commands /pathfinder_params
```

docker cp ros2-dev:/ws/bags .
37 changes: 19 additions & 18 deletions scripts/start.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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")

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")
imu_yaml = os.path.join(pkg_share, "params", "imu.yaml")
e_comms_yaml = os.path.join(pkg_share, "params", "e_comms.yaml")
actuators_yaml = os.path.join(pkg_share, "params", "actuators.yaml")
localization_yaml = os.path.join(pkg_share, "params", "localization.yaml")
opencv_yaml = os.path.join(pkg_share, "params", "opencv_pathfinder.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="pathfinder_node",
name="pathfinder_node",
parameters=[pathfinder_yaml, safety_yaml],
),
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, actuators_yaml, pathfinder_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],
),
]
),
]
)
40 changes: 40 additions & 0 deletions src/autonomous_kart/autonomous_kart/launch/bringup_rubik.launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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")

camera_yaml = os.path.join(pkg_share, "params", "camera.yaml")
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="camera_node",
name="camera_node",
parameters=[camera_yaml],
),
Node(
package="autonomous_kart",
executable="master_api",
name="master_api",
),
]
),
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __init__(self):
raise RuntimeError(f"Failed to open video: {video_path}")

else:
self.cap = cv2.VideoCapture(0)
self.cap = cv2.VideoCapture('/dev/video0')
self.video_fps = self.fps

if not self.cap.isOpened():
Expand Down Expand Up @@ -84,6 +84,7 @@ def timer_callback(self):
self.logger.warning("No frame available")

if publish_frame is not None:
self.logger.info(f"Publishing frame {self.frame_counter}")
self.image_pub.publish(publish_frame)
self.frame_counter += 1

Expand Down
47 changes: 47 additions & 0 deletions src/autonomous_kart/autonomous_kart/nodes/master/master_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,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"
})


@app.route("/residual/status", methods=["GET"])
Expand Down
48 changes: 47 additions & 1 deletion src/autonomous_kart/autonomous_kart/nodes/master/master_node.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import threading
import time
import subprocess
from enum import Enum

import rclpy
Expand Down Expand Up @@ -450,6 +451,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 get_residual_log(self, limit: int = 50) -> list[dict]:
with self._lock:
return list(self._residual_log[: max(0, limit)])
Expand All @@ -463,7 +510,6 @@ def get_residual_status(self) -> dict | None:
def trigger_residual_revert(self) -> None:
self.residual_revert_publisher.publish(Empty())


def main(args=None):
rclpy.init(args=args)

Expand Down
2 changes: 2 additions & 0 deletions src/autonomous_kart/autonomous_kart/params/system.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/line6.csv"
jetson_ip: "evc-jetson.tail73145c.ts.net"
jetson_username: "evc"
Loading