Skip to content

Docker Container Setup

We use the dustynv/ros docker setup use on the Jetson Nano, running jetpack 4 on Ubuntu 18.04.

The link to the docker container is here.

The link to the github for other packages is here.

To install this, simply run:

docker pull dustynv/ros:humble-ros-base-l4t-r36.3.0

Dockerfile

We have a slightly customized dockerfile such that we pull in our required directories:

#!/usr/bin/env bash
# pass-through commands to 'docker run' with some defaults
# https://docs.docker.com/engine/reference/commandline/run/
ROOT="$(dirname "$(readlink -f "$0")")"

# Function to clean up background processes
cleanup() {
    if [[ ${#BG_PIDS[@]} -gt 0 ]]; then
        echo "Terminating background processes..."
        for pid in "${BG_PIDS[@]}"; do
            kill "$pid"  # Terminate each background process
            wait "$pid" 2>/dev/null  # Wait for the process to finish
        done
    fi
	sudo modprobe -r v4l2loopback
}

# Trap signals like INT (Ctrl+C) or TERM to invoke the cleanup function
trap cleanup INT TERM

# Initialize variables (default for arguments)
csi_to_webcam_conversion=false
capture_res="1640x1232@30"
output_res="1280x720@30"
capture_width="1640"
capture_height="1232"
capture_fps="30"
output_width="1280"
output_height="720"
output_fps="30"

# Loop through arguments
for arg in "$@"; do
	# Check for the --csi2webcam option
    if [[ "$arg" == "--csi2webcam" ]]; then
        csi_to_webcam_conversion=true
        continue  # Move to next argument
    fi

    # Check for --csi-capture-res
    if [[ "$arg" =~ --csi-capture-res= ]]; then
        csi_capture_res="${arg#*=}"
        # Extract width, height, and fps from capture_res
        if [[ $csi_capture_res =~ ([0-9]+)x([0-9]+)@([0-9]+) ]]; then
            capture_width="${BASH_REMATCH[1]}"
            capture_height="${BASH_REMATCH[2]}"
            capture_fps="${BASH_REMATCH[3]}"
        else
            echo "Invalid format for --csi-capture-res. Expected format: widthxheight@fps"
            exit 1
        fi
        continue
    fi

    # Check for --csi-output-res
    if [[ "$arg" =~ --csi-output-res= ]]; then
        csi_output_res="${arg#*=}"
        # Extract width, height, and fps from output_res
        if [[ $csi_output_res =~ ([0-9]+)x([0-9]+)@([0-9]+) ]]; then
            output_width="${BASH_REMATCH[1]}"
            output_height="${BASH_REMATCH[2]}"
            output_fps="${BASH_REMATCH[3]}"
        else
            echo "Invalid format for --csi-output-res. Expected format: widthxheight@fps"
            exit 1
        fi
        continue
    fi
done

# check for V4L2 devices
V4L2_DEVICES=""

if [[ "$csi_to_webcam_conversion" == true ]]; then

    echo "CSI to Webcam conversion enabled."
    echo "CSI Capture resolution: ${capture_width}x${capture_height}@${capture_fps}"
    echo "CSI Output resolution : ${output_width}x${output_height}@${output_fps}"

	# Check if v4l2loopback-dkms is installed
	if dpkg -l | grep -q v4l2loopback-dkms; then
		echo "( v4l2loopback-dkms is installed. )"
	else
		echo "[Error] v4l2loopback-dkms is not installed."
		echo " "
		echo "Perform the following command to first install v4l2loopback moddule."
		echo " "
		echo "    sudo apt update && sudo apt install v4l2loopback-dkms"
		echo " "
		exit 1
	fi

	# Check if v4l2-ctl is installed
	if command -v v4l2-ctl &> /dev/null
	then
		echo "(v4l2-ctl is installed)"
	else
		echo "[Error] v4l2-ctl is not installed"
		echo " "
		echo "Perform the following command to first install v4l-utils package."
		echo " "
		echo "    sudo apt install v4l-utils"
		echo " "
		exit 1
	fi

	# Store /dev/video index number for each CSI camera found
	csi_indexes=()
	# Store /dev/video* device name for each CSI camera found
	csi_devices=()

	# Loop through all matching /dev/video* devices
	for device in /dev/video*; do
		# Use v4l2-ctl to check if the device supports RG10 (CSI camera format)
		if v4l2-ctl -d "$device" --list-formats-ext 2>/dev/null | grep -q "RG10"; then
			echo "$device is a CSI camera (RG10 format)"
			# Store the device name in array if CSI camera
			csi_devices+=("$device")
			# Extract the device index number and add to the csi_devices array
			dev_index=$(echo "$device" | grep -o '[0-9]\+')
			csi_indexes+=("$dev_index")
		else
			echo "$device is not a CSI camera (likely a webcam)"
			V4L2_DEVICES="$V4L2_DEVICES --device $device "
		fi
	done

	# Load the v4l2loopback module to create as many devices as CSI cameras found in the prior step
	sudo modprobe v4l2loopback devices=${#csi_indexes[@]} exclusive_caps=1 card_label="Cam1,Cam2"

	# Get all new /dev/video devices created by v4l2loopback
	new_devices=($(v4l2-ctl --list-devices | grep -A 1 "v4l2loopback" | grep '/dev/video' | awk '{print $1}'))
	echo "###### new_devices: ${new_devices[@]}"

	# add the created v4l2loopback devices
	if [[ -n "${new_devices[@]}" ]]; then
		for converted_device in ${new_devices[@]}; do
			V4L2_DEVICES="$V4L2_DEVICES --device $converted_device "
		done
	else
		echo "No v4l2loopback devices found."
	fi

	# Save the current DISPLAY variable
	ORIGINAL_DISPLAY=$DISPLAY

	# Start background processes for each CSI camera found
	i=0
	for csi_index in "${csi_indexes[@]}"; do
		echo "Starting background process for CSI camera device number: $csi_index"

		echo "CSI Capture resolution: ${capture_width}x${capture_height}@${capture_fps}"
		echo "CSI Output resolution : ${output_width}x${output_height}@${output_fps}"

		# Unset the DISPLAY env variable because, apparently, some GStreamer components might try to use this display
		# for video rendering or processing, which can conflict with other GStreamer elements or hardware device

		# Temporarily unset DISPLAY for the GStreamer command
		unset DISPLAY

		echo "gst-launch-1.0 -v nvarguscamerasrc sensor-id=${csi_index} \
					! 'video/x-raw(memory:NVMM), format=NV12, width=${capture_width}, height=${capture_height}, framerate=${capture_fps}/1' \
					! nvvidconv \
					! 'video/x-raw, width=${output_width}, height=${output_height}, framerate=${output_fps}/1', format=I420 \
					! nvjpegenc \
					! multipartmux \
					! multipartdemux single-stream=1 \
					! \"image/jpeg, width=${output_width}, height=${output_height}, parsed=(boolean)true, colorimetry=(string)2:4:7:1, framerate=(fraction)${output_fps}/1, sof-marker=(int)0\" \
					! v4l2sink device=${new_devices[$i]} > /dev/null 2>&1 &"
		gst-launch-1.0 -v nvarguscamerasrc sensor-id=${csi_index} \
					! "video/x-raw(memory:NVMM), format=NV12, width=${capture_width}, height=${capture_height}, framerate=${capture_fps}/1" \
					! nvvidconv \
					! "video/x-raw, width=${output_width}, height=${output_height}, framerate=${output_fps}/1", format=I420 \
					! nvjpegenc \
					! multipartmux \
					! multipartdemux single-stream=1 \
					! "image/jpeg, width=${output_width}, height=${output_height}, parsed=(boolean)true, colorimetry=(string)2:4:7:1, framerate=(fraction)${output_fps}/1, sof-marker=(int)0" \
					! v4l2sink device=${new_devices[$i]} > /dev/null 2>&1 &

		# Store the PID of the background process if you want to manage it later
		BG_PIDS+=($!)
		echo "BG_PIDS: ${BG_PIDS[@]}"

		((i++))
	done

	# Restore the DISPLAY env variable
	export DISPLAY=$ORIGINAL_DISPLAY

else
	# Loop through all matching /dev/video* devices
	for device in /dev/video*; do
	    if [ -e "$device" ]; then  # Check if the device file exists
        	V4L2_DEVICES="$V4L2_DEVICES --device $device "
		fi
    done
fi

echo "V4L2_DEVICES: $V4L2_DEVICES"

if [ -n "$csi_indexes" ]; then
    echo "CSI_INDEXES:  $csi_indexes"
fi

# check for I2C devices
I2C_DEVICES=""

for i in {0..9}
do
	if [ -a "/dev/i2c-$i" ]; then
		I2C_DEVICES="$I2C_DEVICES --device /dev/i2c-$i "
	fi
done

# check for ttyACM devices
ACM_DEVICES=""

# Loop through all matching /dev/ttyACM* devices
for dev in /dev/ttyACM*; do
    if [ -e "$dev" ]; then  # Check if the device file exists
        ACM_DEVICES="$ACM_DEVICES --device $dev "
    fi
done

# check for display
DISPLAY_DEVICE=""

if [ -n "$DISPLAY" ]; then
	echo "### DISPLAY environmental variable is already set: \"$DISPLAY\""
	# give docker root user X11 permissions
	xhost +si:localuser:root || sudo xhost +si:localuser:root

	# enable SSH X11 forwarding inside container (https://stackoverflow.com/q/48235040)
	XAUTH=/tmp/.docker.xauth
	xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge -
	chmod 777 $XAUTH

	DISPLAY_DEVICE="-e DISPLAY=$DISPLAY -v /tmp/.X11-unix/:/tmp/.X11-unix -v $XAUTH:$XAUTH -e XAUTHORITY=$XAUTH"
fi

# check for jtop
JTOP_SOCKET=""
JTOP_SOCKET_FILE="/run/jtop.sock"

if [ -S "$JTOP_SOCKET_FILE" ]; then
	JTOP_SOCKET="-v /run/jtop.sock:/run/jtop.sock"
fi

# PulseAudio arguments
PULSE_AUDIO_ARGS=""

if [ -d "${XDG_RUNTIME_DIR}/pulse" ]; then
	PULSE_AUDIO_ARGS="-e PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native  -v ${XDG_RUNTIME_DIR}/pulse:${XDG_RUNTIME_DIR}/pulse"
fi

# extra flags
EXTRA_FLAGS=""

if [ -n "$HUGGINGFACE_TOKEN" ]; then
	EXTRA_FLAGS="$EXTRA_FLAGS --env HUGGINGFACE_TOKEN=$HUGGINGFACE_TOKEN"
fi

# additional permission optional run arguments
OPTIONAL_PERMISSION_ARGS=""

if [ "$USE_OPTIONAL_PERMISSION_ARGS" = "true" ]; then
	OPTIONAL_PERMISSION_ARGS="-v /lib/modules:/lib/modules --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor=unconfined"
fi

# check if sudo is needed
if [ $(id -u) -eq 0 ] || id -nG "$USER" | grep -qw "docker"; then
	SUDO=""
else
	SUDO="sudo"
fi

# Initialize an empty array for filtered arguments
filtered_args=()

# Loop through all provided arguments
for arg in "$@"; do
    if [[ "$arg" != "--csi2webcam" && "$arg" != --csi-capture-res=* && "$arg" != --csi-output-res=* ]]; then
        filtered_args+=("$arg")  # Add to the new array if not the argument to remove
    fi

    if [[ "$arg" = "--name" || "$arg" = --name* ]]; then
        HAS_CONTAINER_NAME=1
    fi
done

if [ -z "$HAS_CONTAINER_NAME" ]; then
    # Generate a unique container name so we can wait for it to exit and cleanup the bg processes after
    BUILD_DATE_TIME=$(date +%Y%m%d_%H%M%S)
    #CONTAINER_IMAGE_NAME=$(basename "${filtered_args[0]}")  # unfortunately this doesn't work in the general case, and you can't easily parse the container image from the command-line
    #SANITIZED_CONTAINER_IMAGE_NAME=$(echo "$CONTAINER_IMAGE_NAME" | sed 's/[^a-zA-Z0-9_.-]/_/g')
    CONTAINER_NAME="jetson_container_${BUILD_DATE_TIME}"
    CONTAINER_NAME_FLAGS="--name $CONTAINER_NAME"
fi

# run the container
ARCH=$(uname -i)

if [ $ARCH = "aarch64" ]; then

	# this file shows what Jetson board is running
	# /proc or /sys files aren't mountable into docker
	cat /proc/device-tree/model > /tmp/nv_jetson_model

    # https://stackoverflow.com/a/19226038
	( set -x ;

	$SUDO docker run --runtime nvidia -it --rm --network host \
		--shm-size=8g \
		--volume /tmp/argus_socket:/tmp/argus_socket \
		--volume /etc/enctune.conf:/etc/enctune.conf \
		--volume /etc/nv_tegra_release:/etc/nv_tegra_release \
		--volume /tmp/nv_jetson_model:/tmp/nv_jetson_model \
		--volume /var/run/dbus:/var/run/dbus \
		--volume /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket \
		--volume /var/run/docker.sock:/var/run/docker.sock \
		--volume $ROOT/data:/data \
		--volume ~/cusub2.1:/cusub2.1 \
		--volume ~/test/cusub2.1:/test/cusub2.1 \
		--volume ~/.local/lib/python3.6/site-packages/serial:/usr/local/lib/python3.6/dist-packages/serial \
		--volume ~/.local/lib/python3.6/site-packages/maestro:/usr/local/lib/python3.6/dis-packages/maestro \
		--volume ~/.local/lib/python3.6/site-packages/crcmod:/usr/local/lib/python3.6/dist-packages/crcmod \
		--volume /dev:/dev \
		--volume /etc/udev/rules.d:/etc/udev/rules.d \
		--device /dev/snd \
		--device /dev/bus/usb \
		--expose 8080 \
		-v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro \
		--device /dev/snd \
		$PULSE_AUDIO_ARGS \
		--device /dev/bus/usb \
		$OPTIONAL_PERMISSION_ARGS $DATA_VOLUME $DISPLAY_DEVICE $V4L2_DEVICES $I2C_DEVICES $ACM_DEVICES $JTOP_SOCKET $EXTRA_FLAGS \
		$CONTAINER_NAME_FLAGS \
		"${filtered_args[@]}"
	)

elif [ $ARCH = "x86_64" ]; then

	( set -x ;

	$SUDO docker run --gpus all -it --rm --network=host \
		--shm-size=8g \
		--ulimit memlock=-1 \
		--ulimit stack=67108864 \
		--env NVIDIA_DRIVER_CAPABILITIES=all \
		--volume $ROOT/data:/data \
		-v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro \
		$OPTIONAL_ARGS $DATA_VOLUME $DISPLAY_DEVICE $V4L2_DEVICES $I2C_DEVICES $ACM_DEVICES $JTOP_SOCKET $EXTRA_FLAGS \
		$CONTAINER_NAME_FLAGS \
		"${filtered_args[@]}"
	)
fi

if [[ "$csi_to_webcam_conversion" == true ]]; then

	# Wait for the Docker container to finish (if it exits)
	docker wait "$CONTAINER_NAME"

	# When Docker container exits, cleanup will be called
	cleanup

fi