Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 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
fa2b8a2
chore(localization): add steering slop parameter to BicycleModel and …
ShayManor May 17, 2026
6e6b138
chore(mpc): add mppi elite fraction parameter for improved control.
ShayManor May 17, 2026
337d3af
chore(pathfinder): add GPS data handling for improved state tracking.
ShayManor May 17, 2026
aaa3d3b
chore(docker-compose): enable device mappings for hardware integration.
ShayManor May 17, 2026
e4a004c
Merge branch 'main' into smanor/mpc-fix
ShayManor May 17, 2026
a961472
chore(mpc_residual): add safety parameters for outlier handling and r…
ShayManor May 17, 2026
52be1a9
feat(pathfinder): add wheel speed and EKF snapshot handling features.
ShayManor May 17, 2026
f1f9e22
feat(e_comms): log incoming message arbitration ID for debugging.
ShayManor May 17, 2026
46135ba
feat(imu): refine calibration logic and add rotation matrix handling.
ShayManor May 17, 2026
ffb9221
feat(localization): add handling for reverse motion in yaw measurement.
ShayManor May 17, 2026
2f6da81
fix(logging): comment out logging of arbitration ID in e_comms_node.
ShayManor May 17, 2026
bb57a38
fix(e_comms): multiply steering degree by 3 in convert method.
ShayManor May 17, 2026
56785cc
fix(e_comms): increment cmd_count for each received message.
ShayManor May 17, 2026
84e0666
fix(e_comms): adjust steering conversion factor from 3 to 2.
ShayManor May 17, 2026
a56940a
fix(e_comms): set steering conversion factor from 2 to 1.
ShayManor May 17, 2026
f33391a
fix(e_comms): set steering conversion factor from 2 to 1.
ShayManor May 17, 2026
a886b50
fix(e_comms): double the steering degree conversion factor in convert…
ShayManor May 17, 2026
abb9c85
fix(e_comms): triple the steering degree conversion factor in convert…
ShayManor May 17, 2026
93a8d6d
fix(actuators): adjust max and min steering limits to ±60 degrees.
ShayManor May 17, 2026
a42d6a7
fix(mpc): add initial sync flag to manage kart's starting position.
ShayManor May 17, 2026
0e6050e
fix(localization): include angular velocity in EKF prediction method.
ShayManor May 17, 2026
cd316dc
fix(localization): add angular velocity to EKF prediction method.
ShayManor May 17, 2026
d114e1c
fix(params): update pure pursuit latency from 0.3 to 0.4 seconds.
ShayManor May 17, 2026
161d1eb
fix(e_comms): adjust steering conversion factor from 3 to 1.
ShayManor May 17, 2026
a69f5e2
fix(e_comms): update steering conversion factor from 1 to 2.
ShayManor May 17, 2026
de45e43
feat(residual): add residual GBM functionality, caching, and train + …
ShayManor May 18, 2026
7f13313
feat(residual): add buffer, save, batching
ShayManor May 18, 2026
53ed14a
feat(requirements): add joblib to requirements.txt for dependencies.
ShayManor May 18, 2026
0ddf8dc
feat(workflows): add step to install Python requirements in CI pipeline.
ShayManor May 18, 2026
f632bd7
feat(checkpoint): implement Checkpoint and CheckpointRing classes.
ShayManor May 18, 2026
f4663d9
feat(requirements): add joblib and scikit-learn to requirements.txt
ShayManor May 18, 2026
3b9a375
feat(tests): add _default_params function for MasterNode initialization.
ShayManor May 18, 2026
28c9b35
feat(tests): add tests for RLS prediction clipping and selector behav…
ShayManor May 18, 2026
78d1ef8
feat(tests): remove obsolete tests for load_bags
ShayManor May 18, 2026
7f34634
feat(pathfinder): implement per-planner steering gain adjustments
ShayManor May 19, 2026
85ed244
feat(sim): push sim
ShayManor May 19, 2026
f83aa68
feat(sim): add compare-residuals subcommand for mode comparison.
ShayManor May 19, 2026
fead3f6
fix(sbatch): update REPO_ROOT resolution and add error handling.
ShayManor May 19, 2026
7959e51
fix(runner): add datasim_use_mlp parameter and update related logic.
ShayManor May 19, 2026
13de0ca
feat(runner): add trajectory hash and residual stats for diagnostics.
ShayManor May 19, 2026
0cdaa7e
feat(render): increase max_frames to 5000 and add realtime_factor par…
ShayManor May 19, 2026
adfaf95
feat(sbatch): add target_speed_mps parameter for kart speed control.
ShayManor May 19, 2026
7b816f4
feat(bicycle_params): remove obsolete bicycle_params.json configuration.
ShayManor May 19, 2026
b997977
feat(sbatch_scripts): activate virtual environment in all sbatch files.
ShayManor May 19, 2026
d93fcec
feat(params): add kart physics parameters and conversion methods.
ShayManor May 19, 2026
2d5ed97
feat(imu): update acceleration threshold and modify speed subscription.
ShayManor May 19, 2026
cbf9427
feat(imu): increase gyro motion threshold to allow more jitter.
ShayManor May 19, 2026
e3d16e9
feat(imu): increase accel motion threshold for improved calibration c…
ShayManor May 19, 2026
424f2b6
feat(localization): add reverse flip deadband parameter for EKF stabi…
ShayManor May 19, 2026
f5a77ef
feat(localization): add reverse flip deadband parameter for EKF stabi…
ShayManor May 19, 2026
88e9db0
feat(pathfinder): change planner from pure_pursuit to mpc and adjust …
ShayManor May 19, 2026
af5aa48
feat(pathfinder): change planner from pure_pursuit to mpc and adjust …
ShayManor May 19, 2026
1ea6b2e
Update to main
DanielProano May 20, 2026
0d443cc
Update
DanielProano May 20, 2026
029e0b5
Update
DanielProano May 20, 2026
b93fbda
Update
DanielProano May 20, 2026
5c43fed
update
DanielProano May 20, 2026
fa89de7
update
DanielProano May 20, 2026
0c50760
update
DanielProano May 20, 2026
0d7f00f
Update
DanielProano May 20, 2026
1977cf9
Update
DanielProano May 20, 2026
b158e8a
Update
DanielProano May 20, 2026
08d0d25
Update
DanielProano May 20, 2026
82c51f1
Update
DanielProano May 20, 2026
297d30a
Update
DanielProano May 20, 2026
60bb89a
update
DanielProano May 20, 2026
d693c37
Update
DanielProano May 20, 2026
13e2c8d
Update
DanielProano May 20, 2026
69f8e1a
Update
DanielProano May 20, 2026
bc0bbab
Update
DanielProano May 20, 2026
cb080e4
Update
DanielProano May 20, 2026
56212ff
Update
DanielProano May 20, 2026
3a8346c
Updated
DanielProano May 20, 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
e8fb891
Merge branch 'millan/simple_distribute' into dproano/opencv_pathfinder
DanielProano 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
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ jobs:
cp -a "${GITHUB_WORKSPACE}/ws-src/requirements.txt" /ws/requirements.txt || true
ls /ws

- name: Install Python requirements
shell: bash
run: |
set -exo pipefail
pip install --no-cache-dir -r /ws/requirements.txt

- name: Restore colcon build cache
id: colcon-cache
uses: actions/cache@v4
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: 9 additions & 10 deletions compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +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"
- TARGET_DEVICE=${TARGET_DEVICE:-jetson}
- ROS_NETWORK_INTERFACE=${ROS_NETWORK_INTERFACE:-enP8p1s0}
# devices:
# - "/dev/ttyACM0:/dev/ttyACM0"
# - "/dev/ttyTHS1:/dev/ttyTHS1" # GPS
# - "/dev/i2c-7:/dev/i2c-7"
# - "/dev/spidev12.0:/dev/spidev12.0" # SPI bus 12, CS0
# - "/dev/video0:/dev/video0"
# - "/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
13 changes: 10 additions & 3 deletions docs/README/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +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 .
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ python-can
flask
flask-cors
pyserial
smbus2==0.6.1
smbus2==0.6.1
joblib
scikit-learn
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
2 changes: 2 additions & 0 deletions sim/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Offline data-driven kart simulator. See
docs/superpowers/specs/2026-05-18-data-driven-sim-design.md."""
166 changes: 166 additions & 0 deletions sim/clamp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Off-distribution Mahalanobis clamp for the residual MLP."""
from __future__ import annotations

import json
from dataclasses import dataclass

import numpy as np


@dataclass
class Clamp:
mean: np.ndarray
cov_inv: np.ndarray
d_warn: float
d_max: float

def _alpha_from_d(self, d: float) -> float:
if d <= self.d_warn:
return 1.0
if d >= self.d_max:
return 0.0
return 1.0 - (d - self.d_warn) / (self.d_max - self.d_warn)

def alpha(self, feat_normalised: np.ndarray) -> float:
delta = feat_normalised - self.mean
d = float(np.sqrt(delta @ self.cov_inv @ delta))
return self._alpha_from_d(d)

def save(self, path: str) -> None:
payload = {
"mean": self.mean.tolist(),
"cov_inv": self.cov_inv.tolist(),
"d_warn": self.d_warn,
"d_max": self.d_max,
}
with open(path, "w") as f:
json.dump(payload, f, indent=2)

@classmethod
def load(cls, path: str) -> "Clamp":
with open(path) as f:
d = json.load(f)
return cls(
mean=np.asarray(d["mean"], dtype=np.float64),
cov_inv=np.asarray(d["cov_inv"], dtype=np.float64),
d_warn=float(d["d_warn"]),
d_max=float(d["d_max"]),
)


def fit_clamp(feats_normalised: np.ndarray, d_warn_pct=95, d_max_pct=99.5) -> Clamp:
mean = feats_normalised.mean(0)
cov = np.cov(feats_normalised.T)
cov += np.eye(cov.shape[0]) * 1e-6
cov_inv = np.linalg.inv(cov)
delta = feats_normalised - mean
d = np.sqrt(np.einsum("ni,ij,nj->n", delta, cov_inv, delta))
d_warn = float(np.percentile(d, d_warn_pct))
d_max = float(np.percentile(d, d_max_pct))
return Clamp(mean=mean, cov_inv=cov_inv, d_warn=d_warn, d_max=d_max)


def fit_and_write(
train_npz_paths,
params_path: str,
mlp_path: str,
norm_path: str,
out_clamp_path: str,
out_noise_post_path: str,
out_residual_emp_post: str,
) -> None:
import json as _json
import torch
from sim.identify_bicycle import BicycleParams, predict_derivatives
from sim.load_bags import AlignedBag
from sim.noise_model import NoiseModel, estimate_noise
from sim.residual_mlp import (
FEATURE_DIM,
Normaliser,
ResidualMLP,
build_features,
)

with open(params_path) as f:
params = BicycleParams(**_json.load(f)["params"])
with open(norm_path) as fn:
norm = Normaliser.from_json(_json.load(fn))
net = ResidualMLP()
net.load_state_dict(torch.load(mlp_path, map_location="cpu"))
net.eval()

feats_all, dv_res_post, psi_res_post = [], [], []
ct_all, cs_all, v_all = [], [], []
is_straight_all, is_steady_all = [], []
for path in train_npz_paths:
bag = AlignedBag.from_npz(path)
mask = bag.autonomous & (bag.v > 0.5)
if mask.sum() < 100:
continue
dt = float(np.median(np.diff(bag.t)))
dv_m, psi_m = predict_derivatives(params, bag.v, bag.cmd_throttle, bag.cmd_steer, dt)
feats = build_features(bag.v, bag.psi_dot, bag.cmd_throttle, bag.cmd_steer,
bag.cmd_throttle_hist, bag.cmd_steer_hist)
feats_n = norm.apply(feats)
with torch.no_grad():
res = net(torch.from_numpy(feats_n.astype(np.float32))).numpy()
dv_res_post.append((bag.dv_dt - dv_m - res[:, 0])[mask])
psi_res_post.append((bag.psi_dot - psi_m - res[:, 1])[mask])
feats_all.append(feats_n[mask])
ct_all.append(bag.cmd_throttle[mask])
cs_all.append(bag.cmd_steer[mask])
v_all.append(bag.v[mask])
is_straight_all.append(np.abs(bag.cmd_steer[mask]) < 2.0)
d_throttle = np.abs(np.diff(bag.cmd_throttle[mask], prepend=bag.cmd_throttle[mask][0]))
is_steady_all.append(d_throttle < 2.0)

feats_concat = np.concatenate(feats_all).astype(np.float32)
clamp = fit_clamp(feats_concat)
clamp.save(out_clamp_path)
print(f"wrote {out_clamp_path}: d_warn={clamp.d_warn:.2f}, d_max={clamp.d_max:.2f}")

dv_res = np.concatenate(dv_res_post)
psi_res = np.concatenate(psi_res_post)
nm = estimate_noise(
dv_res, psi_res,
np.concatenate(ct_all), np.concatenate(cs_all),
np.concatenate(is_straight_all), np.concatenate(is_steady_all),
)
nm.save(out_noise_post_path)
print(f"wrote {out_noise_post_path}: sigma_v={nm.sigma_v:.3f}, sigma_psidot={nm.sigma_psidot:.3f}")

v_arr = np.concatenate(v_all)
cs = np.concatenate(cs_all)
v_edges = np.linspace(0.0, 15.0, 6)
cs_edges = np.linspace(0.0, 30.0, 4)
v_bin = np.clip(np.digitize(v_arr, v_edges) - 1, 0, v_edges.size - 2)
cs_bin = np.clip(np.digitize(np.abs(cs), cs_edges) - 1, 0, cs_edges.size - 2)
np.savez_compressed(out_residual_emp_post,
dv_res=dv_res, psi_res=psi_res,
v_bin=v_bin, cs_bin=cs_bin,
v_edges=v_edges, cs_edges=cs_edges)
print(f"wrote {out_residual_emp_post}: {dv_res.size} samples")


def main():
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--train", nargs="+", required=True)
ap.add_argument("--params", required=True)
ap.add_argument("--mlp", required=True)
ap.add_argument("--norm", required=True)
ap.add_argument("--out-clamp", required=True)
ap.add_argument("--out-noise-post", required=True)
ap.add_argument("--out-residual-emp-post", required=True)
args = ap.parse_args()
fit_and_write(args.train, args.params, args.mlp, args.norm,
args.out_clamp, args.out_noise_post, args.out_residual_emp_post)


if __name__ == "__main__":
import os
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if REPO not in sys.path:
sys.path.insert(0, REPO)
main()
Loading
Loading