From 4c37670940536cfd7cb3e6d014880282432182e1 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 20:28:18 -0700 Subject: [PATCH 01/12] fix(docker): run the ubuntu user's shell as bash useradd was creating the ubuntu user without -s, leaving its login shell as /bin/sh (dash). tmux then spawned every workshop pane as dash, which does not read ~/.bashrc (so the ROS 2 environment was never sourced and `ros2` was "not found") and has no `source` builtin (`source ...` failed with "source: not found"). Set the login shell to /bin/bash so tmux panes inherit a shell that sources the workshop environment. --- docker/Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7c7bffa..605668a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,9 +15,15 @@ RUN --mount=type=bind,source=./docker/scripts/install_deps.sh,target=/root/scrip # DRI device nodes for GPU rendering (Gazebo, EGL, etc.). When the host's # group GIDs differ from the image's, docker_run.sh additionally passes the # host GIDs via --group-add. +# +# `-s /bin/bash` is required: without it useradd gives the user /bin/sh +# (dash) as login shell. tmux then spawns every workshop pane as dash, +# which does not read ~/.bashrc (so the ROS 2 environment is never sourced +# and `ros2` is "not found") and has no `source` builtin (`source ...` +# fails with "source: not found"). bash fixes both. ENV USER=ubuntu RUN groupadd -g 1000 ${USER} && \ - useradd -m -u 1000 -g ${USER} ${USER} && \ + useradd -m -u 1000 -g ${USER} -s /bin/bash ${USER} && \ (getent group video >/dev/null || groupadd -r video) && \ (getent group render >/dev/null || groupadd -r render) && \ usermod -a -G video,render ${USER} From a7390b0a4a4df249a2ca6cb228060db6d5893b92 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 20:28:41 -0700 Subject: [PATCH 02/12] workshop-tmux: copy selections to the system clipboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The welcome banner told attendees to Shift+drag to copy text out of a pane, which makes a native terminal selection — and that selection is wiped the instant the pane or the animated status bar repaints. Enable `set-clipboard on` so a plain mouse drag (tmux copy-mode, which freezes the pane) forwards the copy to the host clipboard over OSC 52, and update the welcome banner to match. --- docker/scripts/workshop-tmux.sh | 15 +++++++++++---- docker/scripts/workshop-welcome | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 40c6047..688b20d 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -51,11 +51,18 @@ tmux set -ga terminal-overrides ",xterm-256color:Tc" # tell tmux the outer ter tmux set -g pane-border-lines heavy # thicker borders on tmux 3.2+ # Mouse mode ON so attendees can click panes to focus them and scroll -# back through long-running command output with the wheel. To copy text -# while mouse mode is on, HOLD SHIFT while dragging — every common -# terminal (GNOME, Konsole, Alacritty, Kitty, iTerm2, Terminal.app) -# treats Shift+drag as a native terminal selection that bypasses tmux. +# back through long-running command output with the wheel. +# +# To copy text, just drag with the mouse. A drag puts tmux into copy-mode, +# which FREEZES that pane for the duration of the selection — so the +# continuous gazebo / px4 output can no longer scroll your selection away +# (the old advice was Shift+drag for a native terminal selection, but that +# is wiped the instant the pane or the animated status bar repaints). +# `set-clipboard on` then forwards whatever tmux copies to the host's +# system clipboard via the OSC 52 escape sequence, so a plain drag is all +# you need — no Shift, and the selection survives screen updates. tmux set -g mouse on +tmux set -g set-clipboard on # --- Dracula-inspired palette (synthwave-y, dev-friendly) --- # bg #282a36 bg-dark #13111c diff --git a/docker/scripts/workshop-welcome b/docker/scripts/workshop-welcome index fcc78ad..3858816 100755 --- a/docker/scripts/workshop-welcome +++ b/docker/scripts/workshop-welcome @@ -39,6 +39,6 @@ line(f" {DIM}│{RESET} {GREEN}Alt+z{RESET} zoom current pane line(f" {DIM}│{RESET} {CYAN}Ctrl-b 0{RESET} / {CYAN}Ctrl-b 1{RESET} same as Alt+1 / Alt+2 (tmux defaults)") line(f" {DIM}│{RESET} {CYAN}Ctrl-b \"{RESET} / {CYAN}Ctrl-b %{RESET} split current pane horizontally / vertically") line(f" {DIM}│{RESET} {CYAN}Ctrl-b d{RESET} detach (sim keeps running); reattach with `workshop-tmux`") -line(f" {DIM}│{RESET} {YELLOW}Shift+drag{RESET} copy text out of any pane (bypasses tmux mouse capture)") +line(f" {DIM}│{RESET} {YELLOW}mouse drag{RESET} select text — copies straight to your system clipboard") line(f" {DIM}└─────────────────────────────────────────────────────────────────────────────{RESET}") line("") From 44c4fdb76fd5321abe06ffdb47964a1444a24731 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 20:28:48 -0700 Subject: [PATCH 03/12] workshop-tmux: 6-pane layout with two ROS 2 example panes The 5-pane sim window had a single example pane, but the teleop and precision-land exercises each run two ROS 2 nodes at once (e.g. aruco_tracker plus precision_land), leaving the second node nowhere to go. Rebuild the sim window as a 3x2 grid with two example panes (node 1 and node 2). Also seed each pane by running `workshop-hint` as the pane's command instead of injecting it with send-keys: now that panes run bash and source the ROS 2 workspace, the slower startup raced the send-keys and dropped the hint. --- docker/scripts/workshop-tmux.sh | 102 +++++++++++++++++--------------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 688b20d..606935a 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -4,18 +4,20 @@ # # Creates one named session ('ossna') with two windows: # -# sim - a 5-pane layout where each of the four long-running -# foreground processes (gazebo / px4 / common.launch.py / -# example launch) gets its own pane, plus a tall pane on -# the right for QGroundControl. +# sim - a 6-pane grid, one pane per long-running foreground +# process. Four are workshop infrastructure (gazebo / px4 / +# common.launch.py / QGroundControl); the other two are +# example panes, because the teleop and precision-land +# exercises each run TWO ROS 2 example nodes at the same +# time (e.g. aruco_tracker + precision_land). # -# ┌─────────────┬─────────────┐ -# │ 0: gazebo │ 1: px4 │ -# ├─────────────┤ │ -# │ 3: common │ 2: qgc │ -# ├─────────────┤ │ -# │ 4: example │ │ -# └─────────────┴─────────────┘ +# ┌──────────────┬──────────────┐ +# │ gazebo │ px4 │ +# ├──────────────┼──────────────┤ +# │ common │ qgc │ +# ├──────────────┼──────────────┤ +# │ ros2 node 1 │ ros2 node 2 │ +# └──────────────┴──────────────┘ # # Each pane is pre-seeded with comment-only hint lines # showing the command you would paste there. The script @@ -36,10 +38,18 @@ if tmux has-session -t "${SESSION}" 2>/dev/null; then exec tmux attach -t "${SESSION}" fi -# Start the 'sim' window with the first pane. Creating the session also -# starts the tmux server, so subsequent `set -g` (which target the -# server/session) work. -tmux new-session -d -s "${SESSION}" -n sim +# Start the 'sim' window with the first pane (top-left = gazebo). +# +# Every pane is created running `clear; workshop-hint TOPIC; exec bash`: +# it prints the hint card and then `exec bash` hands over to an +# interactive shell that sources ~/.bashrc (the ROS 2 environment). +# Running the hint AS the pane's command — instead of injecting it +# afterwards with `send-keys` — is race-free: there is no freshly-spawned +# shell for the keystrokes to be lost to before it starts reading input. +# +# Creating the session also starts the tmux server, so subsequent +# `set -g` (which target the server/session) work. +tmux new-session -d -s "${SESSION}" -n sim "clear; workshop-hint gazebo; exec bash" # --- Friendlier defaults --- tmux set -g pane-border-status top @@ -118,53 +128,49 @@ tmux bind -n M-k select-pane -U tmux bind -n M-l select-pane -R tmux bind -n M-z resize-pane -Z # Alt+z toggles pane zoom -# Build the 5-pane layout described in the header comment. Use stable -# pane IDs (#{pane_id}, %0/%1/...) instead of numeric pane_index because -# tmux re-numbers pane_index in reading order whenever the layout -# changes, which would scramble titles applied after all splits. +# Build the 6-pane grid described in the header comment (3 rows x 2 +# columns). Use stable pane IDs (#{pane_id}, %0/%1/...) instead of numeric +# pane_index because tmux re-numbers pane_index in reading order whenever +# the layout changes, which would scramble titles applied after all splits. +# +# Each split-window is given its `clear; workshop-hint TOPIC; exec bash` +# command directly, so the pane shows its hint with no send-keys race. -# Pane 0 is the existing pane we got from new-session. +# Pane 0 is the existing pane we got from new-session = top-left (gazebo). GZ_PANE="$(tmux display-message -p -t "${SESSION}:sim" '#{pane_id}')" -# Split horizontally → new pane on the right = px4 -PX4_PANE="$(tmux split-window -h -p 50 -t "${GZ_PANE}" -PF '#{pane_id}')" - -# Split the right column vertically → new pane below = qgc (taking the -# bottom ~67% so QGC has more room than its tiny pane 1 sibling). -QGC_PANE="$(tmux split-window -v -p 67 -t "${PX4_PANE}" -PF '#{pane_id}')" +# Split horizontally → new pane is the whole right column = px4 (top-right). +PX4_PANE="$(tmux split-window -h -p 50 -t "${GZ_PANE}" -PF '#{pane_id}' \ + "clear; workshop-hint px4; exec bash")" -# Split the left column (gazebo) vertically → new pane below for common. -COMMON_PANE="$(tmux split-window -v -p 67 -t "${GZ_PANE}" -PF '#{pane_id}')" +# Right column: split px4 into thirds → qgc (middle), example node 2 (bottom). +QGC_PANE="$(tmux split-window -v -p 67 -t "${PX4_PANE}" -PF '#{pane_id}' \ + "clear; workshop-hint qgc; exec bash")" +EXAMPLE2_PANE="$(tmux split-window -v -p 50 -t "${QGC_PANE}" -PF '#{pane_id}' \ + "clear; workshop-hint example2; exec bash")" -# Split the common pane vertically → new pane below = example launch. -EXAMPLE_PANE="$(tmux split-window -v -p 50 -t "${COMMON_PANE}" -PF '#{pane_id}')" +# Left column: split gazebo into thirds → common (middle), example node 1 (bottom). +COMMON_PANE="$(tmux split-window -v -p 67 -t "${GZ_PANE}" -PF '#{pane_id}' \ + "clear; workshop-hint common; exec bash")" +EXAMPLE1_PANE="$(tmux split-window -v -p 50 -t "${COMMON_PANE}" -PF '#{pane_id}' \ + "clear; workshop-hint example1; exec bash")" # Title every pane by stable ID (titles render in the pane border # thanks to the `pane-border-status top` option set above). -tmux select-pane -t "${GZ_PANE}" -T "gazebo" -tmux select-pane -t "${PX4_PANE}" -T "px4" -tmux select-pane -t "${QGC_PANE}" -T "qgc" -tmux select-pane -t "${COMMON_PANE}" -T "ros2 common.launch.py" -tmux select-pane -t "${EXAMPLE_PANE}" -T "ros2 example launch" - -# Seed each pane with one short, quote-free command: `clear; workshop-hint -# TOPIC`. Earlier attempts at sending a long `printf '...long escape string'` -# via send-keys raced with the freshly-spawned shell and could leave the -# first pane stuck in an unterminated command line. A single tiny invocation -# of an external helper script avoids that entire class of bug. -tmux send-keys -t "${GZ_PANE}" "clear; workshop-hint gazebo" Enter -tmux send-keys -t "${PX4_PANE}" "clear; workshop-hint px4" Enter -tmux send-keys -t "${QGC_PANE}" "clear; workshop-hint qgc" Enter -tmux send-keys -t "${COMMON_PANE}" "clear; workshop-hint common" Enter -tmux send-keys -t "${EXAMPLE_PANE}" "clear; workshop-hint example" Enter +tmux select-pane -t "${GZ_PANE}" -T "gazebo" +tmux select-pane -t "${PX4_PANE}" -T "px4" +tmux select-pane -t "${QGC_PANE}" -T "qgc" +tmux select-pane -t "${COMMON_PANE}" -T "ros2 common.launch.py" +tmux select-pane -t "${EXAMPLE1_PANE}" -T "ros2 node 1" +tmux select-pane -t "${EXAMPLE2_PANE}" -T "ros2 node 2" # Scratch window: title it so the pane-border-format does not render the # default (container hostname); the welcome banner is the first thing # attendees see when they switch to this window with Ctrl-b 1. -tmux new-window -t "${SESSION}" -n scratch +tmux new-window -t "${SESSION}" -n scratch \ + "clear; workshop-welcome 2>/dev/null || true; exec bash" SCRATCH_PANE="$(tmux display-message -p -t "${SESSION}:scratch" '#{pane_id}')" tmux select-pane -t "${SCRATCH_PANE}" -T "scratch" -tmux send-keys -t "${SCRATCH_PANE}" "clear; workshop-welcome 2>/dev/null || true" Enter # Focus the first pane and attach. tmux select-window -t "${SESSION}:sim" From 15f14df51a0fd0833db3a25f5eb6aee48b0eb4e6 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 20:28:53 -0700 Subject: [PATCH 04/12] workshop-hint: make the pane hints exercise-aware Rework the hint cards so every pane (gazebo, px4, common, node 1, node 2) is keyed by the same exercise names, mirroring the per-exercise README flow. The gazebo and px4 cards now list the --world and PX4_SYS_AUTOSTART value each exercise needs, and the example pane is split into node 1 / node 2 cards so the two-node exercises are unambiguous. --- docker/scripts/workshop-hint | 187 ++++++++++++++++++++++++++--------- 1 file changed, 143 insertions(+), 44 deletions(-) diff --git a/docker/scripts/workshop-hint b/docker/scripts/workshop-hint index 8b322f6..8718eab 100755 --- a/docker/scripts/workshop-hint +++ b/docker/scripts/workshop-hint @@ -4,7 +4,12 @@ # one short, quote-safe command (`workshop-hint gazebo`, etc.) instead of # a long multi-line printf that races with the freshly-spawned shell. # -# TOPIC is one of: gazebo, px4, qgc, common, example. +# Every card is keyed by the same exercise names so that, whichever pane +# you look at, you can follow one exercise straight down the layout: +# gazebo -> px4 -> common -> node 1 -> node 2. That mirrors the per-exercise +# README flow in px4_ossna_26/*/README.md. +# +# TOPIC is one of: gazebo, px4, qgc, common, example1, example2. import sys @@ -12,59 +17,153 @@ import sys PINK = "\x1b[38;5;213m" CYAN = "\x1b[38;5;87m" GREEN = "\x1b[38;5;156m" +YELLOW = "\x1b[38;5;228m" DIM = "\x1b[38;5;245m" BOLD = "\x1b[1m" RESET = "\x1b[0m" -def card(title: str, blurb: str, commands: list[str]) -> str: - out = [] - out.append(f"{PINK}{BOLD}▙▟ {title} ▙▟{RESET}") - out.append(f"{DIM}{blurb}{RESET}") - for c in commands: - out.append(f"{CYAN} {c}{RESET}") +# --- The six workshop exercises, in the order the workshop runs them. ---- +# Each entry: name, Gazebo world, PX4 airframe, node-1 command, node-2 +# command (None when the exercise needs only one ROS 2 node). +ARUCO_AT = ("ros2 launch aruco_tracker aruco_tracker.launch.py " + "world_name:={world} model_name:=x500_mono_cam_down_0") + +EXERCISES = [ + { + "name": "Offboard Demo", "world": "default", "airframe": "4001", + "node1": "ros2 launch offboard_demo offboard_demo.launch.py", + "node2": None, + }, + { + "name": "Custom Mode Demo", "world": "default", "airframe": "4001", + "node1": "ros2 launch custom_mode_demo custom_mode_demo.launch.py", + "node2": None, + }, + { + "name": "ArUco Tracker", "world": "aruco", "airframe": "4014", + "node1": ARUCO_AT.format(world="aruco"), + "node2": None, + }, + { + "name": "Teleop", "world": "walls", "airframe": "4013", + "node1": "ros2 launch teleop teleop.launch.py", + "node2": "ros2 run teleop_twist_rpyt_keyboard teleop_twist_rpyt_keyboard", + }, + { + "name": "Precision Land", "world": "aruco", "airframe": "4014", + "node1": ARUCO_AT.format(world="aruco"), + "node2": "ros2 run precision_land precision_land --ros-args -p use_sim_time:=true", + }, + { + "name": "Precision Land Executor", "world": "walls", "airframe": "4014", + "node1": ARUCO_AT.format(world="walls"), + "node2": "ros2 launch precision_land_executor precision_land_executor.launch.py", + }, +] + + +def title(text: str) -> str: + return f"{PINK}{BOLD}▙▟ {text} ▙▟{RESET}" + + +def blurb(text: str) -> str: + return f"{DIM}{text}{RESET}" + + +def label(text: str) -> str: + return f"{YELLOW} {text}{RESET}" + + +def cmd(text: str) -> str: + return f"{CYAN} {text}{RESET}" + + +def steps_card(head: str, intro: str, key: str, empty_note: str | None) -> str: + """Render a node-1 / node-2 card: one `label / command` block per + exercise that actually uses the pane, exercises sharing a command + grouped onto a single label.""" + out = [title(head), blurb(intro)] + seen: dict[str, list[str]] = {} + order: list[str] = [] + for ex in EXERCISES: + c = ex[key] + if c is None: + continue + if c in seen: + seen[c].append(ex["name"]) + else: + seen[c] = [ex["name"]] + order.append(c) + for c in order: + out.append(label(" + ".join(seen[c]))) + out.append(cmd(c)) + if empty_note: + out.append(blurb(empty_note)) + return "\n".join(out) + + +def gazebo_card() -> str: + out = [ + title("Gazebo Harmonic"), + blurb("Spawn the world FIRST. Paste, then set --world for your exercise:"), + cmd("python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\"), + cmd(" --model_store /home/ubuntu/PX4-gazebo-models/ --world default"), + blurb(" --world per exercise:"), + ] + for world, exs in [ + ("default", "Offboard Demo, Custom Mode Demo"), + ("aruco", "ArUco Tracker, Precision Land"), + ("walls", "Teleop, Precision Land Executor"), + ]: + out.append(f"{YELLOW} {world:<9}{RESET}{DIM}{exs}{RESET}") + return "\n".join(out) + + +def px4_card() -> str: + out = [ + title("PX4 v1.16 SITL"), + blurb("Spawn the drone once Gazebo is up. Set AUTOSTART per exercise:"), + cmd("PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\"), + cmd(" PX4_PARAM_UXRCE_DDS_SYNCT=0 \\"), + cmd(" /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs"), + blurb(" PX4_SYS_AUTOSTART per exercise:"), + ] + for code, model, exs in [ + ("4001", "x500", "Offboard Demo, Custom Mode Demo"), + ("4014", "x500_mono_cam_down", "ArUco Tracker, Precision Land, Precision Land Executor"), + ("4013", "x500_lidar_2d", "Teleop"), + ]: + out.append(f"{YELLOW} {code} {model:<20}{RESET}{DIM}{exs}{RESET}") return "\n".join(out) HINTS = { - "gazebo": card( - "Gazebo Harmonic", - "Spawn the physics world. Paste:", - [ - "python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\", - " --model_store /home/ubuntu/PX4-gazebo-models/ --world default", - ], - ), - "px4": card( - "PX4 v1.16 SITL", - "Once Gazebo is up, paste (4001 = x500):", - [ - "PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\", - " PX4_PARAM_UXRCE_DDS_SYNCT=0 \\", - " /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs", - ], - ), - "qgc": card( - "QGroundControl v5.0.8", - "Needs an X11-enabled container (default for ./docker/docker_run.sh).", - ["/home/ubuntu/QGroundControl/qgroundcontrol"], - ), - "common": card( - "ROS 2 :: common.launch.py", - "XRCE-DDS agent + clock/foxglove bridges + robot_state_publisher + px4_tf + static TF:", - ["ros2 launch px4_ossna_26 common.launch.py"], + "gazebo": gazebo_card(), + "px4": px4_card(), + "qgc": "\n".join([ + title("QGroundControl v5.0.8"), + blurb("Start any time. Needs an X11-enabled container " + "(default for ./docker/docker_run.sh). Same for every exercise:"), + cmd("/home/ubuntu/QGroundControl/qgroundcontrol"), + ]), + "common": "\n".join([ + title("ROS 2 :: common.launch.py"), + blurb("Run AFTER Gazebo + PX4, BEFORE node 1. Same for every exercise."), + blurb("XRCE-DDS agent + clock/foxglove bridges + robot_state_publisher + px4_tf:"), + cmd("ros2 launch px4_ossna_26 common.launch.py"), + ]), + "example1": steps_card( + "ROS 2 :: node 1 (first launch, every exercise)", + "Run the line for YOUR exercise, once common.launch.py is up:", + "node1", + None, ), - "example": card( - "ROS 2 :: example launch", - "Pick ONE once common.launch.py is up:", - [ - "ros2 launch offboard_demo offboard_demo.launch.py", - "ros2 launch custom_mode_demo custom_mode_demo.launch.py", - "ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0", - "ros2 launch teleop teleop.launch.py", - "ros2 run precision_land precision_land --ros-args -p use_sim_time:=true", - "ros2 launch precision_land_executor precision_land_executor.launch.py", - ], + "example2": steps_card( + "ROS 2 :: node 2 (second launch — teleop & precision-land only)", + "Run AFTER node 1, same exercise. The other exercises leave this empty.", + "node2", + "# Precision Land also needs node 1 (ArUco Tracker) running first.", ), } From 71b1a8c5665214b716adb85b37f725700c6223f0 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 20:28:58 -0700 Subject: [PATCH 05/12] feat(docker): add --tmux flag to docker_run.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With --tmux the container drops straight into the preconfigured workshop tmux layout instead of a plain shell. workshop-tmux runs as a child process (not PID 1), so detaching the session falls back to a shell and keeps the container — and the simulation — alive. --- docker/docker_run.sh | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docker/docker_run.sh b/docker/docker_run.sh index 0becf8e..aa52973 100755 --- a/docker/docker_run.sh +++ b/docker/docker_run.sh @@ -4,6 +4,7 @@ SCRIPTPATH=$(dirname "$SCRIPT") # Parse command line arguments NO_GUI=false NVIDIA=false +TMUX_LAYOUT=false while [[ $# -gt 0 ]]; do case $1 in @@ -15,9 +16,13 @@ while [[ $# -gt 0 ]]; do NVIDIA=true shift ;; + --tmux) + TMUX_LAYOUT=true + shift + ;; *) echo "Unknown argument: $1" - echo "Usage: $0 [--no-gui] [--nvidia]" + echo "Usage: $0 [--no-gui] [--nvidia] [--tmux]" exit 1 ;; esac @@ -85,7 +90,21 @@ DOCKER_CMD="$DOCKER_CMD -p 8765:8765" DOCKER_CMD="$DOCKER_CMD -v ${SCRIPTPATH}/..:/home/ubuntu/ossna-26-workshop_ws/src/ossna-26-workshop" DOCKER_CMD="$DOCKER_CMD --name=px4-ossna-26" DOCKER_CMD="$DOCKER_CMD -w /home/ubuntu/ossna-26-workshop_ws" -DOCKER_CMD="$DOCKER_CMD dronecode/ossna-26-workshop bash" +DOCKER_CMD="$DOCKER_CMD dronecode/ossna-26-workshop" + +# Container command. With --tmux, drop straight into the preconfigured +# workshop tmux layout; otherwise just open a plain bash shell. +# +# workshop-tmux is run as a child process (not exec'd as PID 1), so when +# you detach the tmux session (Ctrl+b d) you fall back to the `exec bash` +# shell instead of the container exiting — the tmux session keeps running +# and you can reattach with `workshop-tmux` here, or with +# `docker exec -it px4-ossna-26 workshop-tmux` from another terminal. +if [ "$TMUX_LAYOUT" = true ]; then + DOCKER_CMD="$DOCKER_CMD bash -c 'workshop-tmux; exec bash'" +else + DOCKER_CMD="$DOCKER_CMD bash" +fi # Execute the command eval $DOCKER_CMD From 6bc5217f1ddbe7689126de99889c34feb3a56279 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 20:29:06 -0700 Subject: [PATCH 06/12] docs: split the setup guide into setup, simulation and ros2 docs/setup.md had grown to ~500 lines covering installation, running the simulation and the ROS 2 bridge. Split it along the three workshop phases: - setup.md install requirements + start the container - simulation.md multi-terminal workflow + start PX4-Gazebo - ros2.md bridge the simulation into the ROS 2 graph Each page chains to the next, and the README lists all three. Update the exercise READMEs to point at simulation.md for the "start the simulation" steps, and fix a `source source` typo in the workspace rebuild command. --- README.md | 11 +- docs/ros2.md | 102 +++++ docs/setup.md | 396 +----------------- docs/simulation.md | 279 ++++++++++++ px4_ossna_26/custom_mode_demo/README.md | 4 +- px4_ossna_26/offboard_demo/README.md | 2 +- .../precision_land_executor/README.md | 2 +- px4_ossna_26/px4_ossna_26/README.md | 2 +- 8 files changed, 409 insertions(+), 389 deletions(-) create mode 100644 docs/ros2.md create mode 100644 docs/simulation.md diff --git a/README.md b/README.md index 1db1843..d581ef1 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ For more detailed instructions and guidance, please refer to the dedicated **REA ### Presentation -[Link to Presentation on Google Slides](https://docs.google.com/presentation/d/1S0erGP3pqjSlPU8--NCr8zwdNYxXy2enIBOqPs76fCQ/edit?usp=sharing) +[Link to Presentation on Google Slides](https://docs.google.com/presentation/d/19ol2Q97c6IONSkELWRZrWnt5dX43BAUpSw_MzT4H2Lo/edit?usp=sharing) ### Introduction & Drone Architecture @@ -42,10 +42,15 @@ For more detailed instructions and guidance, please refer to the dedicated **REA ### Environment Setup -For detailed environment and Docker setup instructions, see the [docs/README.md](docs/setup.md) guide. +The environment guide is split into three pages — follow them in order: + +1. [Setup](docs/setup.md) — install the requirements and start the container. +2. [Running the simulation](docs/simulation.md) — open the workshop terminals and start PX4 + Gazebo. +3. [Linking the simulation to ROS 2](docs/ros2.md) — bridge PX4 and Gazebo into the ROS 2 graph. + Please complete this step before you proceed. -### Control Pipelines +### Control Pipeliness There are two main ways to interact with PX4 and ROS 2: diff --git a/docs/ros2.md b/docs/ros2.md new file mode 100644 index 0000000..603947c --- /dev/null +++ b/docs/ros2.md @@ -0,0 +1,102 @@ +# Linking the simulation to ROS 2 + +With the simulation [up and running](simulation.md), it is time to bridge ROS 2 with Gazebo and PX4. + +The following sections demo the essential steps in this process. +However, when trying the exercises you can leverage the [common launchfile](../px4_ossna_26/px4_ossna_26/README.md) which automatically sets up the required bridges. + +1. **Clock bridging.** We want to leverage the GZ clock and use it to time all our ROS 2 node. +This is accomplished by first creating an unidirectional bridge between the gz `/clock` topic and the ROS 2 one and then by commanding all ROS 2 to use the newly created `/clock` ROS 2 topic as time reference. +We will use the `ros_gz_bridge` package to create the bridge: + + ```sh + ros2 run ros_gz_bridge parameter_bridge /clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock + ``` + + While the ROS 2 behavior will be set by the parameter `use_sim_time`. +2. **ROS 2 - PX4 bridge.** PX4 leverages [eProsima Micro XRCE-DDS](https://micro-xrce-dds.docs.eprosima.com/en/v2.4.3/) which internal [PX4 messages](https://docs.px4.io/v1.16/en/middleware/uorb) to be directly exposed to the ROS 2 network. +The simulated PX4 instance automatically start the Micro XRCE-DDS client using UDP protocol on port 8888, what we need to do is to just start the agent with the same settings. + + ```sh + MicroXRCEAgent udp4 -p 8888 + ``` + + after running this command you can see PX4 establish the connection - the expected output is a sequence of messages like + + ```sh + INFO [uxrce_dds_client] successfully created rt/fmu/out/vehicle_status_v1 data writer, topic id: 279 + INFO [uxrce_dds_client] successfully created rt/fmu/out/airspeed_validated data writer, topic id: 14 + INFO [uxrce_dds_client] successfully created rt/fmu/out/vtol_vehicle_status data writer, topic id: 288 + INFO [uxrce_dds_client] successfully created rt/fmu/out/home_position data writer, topic id: 123 + ``` + +## Inspecting PX4 messages + +Now that the [PX4 messages](https://docs.px4.io/v1.16/en/msg_docs/) are available to ROS 2, you can list them with + +```sh +ros2 topic list +``` + +The messages in the topics with namespace `/fmu/in` are sent from ROS 2 to PX4 while the ones with namespace `/fmu/out` go from PX4 to ROS 2. + +For example, you can check the PX4 vehicle status with + +```sh +ros2 topic echo /fmu/out/vehicle_status_v1 +``` + +You can also try the `sensor_combined_listener` node from the [px4_ros_com](https://github.com/PX4/px4_ros_com) package and get a user friendly visualization of PX4 accelerometer and gyroscope data: + +```sh +ros2 run px4_ros_com sensor_combined_listener --ros-args -p use_sim_time:=true +``` + +It will output something like: + +```sh +RECEIVED SENSOR COMBINED DATA +============================= +ts: 93380000 +gyro_rad[0]: -0.000287732 +gyro_rad[1]: -0.000181083 +gyro_rad[2]: -0.00105683 +gyro_integral_dt: 4000 +accelerometer_timestamp_relative: 0 +accelerometer_m_s2[0]: -0.00764366 +accelerometer_m_s2[1]: 6.15756e-05 +accelerometer_m_s2[2]: -9.79929 +accelerometer_integral_dt: 4000 +``` + +## Foxglove visualization + +You can use the [px4_tf](../px4_ossna_26/px4_tf/README.md) packages, in conjunction with `foxglove_bridge` to visualize in 3D the drone `base_link`. + +The `px4_tf_publisher` node subscribes to PX4 `/fmu/out/vehicle_odometry` topic and publishes a derived transform for the `odom` frame to the `base_link` frame. + +```sh +ros2 run px4_tf px4_tf_publisher --ros-args -p use_sim_time:=true +``` + +Finally `foxglove_bridge` let's us visualize the tf in Foxglove. + +```sh +ros2 run foxglove_bridge foxglove_bridge --ros-args -p use_sim_time:=true +``` + +Launch your Foxglove client and open a connection of type _Foxglove WebSocket_ with url `ws://localhost:8765`. + +![foxglove example](./assets/foxglove.png) + +**Note:** when restarting the simulations and the foxglove_bridge, you might have to restart Foxglove client too to re-establish the connection. + +## Recompiling the ROS 2 workspace + +To recompile the ROS 2 workspace + +```sh +cd ~/ossna-26-workshop_ws/ +source ~/px4_ros_ws/install/setup.bash +colcon build --symlink-install +``` diff --git a/docs/setup.md b/docs/setup.md index 9e2b21e..0cbd066 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,8 +1,10 @@ # Setup -This page will guide you through the installation of the requirements for running the workshop exercises and it will explain how ROS 2, Gazebo and PX4 will interact. +This page guides you through installing the workshop requirements and +starting the container. Once it is running, continue with +[Running the simulation](simulation.md). -The images contains all the required dependencies for the workshop, in particular: +The image contains all the dependencies required for the workshop, in particular: - [GZ HARMONIC](https://gazebosim.org/docs/harmonic/getstarted/) - [ROS 2 Humble](https://docs.ros.org/en/humble/index.html) @@ -65,11 +67,11 @@ If you're running on AMD64 with GUI, then you can open QGC directly from inside If instead you're running without GUI, then you will have to connect PX4 to a QGC instance running on the _host_. -## How to start the simulation +## Starting the container -There are two ways to start and interact w through VSCode DevContainers or with pure Docker commands. +There are two ways to start and interact with the container: with pure Docker commands, or through VSCode DevContainers. -### Starting the container with pure Docker commands +### Pure Docker commands This mode does not require VSCode. On the other hand it is slightly less user friendly as you'll have to open multiple terminals inside the container. @@ -87,11 +89,12 @@ The script will: - Mount the repository in `/home/ubuntu/ossna-26-workshop_ws/src/ossna-26-workshop` - Forward X11 to run GUI applications (GZ client, QGC) from inside the container. -You can use also use two options: +You can use also use these options: - `--no-gui` to disable GUI in the container. This option also forwards port `18570` to allow external (Host) QGC connection. - `--nvidia` to run the container with the `nvidia` runtime (it requires the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) installed and registered with Docker on the host; see `nvidia-ctk runtime configure --runtime=docker` followed by `sudo systemctl restart docker`). +- `--tmux` to drop straight into the preconfigured workshop `tmux` layout (see [Option C](simulation.md#option-c--the-preconfigured-workshop-layout-workshop-tmux)) instead of a plain shell. Detaching the session (`Ctrl+b d`) leaves you in a normal shell with the simulation still running; reattach with `workshop-tmux`. When using this method you can attach new shell to your container by running @@ -99,387 +102,14 @@ When using this method you can attach new shell to your container by running docker exec -it px4-ossna-26 bash ``` -From now-on, all commands are assumed to be run from a terminal inside the container unless otherwise specified. +From now on, all commands are assumed to be run from a terminal inside the container unless otherwise specified. -### Starting the container through VSCode DevContainers +### VSCode DevContainers To use the DevContainers, simply open the workshop repo in VSCode, then type `CTRL+SHIFT+P` and select `Dev Containers: Reopen in container`. Finally, select the devcontainer of your choice. VSCode will automatically reopen inside the running container. -### Running several workshop commands in one window - -The workshop's setup steps each occupy a terminal of their own. A typical run needs at least four shells inside the container at the same time: - -1. Gazebo (`simulation-gazebo`) — runs in the foreground, prints physics + plugin messages. -2. PX4 SITL (`/home/ubuntu/px4_sitl/bin/px4 …`) — runs in the foreground, prints its uORB / mavlink startup log. -3. The workshop's _common launch_ (`ros2 launch px4_ossna_26 common.launch.py`) — starts the MicroXRCEAgent, clock bridge, foxglove bridge, etc. -4. The example you are running for that exercise (`ros2 launch offboard_demo …`, `ros2 run precision_land …`, and so on). - -You _can_ open a fresh OS terminal for each of those and `docker exec -it px4-ossna-26 bash` four times, but that gets cumbersome. Two friendlier ways to do it in one window are below — pick whichever matches how you started the container. - -#### Option A — VSCode DevContainer integrated terminal (no extra setup) - -If you started the workshop with `Dev Containers: Reopen in container`, VSCode is already _attached to the running container_ and every integrated terminal it opens is a shell inside that container. You do not need `docker exec` again. - -1. Open the terminal panel with **Ctrl+`** (the backtick key) — or **View → Terminal**. -2. The first tab is already inside the container, at `/home/ubuntu/ossna-26-workshop_ws`. Run your first command there (for example, start Gazebo). -3. Click the **`+` icon** in the top-right of the terminal panel (next to the trash bin) to open a second tab. It is also inside the container. -4. Repeat for as many shells as you need. Each tab is independent: closing one doesn't kill the others. -5. To see two tabs side by side, click the **split icon** (the rectangle with a vertical line through it) next to `+`, or right-click a tab and pick **Split Terminal**. -6. Switch between tabs with **`Ctrl+PageDown` / `Ctrl+PageUp`**, or click the tab list on the right. - -Tip: the workspace is bind-mounted, so when you edit a source file in VSCode and rebuild with `colcon build`, the new binary is immediately picked up by `ros2 run` / `ros2 launch` in the integrated terminal. - -#### Option B — `tmux` inside the container (works with plain `docker run`) - -If you started the container with `./docker/docker_run.sh` (i.e. you are not in VSCode), use `tmux` to multiplex one OS terminal into many shells. The workshop image ships with `tmux` pre-installed, so there is nothing to install. - -1. Attach the first terminal to the container as usual: - - ```sh - docker exec -it px4-ossna-26 bash - ``` - -2. Inside the container, start a `tmux` session: - - ```sh - tmux - ``` - - You'll see a green status bar at the bottom — that means you are inside a `tmux` session. Every shortcut starts with the **prefix key**, which is `Ctrl+b` by default. Press the prefix, _release_ it, then press the next key. -3. Common shortcuts (each preceded by `Ctrl+b`, then released): - - | Shortcut | What it does | - | --- | --- | - | `"` | Split the current pane **horizontally** (new pane below) | - | `%` | Split the current pane **vertically** (new pane to the right) | - | `arrow keys` | Move focus between panes | - | `c` | Create a new full-screen **window** (different from a pane) | - | `n` / `p` | Next / previous window | - | `0`-`9` | Jump to window N | - | `z` | Zoom the current pane to full-screen / unzoom | - | `x` | Close the current pane (asks for confirmation) | - | `d` | **Detach** the session (it keeps running in the background) | - | `?` | Show the full key reference | - -4. A workshop-shaped layout, by hand, from a fresh `tmux`: - - ```text - Ctrl+b " # split horizontally ─────────────── - Ctrl+b % # split the new bottom pane vertically - Ctrl+b arrow # move focus - ``` - - gives you three panes — one for Gazebo on top, two stacked below for PX4 and the ROS 2 launches. Paste the relevant command into each. - -5. To detach and free your terminal without killing anything, press `Ctrl+b` then `d`. The simulation keeps running. To reattach later (from a new `docker exec -it px4-ossna-26 bash`): - - ```sh - tmux attach # or: tmux a - ``` - - If you have more than one session, `tmux ls` lists them and `tmux attach -t ` picks one. -6. To end everything cleanly: exit every shell in the session (`Ctrl+d` in each pane) or kill the whole session with `tmux kill-session`. - -For people who are new to `tmux`: think of it as "screen-sharing for shells" — your single terminal window becomes a tiled layout of multiple independent bash sessions, and the session survives even if you accidentally close your terminal. - -#### Option C — the preconfigured workshop layout (`workshop-tmux`) - -If you do not want to remember tmux's split commands at all, the image ships with a small launcher script that builds the layout for you. Run it instead of plain `tmux`: - -```sh -docker exec -it px4-ossna-26 workshop-tmux -``` - -It creates a tmux session named `ossna` with two windows: - -1. **`sim`** — five panes, each labelled in its border with the role it plays. Every long-running foreground process gets its own pane (`ros2 launch …` is foreground, so common and the example can't share one): - - ```text - ┌───────────────────────────────┬───────────────────────────────┐ - │ gazebo │ px4 │ - │ (paste simulation-gazebo │ (paste the PX4 SITL │ - │ here) │ command here) │ - ├───────────────────────────────┤ │ - │ ros2 common.launch.py │ qgc │ - │ (paste │ (paste │ - │ `ros2 launch px4_ossna_26 │ /home/ubuntu/QGroundControl│ - │ common.launch.py`) │ /qgroundcontrol here) │ - ├───────────────────────────────┤ │ - │ ros2 example launch │ │ - │ (paste `ros2 launch │ │ - │ offboard_demo …` etc.) │ │ - └───────────────────────────────┴───────────────────────────────┘ - ``` - - Each pane is also pre-seeded with comment lines (`# ...`) showing the actual commands to paste. The shell treats those as comments and does nothing, so you can read the hint and either paste the suggested command verbatim or edit it (different `--world`, different airframe, different launchfile, etc.). The QGC pane is given the bulk of the right column because its UI is what you'll be looking at most. -2. **`scratch`** — a single empty pane for `ros2 topic echo`, `ros2 node list`, editing files with `nano` / `vim`, and anything else ad-hoc. Switch to it with `Ctrl+b n` (next window) or `Ctrl+b 1`. - -The launcher also enables a couple of friendlier defaults on top of stock tmux: pane titles in the border, mouse mode (click to focus, scroll wheel works), 20 000 lines of scrollback, and vi keys in copy mode. All the keybindings from Option B still work, so once you are comfortable you can split / merge panes further. - -Re-running `workshop-tmux` reattaches to the existing session instead of building a new one, so it is also a convenient way back in after `Ctrl+b d` (detach) or after closing your OS terminal: - -```sh -docker exec -it px4-ossna-26 workshop-tmux # builds session OR reattaches -``` - -To start fresh with a clean layout, kill the session first: - -```sh -docker exec -it px4-ossna-26 tmux kill-session -t ossna -docker exec -it px4-ossna-26 workshop-tmux -``` - -### Starting the PX4-GZ simulation - -PX4 can directly connect to GZ using the `gz-transport` libraries. -This means that PX4 can control any GZ model as long as the model uses the required sensor and actuation plugins. - -For this workshop we will use the x500 quadrotor model. - -![X500](./assets/X500.png) - -The PX4 Gazebo worlds and models are available in the `/home/ubuntu/PX4-gazebo-models` container directory. -From there you can start a GZ simulation with a PX4 compatible world: - -```sh -python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo --model_store /home/ubuntu/PX4-gazebo-models/ --world default -``` - -- If you want to run the gz server in headless mode, add the option `--headless`. -- If you want to change the world, then change the argument of `--world`. - -Note that `--headless` is mandatory when running without GUI. - -The expected output when GUI is enabled is - -```sh -ubuntu@fe14532c7704:~$ python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo --model_store /home/ubuntu/PX4-gazebo-models/ -Found: 219 files in /home/ubuntu/PX4-gazebo-models/ -Models directory not empty. Overwrite not set. Not downloading models. -> Launching gazebo simulation... -QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-ubuntu' -[Err] [SystemLoader.cc:92] Failed to load system plugin [libOpticalFlowSystem.so] : Could not find shared library. -[Err] [SystemLoader.cc:92] Failed to load system plugin [libGstCameraSystem.so] : Could not find shared library. -``` - -- Please ignore the error messages about the plugins not found. -- The gazebo client window will open on the empty world. -- No PX4 model will appear. -This is normal as PX4 instance and model will be spawned in a different step. - -![empty GZ world](./assets/empty_gz_world.png) - -Once the GZ server is running, you can spawn the `x500` model and attach a PX4 instance to it with - -```sh -PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_PARAM_UXRCE_DDS_SYNCT=0 /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs -``` - -The expected output is - -```sh -$ PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_PARAM_UXRCE_DDS_SYNCT=0 /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs -INFO [px4] assuming working directory is rootfs, no symlinks needed. - -______ __ __ ___ -| ___ \ \ \ / / / | -| |_/ / \ V / / /| | -| __/ / \ / /_| | -| | / /^\ \ \___ | -\_| \/ \/ |_/ - -px4 starting. - -INFO [px4] startup script: /bin/sh etc/init.d-posix/rcS 0 -env SYS_AUTOSTART: 4001 -INFO [param] selected parameter default file parameters.bson -INFO [param] selected parameter backup file parameters_backup.bson - SYS_AUTOCONFIG: curr: 0 -> new: 1 - SYS_AUTOSTART: curr: 0 -> new: 4001 - CAL_ACC0_ID: curr: 0 -> new: 1310988 - CAL_GYRO0_ID: curr: 0 -> new: 1310988 - CAL_ACC1_ID: curr: 0 -> new: 1310996 - CAL_GYRO1_ID: curr: 0 -> new: 1310996 - CAL_ACC2_ID: curr: 0 -> new: 1311004 - CAL_GYRO2_ID: curr: 0 -> new: 1311004 - CAL_MAG0_ID: curr: 0 -> new: 197388 - CAL_MAG0_PRIO: curr: -1 -> new: 50 - CAL_MAG1_ID: curr: 0 -> new: 197644 - CAL_MAG1_PRIO: curr: -1 -> new: 50 - SENS_BOARD_X_OFF: curr: 0.0000 -> new: 0.0000 - SENS_DPRES_OFF: curr: 0.0000 -> new: 0.0010 - UXRCE_DDS_SYNCT: curr: 1 -> new: 0 -INFO [dataman] data manager file './dataman' size is 1208528 bytes -INFO [init] Gazebo simulator -INFO [init] Standalone PX4 launch, waiting for Gazebo -INFO [init] Gazebo world is ready -INFO [init] Spawning model -INFO [gz_bridge] world: default, model: x500_0 -INFO [lockstep_scheduler] setting initial absolute time to 2324000 us -INFO [commander] LED: open /dev/led0 failed (22) -WARN [health_and_arming_checks] Preflight Fail: ekf2 missing data -WARN [health_and_arming_checks] Preflight Fail: No connection to the ground control station -INFO [uxrce_dds_client] init UDP agent IP:127.0.0.1, port:8888 -INFO [tone_alarm] home set -INFO [mavlink] mode: Normal, data rate: 4000000 B/s on udp port 18570 remote port 14550 -INFO [mavlink] mode: Onboard, data rate: 4000000 B/s on udp port 14580 remote port 14540 -INFO [mavlink] mode: Onboard, data rate: 4000 B/s on udp port 14280 remote port 14030 -INFO [mavlink] mode: Gimbal, data rate: 400000 B/s on udp port 13030 remote port 13280 -INFO [logger] logger started (mode=all) -INFO [logger] Start file log (type: full) -INFO [logger] [logger] ./log/2025-08-09/11_56_59.ulg -INFO [logger] Opened full log file: ./log/2025-08-09/11_56_59.ulg -INFO [mavlink] MAVLink only on localhost (set param MAV_{i}_BROADCAST = 1 to enable network) -INFO [mavlink] MAVLink only on localhost (set param MAV_{i}_BROADCAST = 1 to enable network) -INFO [px4] Startup script returned successfully -pxh> WARN [health_and_arming_checks] Preflight Fail: No connection to the ground control station -WARN [health_and_arming_checks] Preflight Fail: No connection to the ground control station -``` - -Let's analyze this command: - -- `PX4_GZ_STANDALONE=1` tells the PX4 startup script that it will need to connect to an already running GZ server. -- `PX4_SYS_AUTOSTART=4001` tells the PX4 startup script that it has to use the `4001` _airframe_. -This airframe is defined in the [PX4 simulated airframes](https://github.com/PX4/PX4-Autopilot/tree/v1.16.0/ROMFS/px4fmu_common/init.d-posix/airframes) folder and is bound to the `x500` model. -Because not explicit model name was given, PX4 will insert the model in the GZ world. -An explicit mentioning of the model name would have made PX4 to simply connect to an already spawned model. -- `PX4_PARAM_UXRCE_DDS_SYNCT=0` disabled the [time synchronization](https://docs.px4.io/v1.16/en/ros2/user_guide#ros-gazebo-and-px4-time-synchronization) feature between ROS 2 and PX4. -Synchronization is not needed as Gazebo will control the clock for both PX4 and ROS 2. - -The complete documentation for running PX4 simulation in Gazebo is part of [PX4 documentation](https://docs.px4.io/main/en/sim_gazebo_gz/). - -![GZ world with x500 spawned](./assets/gz_world_with_x500.png) - -Before taking off you just need to connect QGC to your simulated drone. -If you started you container with the GUI, then you can simply run - -```sh -/home/ubuntu/QGroundControl/qgroundcontrol -``` - -If instead you don't have GUI in your container, then you can still run QGC on the host and attach it to the simulated PX4 instance. - -To do so, first [install QGC](https://docs.qgroundcontrol.com/Stable_V5.0/en/qgc-user-guide/getting_started/download_and_install.html), then start it and create a custom UDP connection link by setting the server ip to `127.0.0.1` and the port to `18570`. - -![QGC lin](./assets/qgc_custom_udp_connection.png) - -The no-gui container automatically exposed the udp port `18570` to the host. -The GUI-enable container does not expose the port so this method won't for it. - -On the PX4 terminal you will see the message - -```sh -INFO [mavlink] partner IP: 172.17.0.1 -INFO [commander] Ready for takeoff! -``` - -This is all you need to do to start the GZ + PX4 simulation, you can now takeoff! - -## Next step (optional) - Link the simulation to ROS 2 - -With the simulation up an running, it is time to bridge ROS 2 with Gazebo and PX4. - -The following sections will demo the essential steps in this process. -However, when trying the exercises you can leverage the [common launchfile](../px4_ossna_26/px4_ossna_26/README.md) which automatically sets up the required bridges. - -1. **Clock bridging.** We want to leverage the GZ clock and use it to time all our ROS 2 node. -This is accomplished by first creating an unidirectional bridge between the gz `/clock` topic and the ROS 2 one and then by commanding all ROS 2 to use the newly created `/clock` ROS 2 topic as time reference. -We will use the `ros_gz_bridge` package to create the bridge: - - ```sh - ros2 run ros_gz_bridge parameter_bridge /clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock - ``` - - While the ROS 2 behavior will be set by the parameter `use_sim_time`. -2. **ROS 2 - PX4 bridge.** PX4 leverages [eProsima Micro XRCE-DDS](https://micro-xrce-dds.docs.eprosima.com/en/v2.4.3/) which internal [PX4 messages](https://docs.px4.io/v1.16/en/middleware/uorb) to be directly exposed to the ROS 2 network. -The simulated PX4 instance automatically start the Micro XRCE-DDS client using UDP protocol on port 8888, what we need to do is to just start the agent with the same settings. - - ```sh - MicroXRCEAgent udp4 -p 8888 - ``` - - after running this command you can see PX4 establish the connection - the expected output is a sequence of messages like - - ```sh - INFO [uxrce_dds_client] successfully created rt/fmu/out/vehicle_status_v1 data writer, topic id: 279 - INFO [uxrce_dds_client] successfully created rt/fmu/out/airspeed_validated data writer, topic id: 14 - INFO [uxrce_dds_client] successfully created rt/fmu/out/vtol_vehicle_status data writer, topic id: 288 - INFO [uxrce_dds_client] successfully created rt/fmu/out/home_position data writer, topic id: 123 - ``` - -### Inspecting PX4 messages - -Now that the [PX4 messages](https://docs.px4.io/v1.16/en/msg_docs/) are available to ROS 2, you can list them with - -```sh -ros2 topic list -``` - -The messages in the topics with namespace `/fmu/in` are sent from ROS 2 to PX4 while the ones with namespace `/fmu/out` go from PX4 to ROS 2. - -For example, you can check the PX4 vehicle status with - -```sh -ros2 topic echo /fmu/out/vehicle_status_v1 -``` - -You can also try the `sensor_combined_listener` node from the [px4_ros_com](https://github.com/PX4/px4_ros_com) package and get a user friendly visualization of PX4 accelerometer and gyroscope data: - -```sh -ros2 run px4_ros_com sensor_combined_listener --ros-args -p use_sim_time:=true -``` - -It will output something like: - -```sh -RECEIVED SENSOR COMBINED DATA -============================= -ts: 93380000 -gyro_rad[0]: -0.000287732 -gyro_rad[1]: -0.000181083 -gyro_rad[2]: -0.00105683 -gyro_integral_dt: 4000 -accelerometer_timestamp_relative: 0 -accelerometer_m_s2[0]: -0.00764366 -accelerometer_m_s2[1]: 6.15756e-05 -accelerometer_m_s2[2]: -9.79929 -accelerometer_integral_dt: 4000 -``` - -### Foxglove visualization - -You can use the [px4_tf](../px4_ossna_26/px4_tf/README.md) packages, in conjunction with `foxglove_bridge` to visualize in 3D the drone `base_link`. - -The `px4_tf_publisher` node subscribes to PX4 `/fmu/out/vehicle_odometry` topic and publishes a derived transform for the `odom` frame to the `base_link` frame. - -```sh -ros2 run px4_tf px4_tf_publisher --ros-args -p use_sim_time:=true -``` - -Finally `foxglove_bridge` let's us visualize the tf in Foxglove. - -```sh -ros2 run foxglove_bridge foxglove_bridge --ros-args -p use_sim_time:=true -``` - -Launch your Foxglove client and open a connection of type _Foxglove WebSocket_ with url `ws://localhost:8765`. - -![foxglove example](./assets/foxglove.png) - -**Note:** when restarting the simulations and the foxglove_bridge, you might have to restart Foxglove client too to re-establish the connection. - -### Recompiling the ROS 2 workspace - -To recompile the ROS 2 workspace - -```sh -cd ~/ossna-26-workshop_ws/ -source source ~/px4_ros_ws/install/setup.bash -colcon build --symlink-install -``` - ## Troubleshooting ### T1: Gazebo GUI not showing @@ -494,3 +124,7 @@ If you don't have nvidia drivers or NVIDIA Container Toolkit installed on WSL2 y ### T3: `libEGL warning: egl: failed to create dri2 screen` when starting Gazebo on a hybrid Intel + NVIDIA host A3: These warnings are harmless. On hybrid laptops, Mesa enumerates every `/dev/dri/renderD*` node and tries to build an EGL context on each one. The NVIDIA card cannot be driven by Mesa (it needs the proprietary driver), so EGL initialisation for that node fails and Mesa falls back to the Intel GPU. The Gazebo GUI still opens and renders correctly through Intel. If you want to use the NVIDIA card instead, install the NVIDIA Container Toolkit and run `./docker/docker_run.sh --nvidia`. + +--- + +**Next:** [Running the simulation](simulation.md) — open the workshop terminals and start PX4 + Gazebo. diff --git a/docs/simulation.md b/docs/simulation.md new file mode 100644 index 0000000..aef3f00 --- /dev/null +++ b/docs/simulation.md @@ -0,0 +1,279 @@ +# Running the simulation + +With the container running (see [Setup](setup.md)), this page shows how to +open the workshop's terminals and start the PX4 + Gazebo simulation. + +> All commands are run from a terminal **inside the container**. + +## Running several workshop commands in one window + +The workshop's setup steps each occupy a terminal of their own. A typical run needs at least four shells inside the container at the same time: + +1. Gazebo (`simulation-gazebo`) — runs in the foreground, prints physics + plugin messages. +2. PX4 SITL (`/home/ubuntu/px4_sitl/bin/px4 …`) — runs in the foreground, prints its uORB / mavlink startup log. +3. The workshop's _common launch_ (`ros2 launch px4_ossna_26 common.launch.py`) — starts the MicroXRCEAgent, clock bridge, foxglove bridge, etc. +4. The example you are running for that exercise (`ros2 launch offboard_demo …`, `ros2 run precision_land …`, and so on). + +You _can_ open a fresh OS terminal for each of those and `docker exec -it px4-ossna-26 bash` four times, but that gets cumbersome. Two friendlier ways to do it in one window are below — pick whichever matches how you started the container. + +### Option A — VSCode DevContainer integrated terminal (no extra setup) + +If you started the workshop with `Dev Containers: Reopen in container`, VSCode is already _attached to the running container_ and every integrated terminal it opens is a shell inside that container. You do not need `docker exec` again. + +1. Open the terminal panel with **Ctrl+`** (the backtick key) — or **View → Terminal**. +2. The first tab is already inside the container, at `/home/ubuntu/ossna-26-workshop_ws`. Run your first command there (for example, start Gazebo). +3. Click the **`+` icon** in the top-right of the terminal panel (next to the trash bin) to open a second tab. It is also inside the container. +4. Repeat for as many shells as you need. Each tab is independent: closing one doesn't kill the others. +5. To see two tabs side by side, click the **split icon** (the rectangle with a vertical line through it) next to `+`, or right-click a tab and pick **Split Terminal**. +6. Switch between tabs with **`Ctrl+PageDown` / `Ctrl+PageUp`**, or click the tab list on the right. + +Tip: the workspace is bind-mounted, so when you edit a source file in VSCode and rebuild with `colcon build`, the new binary is immediately picked up by `ros2 run` / `ros2 launch` in the integrated terminal. + +### Option B — `tmux` inside the container (works with plain `docker run`) + +If you started the container with `./docker/docker_run.sh` (i.e. you are not in VSCode), use `tmux` to multiplex one OS terminal into many shells. The workshop image ships with `tmux` pre-installed, so there is nothing to install. + +1. Attach the first terminal to the container as usual: + + ```sh + docker exec -it px4-ossna-26 bash + ``` + +2. Inside the container, start a `tmux` session: + + ```sh + tmux + ``` + + You'll see a green status bar at the bottom — that means you are inside a `tmux` session. Every shortcut starts with the **prefix key**, which is `Ctrl+b` by default. Press the prefix, _release_ it, then press the next key. +3. Common shortcuts (each preceded by `Ctrl+b`, then released): + + | Shortcut | What it does | + | --- | --- | + | `"` | Split the current pane **horizontally** (new pane below) | + | `%` | Split the current pane **vertically** (new pane to the right) | + | `arrow keys` | Move focus between panes | + | `c` | Create a new full-screen **window** (different from a pane) | + | `n` / `p` | Next / previous window | + | `0`-`9` | Jump to window N | + | `z` | Zoom the current pane to full-screen / unzoom | + | `x` | Close the current pane (asks for confirmation) | + | `d` | **Detach** the session (it keeps running in the background) | + | `?` | Show the full key reference | + +4. A workshop-shaped layout, by hand, from a fresh `tmux`: + + ```text + Ctrl+b " # split horizontally ─────────────── + Ctrl+b % # split the new bottom pane vertically + Ctrl+b arrow # move focus + ``` + + gives you three panes — one for Gazebo on top, two stacked below for PX4 and the ROS 2 launches. Paste the relevant command into each. + +5. To detach and free your terminal without killing anything, press `Ctrl+b` then `d`. The simulation keeps running. To reattach later (from a new `docker exec -it px4-ossna-26 bash`): + + ```sh + tmux attach # or: tmux a + ``` + + If you have more than one session, `tmux ls` lists them and `tmux attach -t ` picks one. +6. To end everything cleanly: exit every shell in the session (`Ctrl+d` in each pane) or kill the whole session with `tmux kill-session`. + +For people who are new to `tmux`: think of it as "screen-sharing for shells" — your single terminal window becomes a tiled layout of multiple independent bash sessions, and the session survives even if you accidentally close your terminal. + +### Option C — the preconfigured workshop layout (`workshop-tmux`) + +If you do not want to remember tmux's split commands at all, the image ships with a small launcher script that builds the layout for you. Run it instead of plain `tmux`: + +```sh +docker exec -it px4-ossna-26 workshop-tmux +``` + +It creates a tmux session named `ossna` with two windows: + +1. **`sim`** — a 6-pane grid (3 rows × 2 columns), each pane labelled in its border with the role it plays. Every long-running foreground process gets its own pane (`ros2 launch …` is foreground, so common and the examples can't share one). There are **two** example panes — `node 1` and `node 2` — because the teleop and precision-land exercises each run two ROS 2 nodes at the same time (e.g. `aruco_tracker` plus `precision_land`): + + ```text + ┌───────────────────────────────┬───────────────────────────────┐ + │ gazebo │ px4 │ + │ (paste simulation-gazebo) │ (paste the PX4 SITL command)│ + ├───────────────────────────────┼───────────────────────────────┤ + │ ros2 common.launch.py │ qgc │ + │ (paste `ros2 launch │ (paste /home/ubuntu/ │ + │ px4_ossna_26 │ QGroundControl/ │ + │ common.launch.py`) │ qgroundcontrol) │ + ├───────────────────────────────┼───────────────────────────────┤ + │ ros2 node 1 │ ros2 node 2 │ + │ (first example node, e.g. │ (second node, only teleop / │ + │ aruco_tracker.launch.py) │ precision-land exercises) │ + └───────────────────────────────┴───────────────────────────────┘ + ``` + + Each pane is also pre-seeded with comment lines (`# ...`) showing the actual commands to paste. The shell treats those as comments and does nothing, so you can read the hint and either paste the suggested command verbatim or edit it (different `--world`, different airframe, different launchfile, etc.). For the offboard / custom-mode / aruco exercises the `node 2` pane simply stays empty. +2. **`scratch`** — a single empty pane for `ros2 topic echo`, `ros2 node list`, editing files with `nano` / `vim`, and anything else ad-hoc. Switch to it with `Ctrl+b n` (next window) or `Ctrl+b 1`. + +The launcher also enables a couple of friendlier defaults on top of stock tmux: pane titles in the border, mouse mode (click to focus, scroll wheel works), 20 000 lines of scrollback, and vi keys in copy mode. All the keybindings from Option B still work, so once you are comfortable you can split / merge panes further. + +Re-running `workshop-tmux` reattaches to the existing session instead of building a new one, so it is also a convenient way back in after `Ctrl+b d` (detach) or after closing your OS terminal: + +```sh +docker exec -it px4-ossna-26 workshop-tmux # builds session OR reattaches +``` + +To start fresh with a clean layout, kill the session first: + +```sh +docker exec -it px4-ossna-26 tmux kill-session -t ossna +docker exec -it px4-ossna-26 workshop-tmux +``` + +## Starting the PX4-GZ simulation + +PX4 can directly connect to GZ using the `gz-transport` libraries. +This means that PX4 can control any GZ model as long as the model uses the required sensor and actuation plugins. + +For this workshop we will use the x500 quadrotor model. + +![X500](./assets/X500.png) + +The PX4 Gazebo worlds and models are available in the `/home/ubuntu/PX4-gazebo-models` container directory. +From there you can start a GZ simulation with a PX4 compatible world: + +```sh +python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo --model_store /home/ubuntu/PX4-gazebo-models/ --world default +``` + +- If you want to run the gz server in headless mode, add the option `--headless`. +- If you want to change the world, then change the argument of `--world`. + +Note that `--headless` is mandatory when running without GUI. + +The expected output when GUI is enabled is + +```sh +ubuntu@fe14532c7704:~$ python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo --model_store /home/ubuntu/PX4-gazebo-models/ +Found: 219 files in /home/ubuntu/PX4-gazebo-models/ +Models directory not empty. Overwrite not set. Not downloading models. +> Launching gazebo simulation... +QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-ubuntu' +[Err] [SystemLoader.cc:92] Failed to load system plugin [libOpticalFlowSystem.so] : Could not find shared library. +[Err] [SystemLoader.cc:92] Failed to load system plugin [libGstCameraSystem.so] : Could not find shared library. +``` + +- Please ignore the error messages about the plugins not found. +- The gazebo client window will open on the empty world. +- No PX4 model will appear. +This is normal as PX4 instance and model will be spawned in a different step. + +![empty GZ world](./assets/empty_gz_world.png) + +Once the GZ server is running, you can spawn the `x500` model and attach a PX4 instance to it with + +```sh +PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_PARAM_UXRCE_DDS_SYNCT=0 /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs +``` + +The expected output is + +```sh +$ PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_PARAM_UXRCE_DDS_SYNCT=0 /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs +INFO [px4] assuming working directory is rootfs, no symlinks needed. + +______ __ __ ___ +| ___ \ \ \ / / / | +| |_/ / \ V / / /| | +| __/ / \ / /_| | +| | / /^\ \ \___ | +\_| \/ \/ |_/ + +px4 starting. + +INFO [px4] startup script: /bin/sh etc/init.d-posix/rcS 0 +env SYS_AUTOSTART: 4001 +INFO [param] selected parameter default file parameters.bson +INFO [param] selected parameter backup file parameters_backup.bson + SYS_AUTOCONFIG: curr: 0 -> new: 1 + SYS_AUTOSTART: curr: 0 -> new: 4001 + CAL_ACC0_ID: curr: 0 -> new: 1310988 + CAL_GYRO0_ID: curr: 0 -> new: 1310988 + CAL_ACC1_ID: curr: 0 -> new: 1310996 + CAL_GYRO1_ID: curr: 0 -> new: 1310996 + CAL_ACC2_ID: curr: 0 -> new: 1311004 + CAL_GYRO2_ID: curr: 0 -> new: 1311004 + CAL_MAG0_ID: curr: 0 -> new: 197388 + CAL_MAG0_PRIO: curr: -1 -> new: 50 + CAL_MAG1_ID: curr: 0 -> new: 197644 + CAL_MAG1_PRIO: curr: -1 -> new: 50 + SENS_BOARD_X_OFF: curr: 0.0000 -> new: 0.0000 + SENS_DPRES_OFF: curr: 0.0000 -> new: 0.0010 + UXRCE_DDS_SYNCT: curr: 1 -> new: 0 +INFO [dataman] data manager file './dataman' size is 1208528 bytes +INFO [init] Gazebo simulator +INFO [init] Standalone PX4 launch, waiting for Gazebo +INFO [init] Gazebo world is ready +INFO [init] Spawning model +INFO [gz_bridge] world: default, model: x500_0 +INFO [lockstep_scheduler] setting initial absolute time to 2324000 us +INFO [commander] LED: open /dev/led0 failed (22) +WARN [health_and_arming_checks] Preflight Fail: ekf2 missing data +WARN [health_and_arming_checks] Preflight Fail: No connection to the ground control station +INFO [uxrce_dds_client] init UDP agent IP:127.0.0.1, port:8888 +INFO [tone_alarm] home set +INFO [mavlink] mode: Normal, data rate: 4000000 B/s on udp port 18570 remote port 14550 +INFO [mavlink] mode: Onboard, data rate: 4000000 B/s on udp port 14580 remote port 14540 +INFO [mavlink] mode: Onboard, data rate: 4000 B/s on udp port 14280 remote port 14030 +INFO [mavlink] mode: Gimbal, data rate: 400000 B/s on udp port 13030 remote port 13280 +INFO [logger] logger started (mode=all) +INFO [logger] Start file log (type: full) +INFO [logger] [logger] ./log/2025-08-09/11_56_59.ulg +INFO [logger] Opened full log file: ./log/2025-08-09/11_56_59.ulg +INFO [mavlink] MAVLink only on localhost (set param MAV_{i}_BROADCAST = 1 to enable network) +INFO [mavlink] MAVLink only on localhost (set param MAV_{i}_BROADCAST = 1 to enable network) +INFO [px4] Startup script returned successfully +pxh> WARN [health_and_arming_checks] Preflight Fail: No connection to the ground control station +WARN [health_and_arming_checks] Preflight Fail: No connection to the ground control station +``` + +Let's analyze this command: + +- `PX4_GZ_STANDALONE=1` tells the PX4 startup script that it will need to connect to an already running GZ server. +- `PX4_SYS_AUTOSTART=4001` tells the PX4 startup script that it has to use the `4001` _airframe_. +This airframe is defined in the [PX4 simulated airframes](https://github.com/PX4/PX4-Autopilot/tree/v1.16.0/ROMFS/px4fmu_common/init.d-posix/airframes) folder and is bound to the `x500` model. +Because not explicit model name was given, PX4 will insert the model in the GZ world. +An explicit mentioning of the model name would have made PX4 to simply connect to an already spawned model. +- `PX4_PARAM_UXRCE_DDS_SYNCT=0` disabled the [time synchronization](https://docs.px4.io/v1.16/en/ros2/user_guide#ros-gazebo-and-px4-time-synchronization) feature between ROS 2 and PX4. +Synchronization is not needed as Gazebo will control the clock for both PX4 and ROS 2. + +The complete documentation for running PX4 simulation in Gazebo is part of [PX4 documentation](https://docs.px4.io/main/en/sim_gazebo_gz/). + +![GZ world with x500 spawned](./assets/gz_world_with_x500.png) + +Before taking off you just need to connect QGC to your simulated drone. +If you started you container with the GUI, then you can simply run + +```sh +/home/ubuntu/QGroundControl/qgroundcontrol +``` + +If instead you don't have GUI in your container, then you can still run QGC on the host and attach it to the simulated PX4 instance. + +To do so, first [install QGC](https://docs.qgroundcontrol.com/Stable_V5.0/en/qgc-user-guide/getting_started/download_and_install.html), then start it and create a custom UDP connection link by setting the server ip to `127.0.0.1` and the port to `18570`. + +![QGC lin](./assets/qgc_custom_udp_connection.png) + +The no-gui container automatically exposed the udp port `18570` to the host. +The GUI-enable container does not expose the port so this method won't for it. + +On the PX4 terminal you will see the message + +```sh +INFO [mavlink] partner IP: 172.17.0.1 +INFO [commander] Ready for takeoff! +``` + +This is all you need to do to start the GZ + PX4 simulation, you can now takeoff! + +--- + +**Next:** [Linking the simulation to ROS 2](ros2.md) — bridge PX4 and Gazebo into the ROS 2 graph. diff --git a/px4_ossna_26/custom_mode_demo/README.md b/px4_ossna_26/custom_mode_demo/README.md index b2b61fd..d2eb826 100644 --- a/px4_ossna_26/custom_mode_demo/README.md +++ b/px4_ossna_26/custom_mode_demo/README.md @@ -24,13 +24,13 @@ The demo executes the following autonomous sequence: ## Prerequisites -1. Start the simulation, PX4 and QGC as described in the [setup guide](../../docs/setup.md). +1. Start the simulation, PX4 and QGC as described in the [simulation guide](../../docs/simulation.md). 2. Ensure the vehicle is armed (GPS lock, all sensors healthy) 3. Verify QGroundControl connection for mode monitoring ## Usage -1. Start the simulation, PX4 and QGC as described in the [setup guide](../../docs/setup.md). +1. Start the simulation, PX4 and QGC as described in the [simulation guide](../../docs/simulation.md). 2. Start the additional ROS 2 node through the [common launchfile](../px4_ossna_26/README.md) ```sh diff --git a/px4_ossna_26/offboard_demo/README.md b/px4_ossna_26/offboard_demo/README.md index d9d3ccc..71f7389 100644 --- a/px4_ossna_26/offboard_demo/README.md +++ b/px4_ossna_26/offboard_demo/README.md @@ -16,7 +16,7 @@ The Offboard Demo implements a state-based flight controller that executes the f ## Usage -1. Start the simulation, PX4 and QGC as described in the [setup guide](../../docs/setup.md). +1. Start the simulation, PX4 and QGC as described in the [simulation guide](../../docs/simulation.md). 2. Start the additional ROS 2 node through the [common launchfile](../px4_ossna_26/README.md). ```sh diff --git a/px4_ossna_26/precision_land_executor/README.md b/px4_ossna_26/precision_land_executor/README.md index 2fb87ca..ee0f1b4 100644 --- a/px4_ossna_26/precision_land_executor/README.md +++ b/px4_ossna_26/precision_land_executor/README.md @@ -34,7 +34,7 @@ The system: ## Prerequisites -1. Start the simulation, PX4 and QGC as described in the [setup guide](../../docs/setup.md) with the `walls` world and the x500 with the downfacing camera: +1. Start the simulation, PX4 and QGC as described in the [simulation guide](../../docs/simulation.md) with the `walls` world and the x500 with the downfacing camera: ```sh python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo --model_store /home/ubuntu/PX4-gazebo-models/ --world walls diff --git a/px4_ossna_26/px4_ossna_26/README.md b/px4_ossna_26/px4_ossna_26/README.md index 236edea..61141bf 100644 --- a/px4_ossna_26/px4_ossna_26/README.md +++ b/px4_ossna_26/px4_ossna_26/README.md @@ -21,4 +21,4 @@ You can use [Foxglove client](https://foxglove.dev/) to visualize the drone posi The [ossna-26-workshop](../../foxglove/ossna-26-workshop.json) layout provides 3D visualization, xy-map and altitude plot. Gazebo, PX4 and QGC are not automatically started by this launchfile. -Please refer to [the setup](../../docs/setup.md) to know more on how to start them. +Please refer to the [simulation guide](../../docs/simulation.md) to know more on how to start them. From 374505a5ea190a578511606dcdd83670a6196bb9 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 21:29:40 -0700 Subject: [PATCH 07/12] docs(readme): fix control pipelines heading --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d581ef1..1e4ec9a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ The environment guide is split into three pages — follow them in order: Please complete this step before you proceed. -### Control Pipeliness +### Control Pipelines There are two main ways to interact with PX4 and ROS 2: From c705f2a29709dba24bce628c480bfdcb1ee8e5dd Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 21:29:47 -0700 Subject: [PATCH 08/12] fix(docker): attach to running workshop container --- docker/docker_run.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docker/docker_run.sh b/docker/docker_run.sh index aa52973..06a4975 100755 --- a/docker/docker_run.sh +++ b/docker/docker_run.sh @@ -28,6 +28,19 @@ while [[ $# -gt 0 ]]; do esac done +# If the workshop container is already running, a second `docker run` with +# the same --name fails with a name conflict, so any terminal beyond the +# first never makes it into the container. Detect the running container and +# exec a new shell into it instead. +if docker ps --format '{{.Names}}' | grep -qx px4-ossna-26; then + echo "Container px4-ossna-26 is already running — attaching a new shell." + if [ "$TMUX_LAYOUT" = true ]; then + exec docker exec -it px4-ossna-26 workshop-tmux + else + exec docker exec -it px4-ossna-26 bash + fi +fi + # Build docker run command DOCKER_CMD="docker run -it --rm" From 2835f7f4969264b71d49b2ad73e971d069751be6 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 21:31:22 -0700 Subject: [PATCH 09/12] fix(workshop-tmux): detach existing clients on attach --- docker/scripts/workshop-tmux.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 606935a..4d76779 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -28,14 +28,19 @@ # scratch - empty pane for ad-hoc `ros2 topic echo`, `ros2 node list`, # editing files with vim/nano, etc. # -# If the session already exists this just reattaches to it. +# If the session already exists this reattaches to it. The attach uses +# `-d` to detach any other client first: the 6-pane layout is built from +# percentage splits, so two clients of different terminal sizes attached +# at once make tmux resize the shared window and collapse the panes. One +# client at a time keeps the layout intact, and makes `docker_run.sh +# --tmux` and `docker exec ... workshop-tmux` behave identically. set -eu SESSION="ossna" if tmux has-session -t "${SESSION}" 2>/dev/null; then - exec tmux attach -t "${SESSION}" + exec tmux attach -d -t "${SESSION}" fi # Start the 'sim' window with the first pane (top-left = gazebo). @@ -172,7 +177,7 @@ tmux new-window -t "${SESSION}" -n scratch \ SCRATCH_PANE="$(tmux display-message -p -t "${SESSION}:scratch" '#{pane_id}')" tmux select-pane -t "${SCRATCH_PANE}" -T "scratch" -# Focus the first pane and attach. +# Focus the first pane and attach (-d: detach any other client, see above). tmux select-window -t "${SESSION}:sim" tmux select-pane -t "${SESSION}:sim.0" -exec tmux attach -t "${SESSION}" +exec tmux attach -d -t "${SESSION}" From 67f5bf1a9c324aaa6cac3ac0d6d3b0bee977bd16 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 21:31:59 -0700 Subject: [PATCH 10/12] fix(workshop-tmux): set terminal before creating panes --- docker/scripts/workshop-tmux.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 4d76779..c18aca6 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -52,16 +52,25 @@ fi # afterwards with `send-keys` — is race-free: there is no freshly-spawned # shell for the keystrokes to be lost to before it starts reading input. # -# Creating the session also starts the tmux server, so subsequent -# `set -g` (which target the server/session) work. -tmux new-session -d -s "${SESSION}" -n sim "clear; workshop-hint gazebo; exec bash" +# Pin default-terminal in the SAME tmux invocation that creates the +# session, before new-session. A pane takes its TERM from default-terminal +# at spawn time, and new-session spawns pane %0 immediately — so if +# default-terminal were set afterwards (with the other `set -g` below) %0 +# would keep tmux's built-in `screen` default while the later split-window +# panes got tmux-256color. That mismatch shows: `screen` is an 8-colour, +# non-256 TERM, so the gazebo pane loses the 256-colour hint palette and +# its coloured shell prompt (Ubuntu's ~/.bashrc only colours the prompt +# for a *-256color TERM). The two commands must share one `tmux` call — +# a server with no sessions exits, so `start-server` then a separate `set` +# would not persist. +tmux set -g default-terminal "tmux-256color" \; \ + new-session -d -s "${SESSION}" -n sim "clear; workshop-hint gazebo; exec bash" # --- Friendlier defaults --- tmux set -g pane-border-status top tmux set -g history-limit 20000 tmux setw -g mode-keys vi tmux set -g status-interval 1 # refresh status bar (and animations) every second -tmux set -g default-terminal "tmux-256color" # opt into 256/truecolor where supported tmux set -ga terminal-overrides ",xterm-256color:Tc" # tell tmux the outer terminal is true-color tmux set -g pane-border-lines heavy # thicker borders on tmux 3.2+ From a5021354d43488ec1c0e8261ef165e19a04f2eb8 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 21:32:24 -0700 Subject: [PATCH 11/12] feat(workshop-tmux): copy selections to host clipboard --- docker/scripts/install_deps.sh | 3 ++- docker/scripts/workshop-tmux.sh | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docker/scripts/install_deps.sh b/docker/scripts/install_deps.sh index b6f78d7..de3005d 100755 --- a/docker/scripts/install_deps.sh +++ b/docker/scripts/install_deps.sh @@ -40,7 +40,8 @@ ${SUDO} apt-get install -y --no-install-recommends \ ros-humble-vision-msgs \ libgflags-dev \ python3-rospkg \ - tmux + tmux \ + xclip ${SUDO} rm -rf /var/lib/apt/lists/* ${SUDO} apt-get clean \ No newline at end of file diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index c18aca6..8d3b1e3 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -86,7 +86,28 @@ tmux set -g pane-border-lines heavy # thicker borders on tmux 3.2+ # system clipboard via the OSC 52 escape sequence, so a plain drag is all # you need — no Shift, and the selection survives screen updates. tmux set -g mouse on +# --- Copy to the host's system clipboard --------------------------------- +# The container shares the host X server (DISPLAY and /tmp/.X11-unix are +# forwarded in), so `xclip` writing the X CLIPBOARD selection writes the +# host's real clipboard directly. That works no matter which terminal +# emulator the attendee runs — unlike OSC 52, which VTE-based terminals +# (GNOME Terminal, Terminator, Tilix) silently drop. +# +# On mouse drag-release, pipe the selection to xclip and leave copy-mode so +# the live gazebo/px4 output resumes scrolling. Same for the vi-style `y` +# and `Enter` copy keys. +CLIP='xclip -selection clipboard -in' +tmux bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-pipe-and-cancel "${CLIP}" +tmux bind -T copy-mode-vi y send -X copy-pipe-and-cancel "${CLIP}" +tmux bind -T copy-mode-vi Enter send -X copy-pipe-and-cancel "${CLIP}" + +# Belt-and-braces: also forward copies over OSC 52 for terminals that DO +# support it (kitty, foot, WezTerm, xterm); harmless where unsupported. +# tmux only emits OSC 52 when it thinks the terminal advertises the `Ms` +# capability, which the tmux-256color/screen terminfo entries omit — so +# force it on for every terminal. tmux set -g set-clipboard on +tmux set -ag terminal-overrides ',*:Ms=\E]52;%p1%s;%p2%s\007' # --- Dracula-inspired palette (synthwave-y, dev-friendly) --- # bg #282a36 bg-dark #13111c From c5f39d99c408cfb7eb0adca0c906d2b8b4b4e619 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 19 May 2026 21:32:35 -0700 Subject: [PATCH 12/12] fix(workshop-tmux): redraw hints after client attach --- docker/scripts/workshop-tmux.sh | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 8d3b1e3..1aa973c 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -199,6 +199,15 @@ tmux select-pane -t "${COMMON_PANE}" -T "ros2 common.launch.py" tmux select-pane -t "${EXAMPLE1_PANE}" -T "ros2 node 1" tmux select-pane -t "${EXAMPLE2_PANE}" -T "ros2 node 2" +# Record each pane's hint command in a pane-scoped @hint option so the +# re-hint block below can redraw it once the panes are at their real size. +tmux set -p -t "${GZ_PANE}" @hint "workshop-hint gazebo" +tmux set -p -t "${PX4_PANE}" @hint "workshop-hint px4" +tmux set -p -t "${QGC_PANE}" @hint "workshop-hint qgc" +tmux set -p -t "${COMMON_PANE}" @hint "workshop-hint common" +tmux set -p -t "${EXAMPLE1_PANE}" @hint "workshop-hint example1" +tmux set -p -t "${EXAMPLE2_PANE}" @hint "workshop-hint example2" + # Scratch window: title it so the pane-border-format does not render the # default (container hostname); the welcome banner is the first thing # attendees see when they switch to this window with Ctrl-b 1. @@ -206,6 +215,32 @@ tmux new-window -t "${SESSION}" -n scratch \ "clear; workshop-welcome 2>/dev/null || true; exec bash" SCRATCH_PANE="$(tmux display-message -p -t "${SESSION}:scratch" '#{pane_id}')" tmux select-pane -t "${SCRATCH_PANE}" -T "scratch" +tmux set -p -t "${SCRATCH_PANE}" @hint "workshop-welcome 2>/dev/null || true" + +# The hints above were printed while the session was still detached, i.e. +# into an 80x24 window where every pane is tiny — a card taller than its +# pane loses its top (the title and the command to copy). Re-render each +# hint once a client has attached and the panes are at their real size; +# workshop-hint then fits the card to the pane. This runs in the +# background so it does not block the attach, and only touches panes still +# sitting at an idle bash prompt, so it can never type into a process the +# attendee has already started. +( + # Wait (up to ~15s) for a client, so the window is at its final size. + for _ in $(seq 1 150); do + tmux list-clients -t "${SESSION}" 2>/dev/null | grep -q . && break + sleep 0.1 + done + sleep 0.5 # let the attach-time resize settle + for pane in $(tmux list-panes -s -t "${SESSION}" -F '#{pane_id}'); do + hint="$(tmux show -pqv -t "${pane}" @hint)" + [ -n "${hint}" ] || continue + [ "$(tmux display -p -t "${pane}" '#{pane_current_command}')" = bash ] \ + || continue + # Leading space keeps the re-render out of the shell history. + tmux send-keys -t "${pane}" " clear; ${hint}" Enter + done +) >/dev/null 2>&1 & # Focus the first pane and attach (-d: detach any other client, see above). tmux select-window -t "${SESSION}:sim"