diff --git a/docker/Dockerfile b/docker/Dockerfile index 1cefb98..7c7bffa 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -82,6 +82,20 @@ COPY --from=dep-ros2 --chown=${USER}:${USER} /root/px4_ros_ws/install /home/${US # retrieve QGC COPY --from=qgc --chown=${USER}:${USER} /home/${USER}/QGroundControl /home/${USER}/QGroundControl +# Workshop tmux launcher + its animation / hint / welcome helpers. Copied +# into /usr/local/bin before switching to the unprivileged user so they end +# up owned by root and exec'able by all. +COPY ./docker/scripts/workshop-tmux.sh /usr/local/bin/workshop-tmux +COPY ./docker/scripts/workshop-banner /usr/local/bin/workshop-banner +COPY ./docker/scripts/workshop-spinner /usr/local/bin/workshop-spinner +COPY ./docker/scripts/workshop-hint /usr/local/bin/workshop-hint +COPY ./docker/scripts/workshop-welcome /usr/local/bin/workshop-welcome +RUN chmod +x /usr/local/bin/workshop-tmux \ + /usr/local/bin/workshop-banner \ + /usr/local/bin/workshop-spinner \ + /usr/local/bin/workshop-hint \ + /usr/local/bin/workshop-welcome + # Switch to user USER ${USER} diff --git a/docker/scripts/install_deps.sh b/docker/scripts/install_deps.sh index 1daaf37..b6f78d7 100755 --- a/docker/scripts/install_deps.sh +++ b/docker/scripts/install_deps.sh @@ -39,7 +39,8 @@ ${SUDO} apt-get install -y --no-install-recommends \ ros-humble-gps-msgs \ ros-humble-vision-msgs \ libgflags-dev \ - python3-rospkg + python3-rospkg \ + tmux ${SUDO} rm -rf /var/lib/apt/lists/* ${SUDO} apt-get clean \ No newline at end of file diff --git a/docker/scripts/workshop-banner b/docker/scripts/workshop-banner new file mode 100755 index 0000000..afb4183 --- /dev/null +++ b/docker/scripts/workshop-banner @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# workshop-banner: prints a synthwave-y status-left banner with a traveling +# white-hot "scanner" that sweeps through the title every half second. Called +# from tmux's status-left with `status-interval 1`. The scanner advances two +# columns per tick so it visibly moves at the 1 Hz refresh rate. + +import time + +# The visible title: chunky shaded gradient on either side of the words. +# All characters render fine in a stock terminal; no Nerd Font required. +TITLE = "░▒▓ OSS NA ◆ 26 ▓▒░" + +# Dracula-ish palette, biased toward pink/purple/cyan for the OSS vibe. +PEAK = "#f8f8f2" # white-hot +HOT = "#ff79c6" # pink +WARM = "#bd93f9" # purple +COOL = "#8be9fd" # cyan +DIM = "#6272a4" # muted comment-grey + +n = len(TITLE) +# Advance by 2 columns every tmux tick (status-interval 1 -> 2 cols/sec). +pos = (int(time.time()) * 2) % n + +def style_for(distance: int) -> str: + if distance == 0: + return f"#[fg={PEAK},bg={HOT},bold]" + if distance == 1: + return f"#[fg={HOT},bold]" + if distance == 2: + return f"#[fg={WARM},bold]" + if distance == 3: + return f"#[fg={COOL}]" + return f"#[fg={DIM}]" + +parts: list[str] = [] +for i, ch in enumerate(TITLE): + if ch == " ": + parts.append(" ") + continue + d = min((i - pos) % n, (pos - i) % n) + parts.append(style_for(d) + ch + "#[default]") + +print("".join(parts), end="") diff --git a/docker/scripts/workshop-hint b/docker/scripts/workshop-hint new file mode 100755 index 0000000..8b322f6 --- /dev/null +++ b/docker/scripts/workshop-hint @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# workshop-hint TOPIC — print a short colored hint card for one of the +# workshop tmux panes. Used from workshop-tmux.sh so each pane's seed is +# 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. + +import sys + +# Dracula-ish ANSI 256-colour indices, matched to the tmux theme. +PINK = "\x1b[38;5;213m" +CYAN = "\x1b[38;5;87m" +GREEN = "\x1b[38;5;156m" +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}") + 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"], + ), + "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", + ], + ), +} + +topic = sys.argv[1] if len(sys.argv) > 1 else "" +text = HINTS.get(topic) +if text is None: + sys.stderr.write(f"workshop-hint: unknown topic {topic!r}; " + f"expected one of {', '.join(HINTS)}\n") + sys.exit(2) +print(text) diff --git a/docker/scripts/workshop-spinner b/docker/scripts/workshop-spinner new file mode 100755 index 0000000..19d8346 --- /dev/null +++ b/docker/scripts/workshop-spinner @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# workshop-spinner: prints a 5-bar audio-EQ visualisation that snapshots +# itself every second. Each bar is a different phase of the same sine +# wave, so on each tmux refresh the column heights re-arrange and it +# looks like a small equaliser is bouncing. Designed for tmux's +# status-right with `status-interval 1`. +# +# Output: 5 colored block characters, e.g. `▃▆█▄▂`. + +import time +import math + +# Lower → taller block-fill glyphs (8 levels). +LEVELS = " ▁▂▃▄▅▆▇█" + +# Bar colors, fading from cool to hot — gives the bars a gradient look. +BAR_COLORS = ["#8be9fd", "#bd93f9", "#ff79c6", "#bd93f9", "#8be9fd"] + +t = time.time() * 3.0 # speed +bars: list[str] = [] +for i in range(5): + phase = i * 0.7 + val = (math.sin(t + phase) + 1.0) / 2.0 # normalise to [0, 1] + idx = int(val * (len(LEVELS) - 1)) + bars.append(f"#[fg={BAR_COLORS[i]},bold]{LEVELS[idx]}#[default]") + +print("".join(bars), end="") diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh new file mode 100755 index 0000000..40c6047 --- /dev/null +++ b/docker/scripts/workshop-tmux.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# workshop-tmux: launch a preconfigured tmux layout for the OSSNA 2026 +# PX4 + ROS 2 workshop. +# +# 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. +# +# ┌─────────────┬─────────────┐ +# │ 0: gazebo │ 1: px4 │ +# ├─────────────┤ │ +# │ 3: common │ 2: qgc │ +# ├─────────────┤ │ +# │ 4: example │ │ +# └─────────────┴─────────────┘ +# +# Each pane is pre-seeded with comment-only hint lines +# showing the command you would paste there. The script +# does not start anything for you — you still copy-paste +# the actual commands from the workshop docs, but now into +# ONE terminal window. +# +# 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. + +set -eu + +SESSION="ossna" + +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 + +# --- 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+ + +# 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. +tmux set -g mouse on + +# --- Dracula-inspired palette (synthwave-y, dev-friendly) --- +# bg #282a36 bg-dark #13111c +# pink #ff79c6 purple #bd93f9 cyan #8be9fd +# green #50fa7b yellow #f1fa8c orange #ffb86c +# red #ff5555 fg #f8f8f2 comment #6272a4 +tmux set -g status-style "bg=#13111c,fg=#f8f8f2" +tmux set -g message-style "bg=#ff79c6,fg=#13111c,bold" +tmux set -g pane-border-style "fg=#3a3a5a" # dim border for inactive +tmux set -g pane-active-border-style "fg=#ff79c6,bold" # hot pink for the active pane + +# Pane title in the border: hex-bullets in cyan + bold pink name. +# Active pane gets a brighter title via pane-active-border-style colour, the +# format string itself is the same for all panes. +tmux set -g pane-border-format " #[fg=#8be9fd,bold]⬢#[default] #[fg=#ff79c6,bold]#{pane_title}#[default] #[fg=#8be9fd,bold]⬢#[default] " + +# Window list in the status bar (powerline-ish flat segments) +tmux setw -g window-status-style "fg=#6272a4" +tmux setw -g window-status-current-style "fg=#13111c,bold,bg=#8be9fd" +tmux setw -g window-status-format " #I⋅#W " +tmux setw -g window-status-current-format " #I⋅#W " +tmux setw -g window-status-separator "" + +# Flash window name yellow when an inactive window has new output +tmux set -g monitor-activity on +tmux set -g visual-activity off +tmux setw -g window-status-activity-style "fg=#f1fa8c,bold,blink" + +# --- Animations --- +# status-left: synthwave title with a traveling "scanner" highlight that +# sweeps across the text twice per second (workshop-banner +# emits the tmux format string). +# status-right: 5-bar EQ visualiser + cyan workshop label + magenta clock. +tmux set -g status-left-length 60 +tmux set -g status-right-length 80 +tmux set -g status-left "#(workshop-banner) " +tmux set -g status-right "#[fg=#6272a4]┤ #(workshop-spinner) #[fg=#8be9fd,bold]workshop #[fg=#6272a4]│ #[fg=#bd93f9,bold]%H:%M:%S #[fg=#6272a4]├" + +# Make the prefix indicator obvious when prefix is held +tmux set -g status-keys vi + +# --- Easy-to-reach shortcuts (NO PREFIX needed) ----------------------------- +# Some terminals (notably VSCode's built-in one) eat tmux's mouse events or +# block Ctrl-b. Bind a few common navigations to bare Alt-something so they +# work everywhere. +tmux bind -n M-1 select-window -t 0 # Alt+1 -> sim window +tmux bind -n M-2 select-window -t 1 # Alt+2 -> scratch window +tmux bind -n M-Left previous-window # Alt+Left -> prev window +tmux bind -n M-Right next-window # Alt+Right -> next window +tmux bind -n M-h select-pane -L # Alt+h/j/k/l -> move pane +tmux bind -n M-j select-pane -D +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. + +# Pane 0 is the existing pane we got from new-session. +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 the left column (gazebo) vertically → new pane below for common. +COMMON_PANE="$(tmux split-window -v -p 67 -t "${GZ_PANE}" -PF '#{pane_id}')" + +# Split the common pane vertically → new pane below = example launch. +EXAMPLE_PANE="$(tmux split-window -v -p 50 -t "${COMMON_PANE}" -PF '#{pane_id}')" + +# 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 + +# 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 +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" +tmux select-pane -t "${SESSION}:sim.0" +exec tmux attach -t "${SESSION}" diff --git a/docker/scripts/workshop-welcome b/docker/scripts/workshop-welcome new file mode 100755 index 0000000..fcc78ad --- /dev/null +++ b/docker/scripts/workshop-welcome @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# workshop-welcome: prints a colored Tux + tech-stack badge for the OSSNA +# 2026 workshop. Run inside the scratch tmux window so attendees see it on +# arrival. + +import sys + +# Dracula-inspired ANSI 256 colour indices (close matches to the hex +# palette workshop-tmux.sh uses for tmux itself): +PINK = "\x1b[38;5;213m" # ~#ff79c6 +PURPLE = "\x1b[38;5;141m" # ~#bd93f9 +CYAN = "\x1b[38;5;87m" # ~#8be9fd +GREEN = "\x1b[38;5;156m" # ~#50fa7b +YELLOW = "\x1b[38;5;228m" # ~#f1fa8c +DIM = "\x1b[38;5;245m" +BOLD = "\x1b[1m" +RESET = "\x1b[0m" +SEP = f"{DIM}::{RESET}" + + +def line(s: str) -> None: + sys.stdout.write(s + "\n") + + +line("") +line(f"{PINK} .--.{RESET}") +line(f"{PINK} |o_o | {CYAN}{BOLD}░▒▓ OSS NA 2026 · Hands-On Aerial Robotics ▓▒░{RESET}") +line(f"{PINK} |:_/ | {PURPLE}tmux session{RESET} {SEP} {PURPLE}ossna{RESET} {SEP} {PURPLE}prefix = Ctrl-b{RESET} {SEP} {PURPLE}? for help{RESET}") +line(f"{PINK} // \\ \\ {GREEN}stack{RESET} {SEP} {GREEN}PX4 v1.16{RESET} {SEP} {GREEN}ROS 2 Humble{RESET} {SEP} {GREEN}Gazebo Harmonic{RESET} {SEP} {GREEN}QGC v5.0.8{RESET}") +line(f"{PINK} (| | ) {YELLOW}scratch{RESET} {SEP} {YELLOW}use this window for ros2 topic echo / node list / vim / nano{RESET}") +line(f"{PINK} /'\\_ _/`\\{RESET}") +line(f"{PINK} \\___)=(___/{RESET}") +line("") +line(f" {DIM}┌─ tmux quick reference ─────────────────────────────────────────────────────{RESET}") +line(f" {DIM}│{RESET} {GREEN}Alt+1{RESET} / {GREEN}Alt+2{RESET} switch to window 1 (sim) / 2 (scratch) {DIM}— no prefix{RESET}") +line(f" {DIM}│{RESET} {GREEN}Alt+←{RESET} / {GREEN}Alt+→{RESET} previous / next window {DIM}— no prefix{RESET}") +line(f" {DIM}│{RESET} {GREEN}Alt+h{RESET} {GREEN}Alt+j{RESET} {GREEN}Alt+k{RESET} {GREEN}Alt+l{RESET} move focus between panes (vim arrows) {DIM}— no prefix{RESET}") +line(f" {DIM}│{RESET} {GREEN}Alt+z{RESET} zoom current pane to full screen {DIM}— no prefix{RESET}") +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}") +line("") diff --git a/docs/setup.md b/docs/setup.md index e55ceee..9e2b21e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -107,6 +107,130 @@ To use the DevContainers, simply open the workshop repo in VSCode, then type `CT 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.