Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,44 @@ UBEROS_AUTH=off
UBEROS_SERVICES=ros,gazebo,editor,frontend

# --- Simulators (PRD Simulation & Visualization, Themes A/B/D) ---
# Simulator services (gazebo, turtlesim) live under the compose `simulators`
# profile so they are only created when the profile is active. Setting
# COMPOSE_PROFILES=simulators here makes a plain `docker compose up` bring BOTH
# simulators up by default (FR-B8). Clear it to define them without starting any,
# then Launch each from the Simulators menu on demand.
# Two coordinated knobs decide which simulators exist and appear:
#
# 1. COMPOSE_PROFILES — BUILD-TIME selection (Theme D, FR-D1..D3). Chooses
# which simulator IMAGES build and which SERVICES compose creates. Each
# simulator has its own profile plus the umbrella `simulators` (= all):
# gazebo -> profiles: simulators, sim-gazebo
# turtlesim -> profiles: simulators, sim-turtlesim
# A simulator is built/created only when one of its profiles is active.
# 2. UBEROS_SIMULATORS — REGISTRY/MENU gating (FR-A1). Chooses which entries
# the control plane exposes at GET /simulators, i.e. what the menu shows.
#
# Keep the two IN TANDEM so an excluded simulator disappears from BOTH the built
# services AND the menu (FR-D3). Default below installs and shows BOTH (FR-D2):
# COMPOSE_PROFILES=simulators # umbrella = every simulator
# UBEROS_SIMULATORS (unset) # registry default = whole catalog
# To install a SUBSET, list the matching profile(s) and ids together, e.g.:
# Gazebo only: COMPOSE_PROFILES=sim-gazebo + UBEROS_SIMULATORS=gazebo
# Turtlesim only: COMPOSE_PROFILES=sim-turtlesim + UBEROS_SIMULATORS=turtlesim
# Both (explicit): COMPOSE_PROFILES=sim-gazebo,sim-turtlesim
# Verify a selection with: docker compose config --services
#
# Setting COMPOSE_PROFILES=simulators here also makes a plain `docker compose up`
# bring BOTH simulators up by default (FR-B8). Clear it to define none, then
# Launch each from the Simulators menu on demand.
COMPOSE_PROFILES=simulators

# Which installed simulators the control plane auto-starts at stack up (FR-B8).
# Comma-separated simulator ids; when unset or empty, the registry's per-simulator
# `autostart` flag applies (default: both gazebo and turtlesim on). Set to a
# subset (e.g. `turtlesim`) to auto-start only those — the rest are created but
# left stopped until launched from the Simulators menu. Requires the `simulators`
# profile (above) to be active.
# left stopped until launched from the Simulators menu. Requires the matching
# build profile(s) above to be active.
# UBEROS_SIMULATORS_AUTOSTART=gazebo,turtlesim

# Which simulators are installed/exposed in the registry (FR-A1). Comma-separated
# ids; defaults to the whole catalog (gazebo, turtlesim). Unknown ids are dropped.
# Set this to MATCH the COMPOSE_PROFILES selection above (see the tandem examples)
# so the menu never offers a simulator whose service was not created (FR-D3).
# UBEROS_SIMULATORS=gazebo,turtlesim

# --- Intel GPU overlay (compose.override.intel.yaml) ---
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ workspace/install/
workspace/log/
.env
build*.log

.playwright-mcp/
59 changes: 51 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ standard web browser with no local install beyond Docker.
docker compose up
```

Then open <http://localhost:8080>. The window-manager canvas loads with four
panels: Simulator (noVNC), Terminal, Code Editor, and ROS Status.
Then open <http://localhost:8080>. The window-manager canvas loads with
Terminal, Code Editor, and ROS Status panels, plus a **Simulators** menu to
launch Gazebo (rendered in the browser via gzweb) or Turtlesim on demand.

To run detached:

Expand All @@ -39,12 +40,15 @@ docker compose down
| `proxy` | Single ingress (Nginx); the only host-published port | 8080 |
| `frontend` | Svelte + Golden Layout window manager | 3000 |
| `ros` | ROS 2 middleware, rosbridge, ttyd terminals, colcon | 9090, 7681 |
| `simulator` | Gazebo + Xvfb (software rendering by default) | 5900, 6080 |
| `vnc` | x11vnc + noVNC sidecar (shares the simulator namespace) | 5900, 6080 |
| `gazebo` | Headless `gz sim -s` + gz-launch WebsocketServer (scene-state stream) + `ros_gz` bridge | 9002 |
| `gzweb-client` | Self-hosted gzweb web client (Three.js), served at `/gzweb/` | 3000 |
| `turtlesim` | Turtlesim + Xvfb + x11vnc + noVNC (GUI simulator over VNC) | 5900, 6080 |
| `editor` | code-server on the shared ROS workspace | 8443 |
| `control` | Operational control plane (per-service reset, workspace config) | 9000 |
| `control` | Operational control plane (service reset, simulator launch/stop, config) | 9000 |
| `discovery-server` | Fast DDS discovery (removes multicast dependency) | 11811 |

The simulator services (`gazebo`, `gzweb-client`, `turtlesim`) run under the
`simulators` compose profile (see [Simulator selection](#simulator-selection-build-time)).
Only the proxy port is published to the host. Backend ports are reachable only
through the proxy.

Expand All @@ -59,7 +63,44 @@ Settings live in `.env` (committed defaults contain no secrets):
| `UBEROS_PORT` | `8080` | Host port for the proxy |
| `ROS_DOMAIN_ID` | `42` | DDS domain (cross-platform-safe range) |
| `UBEROS_AUTH` | `off` | Set to `basic` to enable proxy authentication |
| `UBEROS_SERVICES` | `ros,simulator,vnc,editor,frontend` | Services the system menu may reset |
| `UBEROS_SERVICES` | `ros,gazebo,editor,frontend` | Services the system menu may reset |
| `NPM_REGISTRY` | public npm | npm registry for image builds; point at a proxy in `.env` |

### Simulators (runtime)

Simulators are launched on demand from the **Simulators** menu and run
concurrently, each as its own container; they survive a browser reload (the
panel reconnects to the still-running simulator). By default both start with the
stack (`COMPOSE_PROFILES=simulators`) and can be stopped/relaunched from the menu.

| Simulator | Visualization | ROS integration |
|---|---|---|
| **Gazebo** | Browser 3D via `gzweb` (headless `gz sim -s` streams scene state over a WebSocket; no VNC) | `ros_gz` bridge, `/clock` bridged by default |
| **Turtlesim** | noVNC (Xvfb + x11vnc GUI window) | Native ROS 2 node (`/turtle1/cmd_vel`, `/turtle1/pose`) |

All simulators join the shared `ROS_DOMAIN_ID` through the Fast DDS discovery
server (no multicast).

### Simulator selection (build-time)

Which simulators build and appear is controlled by two coordinated settings in
`.env` (defaults install and show **both** Gazebo and Turtlesim):

- `COMPOSE_PROFILES` — build-time selection. Each simulator has its own compose
profile (`sim-gazebo`, `sim-turtlesim`) plus the umbrella `simulators` (all).
Only a simulator whose profile is active is built and created as a service.
- `UBEROS_SIMULATORS` — registry/menu gating; which entries the menu offers.

Set them in tandem so an excluded simulator drops from both the built services
and the menu:

| Selection | `COMPOSE_PROFILES` | `UBEROS_SIMULATORS` |
|---|---|---|
| Both (default) | `simulators` | *(unset)* |
| Gazebo only | `sim-gazebo` | `gazebo` |
| Turtlesim only | `sim-turtlesim` | `turtlesim` |

Verify a selection with `docker compose config --services`.

### GPU acceleration (opt-in)

Expand Down Expand Up @@ -91,7 +132,8 @@ path is used there. Load only one GPU overlay at a time.
1. Edit a package in the **Code Editor** panel (`workspace/src/`).
2. In a **Terminal** panel: `cd /ros_ws && colcon build --symlink-install`.
3. `source install/setup.bash`, then `ros2 run <pkg> <node>`.
4. Observe the result in the **Simulator** and **ROS Status** panels.
4. Launch a simulator from the **Simulators** menu (Gazebo web view or
Turtlesim) and observe the result there and in the **ROS Status** panel.

### Terminal copy and paste

Expand Down Expand Up @@ -121,7 +163,8 @@ stored credentials and forces re-authentication.

- Project brief: [docs/specs/01-Init.md](docs/specs/01-Init.md)
- Research report: [docs/specs/01-Init-research.md](docs/specs/01-Init-research.md)
- PRD: [docs/prds/uberos-init.md](docs/prds/uberos-init.md)
- PRD (init): [docs/prds/uberos-init.md](docs/prds/uberos-init.md)
- Simulation & Visualization: [BRD](docs/brds/uberos-simulation-visualization-brd.md) · [PRD](docs/prds/uberos-simulation-visualization.md)
- Decisions: [docs/decisions/](docs/decisions/)

> **Note:** The primary ROS distribution (Kilted) passed SPIKE-A image and
Expand Down
51 changes: 42 additions & 9 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,19 @@ services:
# turtlesim); the proxy therefore must not hard-depend on it, and /gzweb/ws/
# resolves gazebo lazily. On ros_net (DDS discovery, for the Theme E bridge)
# and web_net (proxy). No host-published ports (INV-04, WP-13).
#
# Build-time selection (Theme D, FR-D1): two profiles gate this service — the
# umbrella `simulators` (all sims at once) and its own `sim-gazebo` (Gazebo
# only). COMPOSE_PROFILES picks the set, so `sim-gazebo` builds/creates just
# Gazebo while `sim-gazebo,sim-turtlesim` (or the umbrella `simulators`) yields
# both. Set UBEROS_SIMULATORS in tandem so the menu matches (see .env.example).
gazebo:
build:
context: ./services/gazebo
args:
GZ_RELEASE: ${GZ_RELEASE:-ionic}
ROS_DISTRO: ${ROS_DISTRO:-kilted}
profiles: ["simulators", "sim-gazebo"]
depends_on:
discovery-server:
condition: service_healthy
Expand Down Expand Up @@ -127,15 +134,21 @@ services:
# Turtlesim visualizer (PRD Theme C). A lightweight, ROS-native second
# simulator: Xvfb + turtlesim_node + openbox + x11vnc + websockify in one
# container, streamed to the browser over noVNC at /sim/turtlesim/novnc/.
# Under the `simulators` profile so it is defined but launched on demand (the
# full build-selection matrix + auto-start is Theme D). On ros_net for the DDS
# discovery server and web_net for the proxy. No host-published ports.
# On ros_net for the DDS discovery server and web_net for the proxy. No
# host-published ports.
#
# Build-time selection (Theme D, FR-D1): two profiles gate this service — the
# umbrella `simulators` (all sims at once) and its own `sim-turtlesim`
# (Turtlesim only). COMPOSE_PROFILES picks the set, so `sim-turtlesim` builds/
# creates just Turtlesim while `sim-gazebo,sim-turtlesim` (or the umbrella
# `simulators`) yields both. Set UBEROS_SIMULATORS in tandem so the menu
# matches (see .env.example).
turtlesim:
build:
context: ./services/turtlesim
args:
ROS_DISTRO: ${ROS_DISTRO:-kilted}
profiles: ["simulators"]
profiles: ["simulators", "sim-turtlesim"]
depends_on:
discovery-server:
condition: service_healthy
Expand Down Expand Up @@ -205,6 +218,29 @@ services:
retries: 5
start_period: 10s

# Built gzweb 3D client (PRD Theme F, FR-F3). A minimal single-page client that
# bundles the gazebo-web `gzweb` SceneManager + Three.js + protobufjs (fetched
# at build time through $NPM_REGISTRY, like the frontend image) and RENDERS the
# Gazebo scene state streamed from gazebo:9002 — no VNC, no server GL. Nginx
# serves the built assets at /gzweb/. Deliberately ALWAYS-ON (no simulators
# profile) so the panel shell at /gzweb/ loads even when the on-demand gazebo
# container is down; only the /gzweb/ws/ upgrade returns 502 until gazebo is up.
# No host-published ports — the proxy fronts it at /gzweb/ (INV-04, WP-13).
gzweb-client:
build:
context: ./services/gazebo/client
args:
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
networks:
- web_net
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:3000/healthz >/dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s

control:
build:
context: ./services/control
Expand Down Expand Up @@ -250,18 +286,15 @@ services:
condition: service_healthy
frontend:
condition: service_healthy
gzweb-client:
condition: service_healthy
control:
condition: service_healthy
ports:
- "${UBEROS_PORT:-8080}:8080"
volumes:
- ./services/proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./config/nginx:/etc/nginx/auth:ro
# Self-hosted gzweb static client (Theme F, FR-F2), served by nginx at
# /gzweb/. Mounted here (not baked into the gazebo image) so the panel
# shell is ALWAYS available even when the on-demand gazebo container is
# down; only the /gzweb/ws/ upgrade returns 502 until gazebo is up.
- ./services/gazebo/client:/usr/share/nginx/gzweb:ro
networks:
- web_net
- ros_net
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/ADR-009-display-transport.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ADR-009: Simulator Display Transport and GPU Rejection for the Interactive Path

- Status: Accepted — Implemented (software display + noVNC; GPU rejected for the interactive path; damage-aware transport pending, AE-P1)
- Implementation: `services/simulator` (Xvfb `:99`, `LIBGL_ALWAYS_SOFTWARE=1`) + `services/vnc` (x11vnc/noVNC sidecar sharing the sim netns)
- Status: Accepted — Implemented (Gazebo via gzweb scene-state streaming; Turtlesim via software display + noVNC; GPU rejected for the interactive path; damage-aware transport superseded by gzweb for Gazebo, AE-P1)
- Implementation: `services/gazebo` (headless `gz sim -s` + gz-launch WebsocketServer on :9002) streamed to the self-hosted `services/gazebo/client` (gzweb SceneManager + Three.js) served by `services/gzweb-client`, wired through the proxy at `/gzweb/` + `/gzweb/ws/`; `services/turtlesim` (Xvfb + turtlesim_node + x11vnc + websockify) still uses noVNC at `/sim/turtlesim/novnc/`. The retired `services/simulator` (Xvfb `gz sim`) + `services/vnc` noVNC-for-Gazebo path and the `/novnc` route are gone.
- Date: 2026-07-19
- Deciders: jmservera (product), Neo, Trinity (technical)
- Related: PRD FR-E1..E3 (G-006), BRD Theme E (BR-GPU-1..3), spike PR #5, issue #12, docs/specs/04-gazebo-gpu-wsl2-research.md, ADR-002 (proxy)
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ a PR lands (or removes) the code that realizes an ADR, update that ADR's
| [006](ADR-006-resilient-ingress.md) | Resilient Ingress & Health Visibility | Proposed | Not built; Theme G design gate |
| [007](ADR-007-discovery-server.md) | DDS Discovery Server | Implemented | `discovery-server` + `dds_discovery.xml` (since Init) |
| [008](ADR-008-control-plane.md) | Operational Control Plane & Docker Socket | Implemented | `services/control/server.js`; socket-proxy hardening pending (AE-S1) |
| [009](ADR-009-display-transport.md) | Simulator Display Transport & GPU Rejection | Implemented | `services/simulator` + `services/vnc`; GPU rejected for interactive path; AE-P1 pending |
| [009](ADR-009-display-transport.md) | Simulator Display Transport & GPU Rejection | Implemented | Gazebo via gzweb (`services/gazebo` WebsocketServer → `services/gzweb-client` Three.js client, `/gzweb/` + `/gzweb/ws/`); Turtlesim via noVNC (`services/turtlesim`, `/sim/turtlesim/novnc/`); GPU rejected for interactive path; retired `simulator`+`vnc` noVNC-for-Gazebo path |
| [010](ADR-010-persistence.md) | Persistence & Volume Strategy | Partially implemented | editor volumes done; server-side config store (Theme C) done via `control` GET/PUT `/config/settings` → `control-data` volume; backup/restore (AE-R2) & migration (AE-O2) pending |

## Adding a new ADR
Expand Down
23 changes: 23 additions & 0 deletions docs/plans/sim-theme-d-build-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Theme D — Build-time simulator selection

> Plan stub. Source of truth: [Simulation & Visualization PRD](../prds/uberos-simulation-visualization.md) §7.4.

## Scope — FR-D1 … FR-D4
- FR-D1 — Build/compose config (compose profiles + build args, e.g. `UBEROS_SIMULATORS`) selects which simulator images build and which services are created.
- FR-D2 — Default install set is **Gazebo + Turtlesim**.
- FR-D3 — Excluding a simulator keeps its image/service out of the build and its registry entry out of the menu.
- FR-D4 — Documented in `.env`/compose comments (default + how to change).

## Dependency / lane
- **Lane 4 (integrate last).** Depends on **Theme A** (registry) and the simulator services from **Themes C and F** existing.

## Likely files
- `compose.yaml` (profiles, build args)
- `.env` / compose comments, README/docs

## Tasks
- [ ] Research: compose profiles + created-but-selectable services; registry `enabled` gating
- [ ] Plan: selection mechanism + defaults + docs
- [ ] Implement: profiles/args + registry gating
- [ ] Tests: include/exclude a simulator changes services + menu
- [ ] Acceptance (PRD §7.4)
3 changes: 1 addition & 2 deletions services/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import 'golden-layout/dist/css/goldenlayout-base.css';
import 'golden-layout/dist/css/themes/goldenlayout-dark-theme.css';
import {
buildSimulatorPanel,
buildTerminalPanel,
buildEditorPanel,
buildRosStatusPanel,
Expand Down Expand Up @@ -78,7 +77,6 @@
let configError = '';

const factories = {
simulator: buildSimulatorPanel,
terminal: buildTerminalPanel,
editor: buildEditorPanel,
'ros-status': buildRosStatusPanel,
Expand Down Expand Up @@ -841,6 +839,7 @@
<span class="svc-name">{sim.label}</span>
<span class="sim-state">{sim.state}</span>
{#if sim.state === 'running' || sim.state === 'starting'}
<button class="svc-reset" on:click={() => { openSimulatorPanel(sim); closeMenu(); }}>Open</button>
<button class="svc-reset" disabled={simBusy === sim.id} on:click={() => stopSim(sim.id)}>
{simBusy === sim.id ? '…' : 'Stop'}
</button>
Expand Down
Loading
Loading