Skip to content
Merged
14 changes: 14 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
3 changes: 2 additions & 1 deletion docker/scripts/install_deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
43 changes: 43 additions & 0 deletions docker/scripts/workshop-banner
Original file line number Diff line number Diff line change
@@ -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="")
77 changes: 77 additions & 0 deletions docker/scripts/workshop-hint
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 27 additions & 0 deletions docker/scripts/workshop-spinner
Original file line number Diff line number Diff line change
@@ -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="")
165 changes: 165 additions & 0 deletions docker/scripts/workshop-tmux.sh
Original file line number Diff line number Diff line change
@@ -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}"
44 changes: 44 additions & 0 deletions docker/scripts/workshop-welcome
Original file line number Diff line number Diff line change
@@ -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("")
Loading
Loading