BROS2 (Block ROS2) is an Electron desktop environment for building, simulating, and introspecting ROS2 graphs with a drag-and-drop block interface. It streamlines going from idea to runnable robot behavior by generating ROS packages, launch files, and providing live insight into running nodes.
Basically, BROS2 (Block ROS2) is an Electron desktop app that lets you assemble ROS2 graphs visually and run them through a managed Docker workspace 🦾🤖
- Visual composition of ROS 2 nodes, topics, and services without leaving the editor.
- Automatic generation of package scaffolding and launch files from the block graph.
- Integrated simulation hooks (Gazebo, Isaac) and telemetry panels for rapid iteration.
- Cross-platform desktop app distributed via Electron so teams share a single workflow.
- macOS (Apple Silicon or Intel) or Linux with Docker Desktop / Docker Engine running.
- Git, curl, and bash (used by the bootstrap script).
- Internet access to download Node, pnpm, and Electron during setup.
Verify Docker access before continuing:
docker ps-
Select Node 20.19.x
source ~/.nvm/nvm.sh nvm use 20.19.0
-
Refresh deps (clean if things feel stale):
pnpm install -r pnpm -r clean # optional, only if builds seem out of dateIf you did clean, run the workspace builds in the Daily Development section (step 3) before moving on below.
-
Build the ROS image once per machine (safe to rerun):
pnpm --filter ./apps/desktop-app ros:build-image
-
Regenerate the Electron main + preload bundle (needed after a clean):
pnpm --filter ./apps/desktop-app build:main
-
Launch the desktop dev stack (Electron main + Vite renderer):
pnpm --filter ./apps/desktop-app dev
-
In Electron DevTools, bring up ROS 2 + rosbridge when you need it (backgrounded so the promise resolves). In the Workspace header you can also click Start ROS to run these for you:
await window.runner.up("turtlesim");
await window.runner.exec('bash -lc "source /opt/ros/humble/setup.bash && nohup ros2 launch rosbridge_server rosbridge_websocket_launch.xml >/tmp/rosbridge.log 2>&1 & echo $!"');
await window.runner.exec('bash -lc "source /opt/ros/humble/setup.bash && nohup ros2 run turtlesim turtlesim_node >/tmp/turtlesim.log 2>&1 & echo $!"'); Skip to the sections below for the full workflow details and tests.
git clone https://github.com/nhathout/BROS2.git
cd BROS2
./apps/desktop-app/scripts/bootstrap.shThe bootstrap script installs or activates:
nvm(if missing) and Node20.19.0.- pnpm
10.17.1via Corepack (or npm fallback). - Workspace dependencies with
pnpm install -r. - Electron binaries and a first build of the desktop app.
It launches the packaged app once everything compiles. If the script adds an nvm use snippet to your shell profile, open a new terminal so pnpm is on your PATH next time.
-
Select Node 20.19.x (every new shell resets your
nvmversion):source ~/.nvm/nvm.sh nvm use 20.19.0
-
Refresh dependencies after pulling changes (rerun
pnpm -r cleanfirst if builds look stale):pnpm install -r
-
Build the workspace libraries (run this after changing any of these packages so their
.d.tsfiles stay fresh for Electron main):pnpm --filter @bros2/runtime build pnpm --filter @bros2/shared build pnpm --filter @bros2/ui build pnpm --filter @bros2/validation build pnpm --filter @bros2/runner build
-
Keep the ROS runner image up to date (once per machine, rerun after touching
packages/services/runner/images/ros2-humble):pnpm --filter ./apps/desktop-app ros:build-image
(This rebuild pulls rosbridge, turtlesim, rqt_graph, and graphviz into the runner image.)
-
Emit the desktop main + preload bundle (run from the repo root).
Do not skip this step after runningpnpm -r clean; it regenerates the preload bridges and the runtime registry that powerwindow.runtime.pnpm --filter ./apps/desktop-app build:main
(Optional) Build the renderer bundle for production checks — also from the repo root:
pnpm --filter ./apps/desktop-app build:renderer
-
Start the dev environment (Electron main + Vite renderer):
pnpm --filter ./apps/desktop-app dev
Keep this process running while you iterate. You can still
cd apps/desktop-app && pnpm dev, but the filtered form avoids path mistakes. After Electron opens, pop open DevTools and runtypeof window.runtime—it should log"object"if the preload bridges built correctly.
If Electron complains about missing binaries, reinstall them once:
cd apps/desktop-app
node node_modules/electron/install.jsThe preload bridge exposes window.runner for Docker-backed ROS 2 sessions and window.ir for graph validation.
window.runner.up(projectName: string): Promise<void>;
window.runner.exec(command: string): Promise<{ stdout: string; stderr: string; code: number }>;
window.runner.down(): Promise<void>;
window.ir.build(graph: BlockGraph): Promise<{ ir: IR; issues: string[] }>;
window.ir.validate(ir: IR): Promise<{ errors: Issue[]; warnings: Issue[] }>;Renderer runtime nodes (ArrowKeyPub, ConsoleSub, TurtleSimSub, etc.) live in the renderer process. They forward data over rosbridge but do not yet spawn real ROS 2 processes, so tools like rqt_graph will show rosbridge_websocket as the publisher until codegen/launch support is added.
With Docker running and the app in dev mode, open DevTools (View → Toggle Developer Tools) and run:
await window.runner.up("hello_ros");
await window.runner.exec("ros2 --help");
await window.runner.exec("ros2 pkg list | head -n 5");
await window.runner.down();This spins up the bros2_hello_ros container defined in Projects/hello_ros and exercises the ROS 2 CLI.
const graph = {
blocks: [
{ kind: "node", id: "talker", name: "talker" },
{ kind: "publish", nodeId: "talker", topic: "/chatter", type: "std_msgs/msg/String" },
],
};
const { ir, issues } = await window.ir.build(graph);
const { errors, warnings } = await window.ir.validate(ir);
console.log({ issues, errors, warnings });Start rosbridge and turtlesim inside the runner, then wire arrow keys to /turtle1/cmd_vel:
await window.runner.up("turtlesim");
await window.runner.exec('bash -lc "source /opt/ros/humble/setup.bash && nohup ros2 launch rosbridge_server rosbridge_websocket_launch.xml >/tmp/rosbridge.log 2>&1 & echo $!"');
await window.runner.exec('bash -lc "source /opt/ros/humble/setup.bash && nohup ros2 run turtlesim turtlesim_node >/tmp/turtlesim.log 2>&1 & echo $!"');
const pubId = window.runtime.create("ArrowKeyPub", { topic: "keys/arrows" });
const turtleId = window.runtime.create("TurtleSimSub", {
inputTopic: "keys/arrows",
cmdVelTopic: "/turtle1/cmd_vel",
});
window.runtime.start(pubId);
window.runtime.start(turtleId);
// Optional: echo the bus traffic
const logId = window.runtime.create("ConsoleSub", { topic: "keys/arrows" });
window.runtime.start(logId);Press arrow keys with the Electron window focused to drive the turtle. Stop the background processes with:
await window.runner.exec('bash -lc "pkill -f rosbridge_websocket || true; pkill -f turtlesim_node || true"');
await window.runner.down();The preload now exposes window.runtime alongside the runner and IR bridges. With the dev app running:
typeof window.runtime; // "object"
const pubId = window.runtime.create("ArrowKeyPub", { topic: "keys/arrows" });
const subId = window.runtime.create("ConsoleSub", { topic: "keys/arrows" });
window.runtime.start(pubId);
window.runtime.start(subId);
// Press arrow keys while the Electron window is focused:
// [publish] keys/arrows <- { key: "left", ts: ... }
// [node:ConsoleSub_1] received from ArrowKeyPub_1: {"key":"left","ts":...}
window.runtime.stop(subId);
window.runtime.stop(pubId);
window.runtime.list(); // ["ArrowKeyPub_1", "ConsoleSub_1"]If window.runtime is missing, run pnpm --filter ./apps/desktop-app build:main again to regenerate the preload bridges.
-
window.runner.up(projectName)– start/update the Docker containerbros2_<projectName>backed bybros2/ros2-humble:latest. -
window.runner.exec(command)– run commands likeros2 topic listor launch background services:await window.runner.exec('bash -lc "source /opt/ros/humble/setup.bash && nohup ros2 launch rosbridge_server rosbridge_websocket_launch.xml >/tmp/rosbridge.log 2>&1 & echo $!"');
-
window.runner.down()– stop/remove the ROS 2 container. -
window.runtime.create(type, config)– instantiate nodes registered inapps/desktop-app/src/renderer/runtime/registry.ts(ArrowKeyPub,ConsoleSub,TurtleSimSub, plus plumbing nodesRosbridgeBridgeandForwarder). -
window.runtime.start(id),window.runtime.stop(id),window.runtime.stopAll()– control renderer-runtime nodes. -
window.ir.build(...)/window.ir.validate(...)– convert block graphs to IR and run validators. -
globalThis.__rosbridge__– dev-only handle populated by rosbridge-aware nodes with helpers likepublishRos(topic, msg).
-
Remove build outputs everywhere (this clears
dist/folders andtsconfig.main.tsbuildinfo, ensuring the desktop main bundle re-emitsdist/main.js):pnpm -r clean
-
Reset dependencies if things get out of sync:
pnpm store prune # optional rm -rf node_modules pnpm install -r -
Rebuild the workspaces and desktop app using the daily workflow above. When
build:maincompletes you should have:apps/desktop-app/dist/main.js apps/desktop-app/dist/preload.js apps/desktop-app/dist/remote/runtime-bridge.cjs apps/desktop-app/dist/renderer/runtime/registry.js -
(Optional) Produce installers:
pnpm -r build
apps/desktop-app/README.md– ROS 2 quickstart snippet, DevTools walkthrough, and desktop-specific scripts.packages/services/runner/images/ros2-humble/README.md– maintenance notes for the Docker image used bywindow.runner.
- Keep Docker running whenever you use
window.runner.*; the runner manages containers inProjects/. - If
pnpm devfails because Electron is missing, re-runnode node_modules/electron/install.js. - Rerun the bootstrap script after major Node/pnpm upgrades—it is idempotent and safe to run again.
