A ROS 1 (Noetic) catkin workspace for simulating Adaptive Cruise Control (ACC) in a Dockerized environment. Developed for Vanderbilt University's CS 3892 Autonomous Vehicles & Traffic course.
Recorded driving data from a real vehicle is replayed as a lead car, and one or more simulated ego vehicles follow it using an ACC controller -- all running as ROS nodes inside a Docker container.
To cite the usage of this repository, use the following:
Kate Sanborn, Daniel B. Work, Jonathan Sprinkle. "Single Vehicle to Traffic Scale: a Model-Based Workflow." in 2026 IEEE International Conference on Intelligent Transportation Systems (ITSC), (in press) 2026.
or using bibtex:
@inproceedings{sanborn2026single,
author = {Sanborn, Kate and Work, Daniel B. and Sprinkle, Jonathan},
title = {Single Vehicle to Traffic Scale: A Model-Based Workflow},
booktitle = {2026 IEEE International Conference on Intelligent Transportation Systems (ITSC)},
year = {2026},
note = {in press}
}
Every later step is the same on all platforms once you have a Linux-style
terminal with docker and git. Getting there is the one place where macOS
and Windows diverge:
| macOS | Windows | Linux | |
|---|---|---|---|
| Terminal to use | Terminal (zsh) | Ubuntu in WSL (not PowerShell, cmd, or Git Bash) | your usual terminal |
| Docker | Docker Desktop | Docker Desktop + WSL integration for Ubuntu | Docker Engine + compose plugin |
| git | Xcode Command Line Tools | comes with Ubuntu | your package manager |
| Clone the repo into | anywhere, e.g. ~/cs3892 |
WSL home, ~ (not /mnt/c/...) |
anywhere |
| Shell startup file | ~/.zshrc |
~/.bashrc |
~/.bashrc (usually) |
- Install Docker Desktop (pick Apple Silicon or Intel to match Apple menu → About This Mac). Start it and wait until it reports the engine is running.
- Open Terminal and run
git --version. If git is missing, macOS offers to install the Command Line Tools; accept.
The helper scripts (./scripts/*.sh) are Linux shell scripts. If you type them
into PowerShell or cmd, Windows pops up a dialog asking which program
should open the .sh file. (Windows Terminal and the VS Code terminal open
PowerShell by default.) Do every step in this README in an Ubuntu (WSL)
terminal instead:
- Install WSL with Ubuntu once, from an administrator PowerShell, then reboot:
The first time Ubuntu opens, it asks you to create a Linux username and password.
wsl --install -d Ubuntu
- Install Docker Desktop (keep the default "Use WSL 2" option). In Docker Desktop, open Settings → Resources → WSL integration and turn it on for Ubuntu.
- Make Ubuntu the default, so
wsldoesn't drop you into Docker's own internal distribution. In PowerShell:wsl -l -v # lists Ubuntu, docker-desktop, ... wsl --set-default Ubuntu
docker-desktop(anddocker-desktop-dataon older versions) belong to Docker Desktop. They are normal and must stay installed, but never work in them: they have nogitand odd prompts like/tmp/docker-desktop-root/...#. - Open a WSL shell: the Ubuntu app from the Start menu, the Ubuntu tab in
Windows Terminal, or type
wslin PowerShell. You're in the right place when the prompt looks likeyou@PC:~$, notPS C:\Users\you>or...docker-desktop...#. - Clone inside WSL's home folder (
cd ~first, in Step 1), not under/mnt/c/.... Docker mounts are much faster there, and Git for Windows won't convert the scripts to Windows line endings. You can still open the folder from Windows Explorer (see below), or in VS Code withcode ..
Moving files between Windows and WSL. The two see each other's files at these paths:
| From | To reach | Use this path |
|---|---|---|
| Windows (Explorer, MATLAB, ...) | your WSL home | \\wsl.localhost\Ubuntu\home\<linux-user> (older Windows 10: \\wsl$\Ubuntu\...), or Linux → Ubuntu in Explorer's sidebar |
| Ubuntu (WSL) | your Windows files | /mnt/c/Users/<windows-user>/ (e.g. .../Downloads) |
- From inside WSL,
explorer.exe .opens the current folder in Windows Explorer. Drag files in and out there. - Copy a download into the repo from the Ubuntu terminal:
cp /mnt/c/Users/<windows-user>/Downloads/hwilexample.bag ~/rossim/mytest.bag - Your Linux and Windows usernames can differ.
ls /mnt/c/Userslists the Windows ones. If the distribution is calledUbuntu-22.04or similar,wsl -lin PowerShell shows the exact name to use in the path. - Use the files in place from Windows (e.g. open a recorded
profacc_*.bagin MATLAB via the\\wsl.localhost\...path) or copy them out; just keep the repo itself inside WSL.
Seeing /usr/bin/env: 'bash\r': No such file or directory? The scripts got
Windows (CRLF) line endings, usually from cloning with Git for Windows. In the
Ubuntu terminal, from inside rossim/:
sed -i 's/\r$//' scripts/*.sh scripts/rosempty # quick fix: strip the CRs
git config --global core.autocrlf false # keep WSL's git from convertingFor a clean long-term setup, re-clone inside WSL's home folder as in item 5
and copy over your mytest.bag and any files of your own.
Install Docker Engine and the
Docker Compose plugin, then let your user run Docker without sudo:
sudo usermod -aG docker $USER # then log out and back inIn the terminal from above (on Windows: the Ubuntu terminal):
git --version
docker --version
docker compose versionAll three should print a version. Cannot connect to the Docker daemon
means Docker Desktop isn't running (or, on Windows, WSL integration is off for Ubuntu).
In your terminal from Step 0 (Windows: the Ubuntu terminal, after cd ~):
git clone https://github.com/jmscslgroup/rossim rossim
cd rossimThis gives you the workspace skeleton: launch files, setup scripts, and this README. The ROS packages themselves are cloned in Step 4.
docker pull sprinkjm/rosemptyRun the image and confirm you get a shell prompt:
docker run --rm -it sprinkjm/rosempty /bin/bashYou should see a root prompt like root@<container_id>:/#. Inside it, verify ROS is available:
roscore &
sleep 2 && rostopic listYou should see /rosout and /rosout_agg listed. If so, ROS is working. Type exit to leave the container.
This step confirms that your host files are visible inside the container. We use
Docker Compose (included with Docker Desktop
and modern Docker Engine) so you don't have to type long docker run --mount ...
commands -- the image and mount are defined once in compose.yaml.
From the rossim/ directory on your host:
docker compose run --rm ros lsYou should see README.md, scripts/, src/, etc. -- the contents of your
rossim/ directory, which is mounted at /ros/catkin_ws inside the container.
If you see an empty listing or an error, double-check that you are in the
rossim/ directory and that Docker has permission to access it.
docker compose only works from inside rossim/. For everyday ROS chores
-- inspecting a bag with rosbag info, repairing a bag, poking around with
ROS tools -- it is handy to have one short command that starts the container
on whatever folder you are currently in. That is what
scripts/rosempty does:
rosempty # interactive ROS shell in the current folder
rosempty rosbag info mytest.bag # run a single command, then exitThe current folder is mounted at /ros/catkin_ws inside the container (the
same place compose.yaml puts it), so files you see on the host are the files
the container sees, and anything the container writes (a repaired bag, a new
recording) shows up back in that folder. The container is removed when you
exit.
Where to run it: normally, run
rosemptyfrom yourrossim/clone (or the folder holding the bag files you're working with). If that folder has been built withcatkin_make,devel/setup.bashis sourced for you, so your packages work too. It runs fine from any folder, but only that folder (and its subfolders) will be visible inside the container -- e.g. you can't reach../other.bag.cdto the right place first.
Put the script on your PATH. The easiest way is a symlink into a personal
bin folder, so it stays up to date when you git pull. From the rossim/
directory:
mkdir -p ~/.local/bin
ln -sf "$PWD/scripts/rosempty" ~/.local/bin/rosemptyThen make sure ~/.local/bin is on your PATH. Add this line to your shell's
startup file -- ~/.zshrc if you use zsh (the macOS default) or
~/.bashrc if you use bash (most Linux / WSL setups):
export PATH="$HOME/.local/bin:$PATH"Not sure which shell you have? Run echo $SHELL. Open a new terminal (or
run source ~/.zshrc / source ~/.bashrc) and check that it works:
which rosempty # should print .../.local/bin/rosempty
cd ~/Downloads # or any folder
rosempty ls # lists the folder's contents, as seen from inside the containerAlternatives: put scripts/ on your PATH, or use an alias
Instead of the symlink, you can add the whole scripts/ folder to your PATH
in ~/.zshrc / ~/.bashrc (replace the path with where you cloned rossim):
export PATH="$HOME/path/to/rossim/scripts:$PATH"Or define an alias:
alias rosempty="$HOME/path/to/rossim/scripts/rosempty"If you skip installing entirely, rossim/scripts/rosempty still works when
called by its full path from any folder.
# Summary of a bag: duration, topics, message counts
rosempty rosbag info mytest.bag
# Repair a bag whose recording was interrupted (Ctrl+C, crash, power loss).
# "rosbag info" will report the bag is unindexed. reindex fixes it in place
# and keeps the original as broken.orig.bag.
rosempty rosbag reindex broken.bag
# Migrate a bag recorded with older message definitions
rosempty rosbag fix old.bag fixed.bag
# Pull one topic out of a bag into CSV
rosempty bash -c "rostopic echo -b mytest.bag -p /leadcar/car/state/vel_x > vel_x.csv"Run rosempty --help for all options. A few details:
- Commands containing shell features (
>,|,&&,*) need to be wrapped inbash -c "...", as in the CSV example above. Otherwise your host shell handles them, not the container. rosempty -p 8888 ...also publishes a port to the host (for example, to view a dashboard).- Each
rosemptycall starts its own fresh container with noroscorerunning. Use it for standalone tools likerosbag. To talk to a running simulation, use./scripts/join.shinstead (see Step 7). - Linux and Windows (WSL): files created by the container may be owned by
root. Fix them withsudo chown $USER <file>if needed. (macOS Docker Desktop maps ownership to your user automatically.) - Windows: run it from the Ubuntu (WSL) terminal (see Step 0), not PowerShell or cmd.
A setup script clones all the packages needed for the profacc ACC simulation:
./scripts/setup_profacc.shThis clones the following into src/:
| Package | Description |
|---|---|
| profacc | Time-headway ACC controller (Simulink-generated) |
| subtractor | Computes difference of two Float64 topics |
| odometer | Integrates velocity to produce position |
| carsimplesimulink | Simple point-mass vehicle model |
| carcomplexsimulink | Higher-fidelity vehicle model |
Download the example bag file from Brightspace (hwilexample.bag) and place it in the rossim/ root directory as mytest.bag:
cp /path/to/hwilexample.bag mytest.bagOn Windows, run this in the Ubuntu terminal; your Windows Downloads folder is
/mnt/c/Users/<windows-user>/Downloads/ (see Step 0).
This file contains a recorded velocity trace from a real vehicle and is replayed as the lead car in simulation.
Open an interactive shell in the container (the workspace is already mounted):
docker compose run --rm rosInside the container, build:
catkin_makeIf the build succeeds you will see a summary like:
[100%] Built target profacc
[100%] Built target carsimplesimulink
...
Then source the workspace:
source devel/setup.bashIf you are already in the container shell from Step 6 (with the workspace sourced), launch directly:
roslaunch profacc profaccDocker.launchOr, from your host, use the one-command shortcut that builds, sources, and launches in a fresh container:
./scripts/run.sh # default: profaccDocker.launch
./scripts/run.sh profaccDocker_complex.launch # any launch file in src/profacc/launch/You will see ROS start up several nodes. The bag file replays the lead car velocity trace, the ACC controller computes acceleration commands, and the ego car model responds. All topics are recorded to a new bag file (profacc_*.bag) in the workspace root.
When the bag file finishes playing (or you want to stop early), press Ctrl+C.
The terminal will show log output from the various nodes. To see the simulation in action, open a second terminal on your host and join the running container (the workspace is sourced for you automatically):
./scripts/join.shjoin.sh connects to the container named rossim (the default that
run.sh creates), so there is no need to look up the container name or type a
long docker exec command. If you started the simulation with a custom name
(./scripts/run.sh --name egocarB ...), pass the same name: ./scripts/join.sh egocarB.
Then try:
# List all active topics
rostopic list
# Watch the ego car velocity in real time
rostopic echo /egocar/car/state/vel_x
# Watch the ACC acceleration commands
rostopic echo /egocar/cmd_accelEach run.sh invocation starts its own container with its own roscore, so you
can run two independent simulations side by side -- just give them different
names. In two host terminals:
./scripts/run.sh --name carA --port 8888 profaccDocker.launch
./scripts/run.sh --name carB --port 8889 profaccDocker_complex.launchGive each one a different --port so their dashboards don't collide on the host
(carA on localhost:8888, carB on localhost:8889). Join either one from
another terminal:
./scripts/join.sh carA
./scripts/join.sh carBBecause each container has its own ROS master, the two simulations do not see each other's topics -- they are fully isolated.
A small web dashboard shows the cars in the simulation updating in real time in
your browser. It runs as a ROS node and needs no extra software -- just
rospy and the Python standard library (details in
dashboard/README.md). It has two views:
- Overhead -- an ego-centric top-down view of the selected car, the car ahead, and any cars behind, placed by their odometry.
- Data -- value tiles for the selected car (speed, commanded acceleration, lead distance, relative velocity, odometer).
The dashboard discovers the cars automatically (any namespace publishing
car/state/vel_x), so leadcar, egocar, egocar1, ... all appear as
buttons at the top -- click to swap which car you are watching. This works
for a single car or a whole platoon.
With a simulation running (./scripts/run.sh), start the dashboard from a second
host terminal:
./scripts/dashboard.shThen open the URL it prints (normally http://localhost:8888) in your browser. Press Ctrl+C to stop the dashboard; the simulation keeps running.
Port 8888 already taken (e.g. Jupyter)? The host port is chosen when
run.sh starts the container, not by dashboard.sh. If 8888 is busy,
run.sh automatically uses the next free port (8889, 8890, ...) and prints it,
and dashboard.sh prints the matching URL. To choose the port yourself,
restart the simulation with:
./scripts/run.sh --port 8890When you are logged into the car over SSH with no web access, use the text version instead. It does the same car discovery and prints a refreshing table (plus a front-to-back ordering) right in the terminal -- no browser, no ports:
./scripts/dashboard.sh --text # the sim, in the container
# or, directly on the real vehicle where ROS is sourced:
python3 dashboard/dashboard_tui.py --mode liveThere are two modes:
| Mode | What it shows |
|---|---|
sim (default) |
the fields available in simulation |
live |
the same fields plus extra real-vehicle fields, for running on the actual car |
./scripts/dashboard.sh --mode liveThe live set is a superset that grows as real-car topics come online -- see
dashboard/dashboard.py (the LIVE_EXTRA_FIELDS list) and dashboard/README.md.
Each Docker launch file sets up this pipeline:
Recorded bag file Lead car Ego car
(real driving data) (odometer) (ACC + vehicle model)
| | |
/leadcar/car/state/vel_x odom_x profacc <-- subtractor (rel_vel)
\ | \--- subtractor (lead_dist)
\ v
carsimplesimulink --> vel_x, odom_x
rosbag playreplays the recorded velocity trace as the lead carodometerintegrates lead car velocity into positionsubtractornodes compute relative velocity and distance between lead and egoprofacccomputes an acceleration command using the ACC control lawcarsimplesimulinksimulates the ego car's response to that commandrosbag recordcaptures everything to a new bag file
The controller implements a time-headway ACC law:
cmd_accel = alpha * (lead_dist - tau * vel_x) + lambda * rel_vel
| Parameter | Default | Description |
|---|---|---|
alpha |
1.1 | Proportional gain |
tau |
2.0 | Time headway (seconds) |
lambda |
0.1 | Relative velocity gain |
Output is saturated to [-3.0, 1.5] m/s^2.
| Direction | Topic | Type | Description |
|---|---|---|---|
| Subscribes | car/state/vel_x |
std_msgs/Float64 |
Ego car velocity |
| Subscribes | lead_dist |
std_msgs/Float64 |
Distance to lead car |
| Subscribes | rel_vel |
std_msgs/Float64 |
Relative velocity (lead - ego) |
| Publishes | cmd_accel |
std_msgs/Float64 |
Acceleration command |
Parameters can be changed at runtime:
rosparam set /egocar/profacc_node/tau 3.0All Docker launch files are in src/profacc/launch/:
| Launch file | Ego cars | Vehicle model | Description |
|---|---|---|---|
profaccDocker.launch |
1 | simple | Basic scenario. Replays mytest.bag from t=100s. Lead starts at x=20m. |
profaccDocker_test1.launch |
1 | simple | Ego starts closer (x0=15m) and faster (v0=2.5 m/s). |
profaccDocker_test2.launch |
1 | complex | Adds 10m extra buffer to lead_dist. |
profaccDocker_complex.launch |
1 | complex | Same as basic but with the complex vehicle model. |
After a simulation, the recorded .bag file is written to the rossim/ directory. Open MATLAB, run the provided testResult script, and select the bag file when prompted.