# EMOS Documentation -- Context for AI Agents
You are an expert EMOS recipe developer. EMOS (The Embodied Operating System) is a unified orchestration layer for Physical AI that combines EmbodiedAgents (intelligence) and Kompass (navigation) into a single framework.
## How to Write an EMOS Recipe
An EMOS Recipe is a pure Python script that defines a robot behavior. When writing recipes, follow these principles:
1. **Define Topics** -- Declare ROS2 topics as `Topic(name=..., msg_type=...)` for inter-component communication. Match `msg_type` to your data (String, Image, Audio, Detections, etc.).
2. **Configure Clients & Models** -- Create a model client (OllamaClient, GenericHTTPClient, LeRobotClient, etc.) with a model wrapper. Clients are interchangeable -- swap inference backends without changing component logic.
3. **Build Components** -- Instantiate components (LLM, VLM, VLA, SpeechToText, TextToSpeech, Vision, Memory, Cortex, SemanticRouter) with inputs, outputs, and a model_client. Set `trigger` to control when the component executes. Use `Memory` for spatio-temporal memory and `Cortex` as an agentic harness that auto-discovers the rest of the graph as LLM tools.
4. **Wire Navigation** -- For mobile robots, configure a `RobotConfig` and instantiate Kompass components (Planner, Controller, DriveManager) with appropriate algorithms (DWA, PurePursuit, etc.).
5. **Add Events & Fallbacks** -- Use `on_fail()` per component for restart-style recovery, `launcher.on_process_fail()` for process-level crash recovery, and custom event/action pairs for runtime adaptivity. Events can trigger model swaps, component restarts, or arbitrary callbacks.
6. **Launch** -- Use `Launcher()` to add component packages with `launcher.add_pkg(components=[...], multiprocessing=True)` and call `bringup()`. The `multiprocessing` flag goes on `add_pkg`, not on the Launcher constructor.
The documentation below is ordered as a curriculum: architecture first, then components and APIs, then example recipes of increasing complexity.
---
## File: overview.md
```markdown
# EMOS -- The Embodied Operating System
**The open-source unified orchestration layer for Physical AI.**
EMOS transforms robots into Physical AI Agents. It provides a hardware-agnostic runtime that lets robots **see**, **think**, **move**, and **adapt** -- all orchestrated from pure Python scripts called Recipes.
Write a _Recipe_ once, deploy it on any robot -- from wheeled AMRs to humanoids -- without rewriting code.
:::{image} _static/images/diagrams/emos_robot_stack_light.png
:align: center
:width: 70%
:class: light-only
:::
:::{image} _static/images/diagrams/emos_robot_stack_dark.png
:align: center
:width: 70%
:class: dark-only
:::
Get Started •
Why EMOS? •
View on GitHub
---
## What You Can Build
::::{grid} 1 2 2 2
:gutter: 3
:::{grid-item-card} {material-regular}`psychology;1.2em;sd-text-primary` Intelligent Agents
Wire together vision, language, speech, and memory components into **agentic workflows**. Route queries by intent, answer questions about the environment, or build a semantic map -- all from a single Python script.
[See cognition recipes](recipes/foundation/index) {material-regular}`arrow_forward;0.9em`
:::
:::{grid-item-card} {material-regular}`route;1.2em;sd-text-primary` Autonomous Navigation
**GPU-accelerated** planning and control for real-world mobility. Point-to-point navigation, path recording, and vision-based target following -- across differential drive, Ackermann, and omnidirectional platforms.
[See navigation recipes](recipes/navigation/index) {material-regular}`arrow_forward;0.9em`
:::
:::{grid-item-card} {material-regular}`sync_alt;1.2em;sd-text-primary` Runtime Adaptivity
**Event-driven** architecture lets agents reconfigure themselves at runtime. Hot-swap ML models on network failure, switch navigation algorithms when stuck, trigger recovery maneuvers from sensor events, or compose complex behaviors with logic gates.
[See adaptivity recipes](recipes/events-and-resilience/index) {material-regular}`arrow_forward;0.9em`
:::
:::{grid-item-card} {material-regular}`precision_manufacturing;1.2em;sd-text-primary` Planning & Manipulation
Use **VLMs** for high-level task decomposition and **VLAs** for end-to-end manipulation. Closed-loop control where a VLM referee stops actions on visual task completion.
[See manipulation recipes](recipes/planning-and-manipulation/index) {material-regular}`arrow_forward;0.9em`
:::
::::
---
## What's Inside
EMOS is built on three open-source components:
| Component | Role |
| :--- | :--- |
| **[EmbodiedAgents](https://github.com/automatika-robotics/embodied-agents)** | Intelligence layer -- agentic graphs of ML models with semantic memory and event-driven reconfiguration |
| **[Kompass](https://github.com/automatika-robotics/kompass)** | Navigation layer -- GPU-powered planning and control for real-world mobility |
| **[Sugarcoat](https://github.com/automatika-robotics/sugarcoat)** | Architecture layer -- event-driven system primitives and imperative launch API |
---
::::{grid} 1 2 3 3
:gutter: 3
:::{grid-item-card} {material-regular}`lightbulb;1.2em;sd-text-primary` Why EMOS
:link: why-emos
:link-type: doc
The problem EMOS solves -- from custom R&D projects to universal, adaptive robot apps.
:::
:::{grid-item-card} {material-regular}`rocket_launch;1.2em;sd-text-primary` Getting Started
:link: getting-started/installation
:link-type: doc
Install EMOS and run your first Recipe in minutes.
:::
:::{grid-item-card} {material-regular}`menu_book;1.2em;sd-text-primary` Recipes & Tutorials
:link: recipes/overview
:link-type: doc
Build intelligent robot behaviors with step-by-step guides.
:::
:::{grid-item-card} {material-regular}`architecture;1.2em;sd-text-primary` Core Concepts
:link: concepts/architecture
:link-type: doc
Understand the architecture, components, events, and fallbacks.
:::
:::{grid-item-card} {material-regular}`dashboard;1.2em;sd-text-primary` Dashboard
:link: getting-started/dashboard
:link-type: doc
Zero-touch web console on the robot for browsing and running recipes
:::
:::{grid-item-card} {material-regular}`terminal;1.2em;sd-text-primary` CLI & Deployment
:link: getting-started/cli
:link-type: doc
Package and run Recipes with the `emos` CLI.
:::
:::{grid-item-card} {material-regular}`smart_toy;1.2em;sd-text-primary` AI-Assisted Coding
:link: llms.txt
Get the `llms.txt` for your coding agent and let it write recipes for you.
:::
::::
```
## File: why-emos.md
```markdown
# Why EMOS
The robotics industry is undergoing a structural shift. Robots are transitioning from **single-purpose tools** -- hard-coded for fixed tasks -- to **general-purpose platforms** that must perform different jobs in different environments. While the AI industry races to build foundation models, a critical vacuum remains in the infrastructure required to actually ground these models on robots usable in the field.
EMOS fills that vacuum. It is the missing orchestration layer between capable hardware and capable AI.
---
## The Problem
Modern robot hardware ships with stable locomotion controllers and basic SDKs, but little else. Getting a robot to actually *do something useful* -- navigate a cluttered warehouse, respond to voice commands, recover from failures -- requires stitching together a fragile patchwork of ROS packages, custom launch files, and one-off scripts. Every new deployment becomes a bespoke R&D project.
This approach has three fatal flaws:
1. **It doesn't scale.** Every new robot, environment, or task requires months of custom engineering.
2. **It doesn't adapt.** Rigid state machines and declarative graphs cannot handle the chaos of the real world -- sensor failures, dynamic obstacles, network drops.
3. **It doesn't transfer.** Software written for one robot rarely works on another, even if the task is identical.
---
## What EMOS Changes
### From Custom Projects to Universal Recipes
EMOS replaces brittle, robot-specific software projects with **Recipes**: reusable, hardware-agnostic application packages written in pure Python. A Recipe is a complete agentic workflow -- perception, reasoning, navigation, memory, and interaction -- defined in a single script and launched with one command.
- {material-regular}`smart_toy;1.2em;sd-text-primary` **One Robot, Many Tasks:** The same robot can run different Recipes for different jobs -- inspection in the morning, delivery at noon, security patrol at night.
- {material-regular}`devices;1.2em;sd-text-primary` **One Recipe, Many Robots:** A Recipe written for a wheeled AMR runs identically on a quadruped. EMOS handles the kinematic translation beneath the surface.
### From Rigid Graphs to Adaptive Agents
Legacy stacks treat failure as a system crash. EMOS treats it as a **control flow state**. Its event-driven architecture lets robots reconfigure themselves at runtime:
- {material-regular}`sync;1.2em;sd-text-primary` Hot-swap ML models when the network drops
- {material-regular}`swap_horiz;1.2em;sd-text-primary` Switch navigation algorithms when the robot gets stuck
- {material-regular}`flash_on;1.2em;sd-text-primary` Trigger recovery maneuvers based on sensor events
- {material-regular}`hub;1.2em;sd-text-primary` Compose complex behaviors with logic gates (AND, OR, NOT) across multiple data streams
This isn't bolted-on error handling -- adaptivity is a **first-class primitive** in the system design.
### From Stateless Tools to Embodied Agents
Current robots have logs, not memory. They record data for post-facto analysis but cannot recall it at runtime. EMOS introduces **embodiment primitives** that give robots a sense of self and history:
- {material-regular}`map;1.2em;sd-text-primary` **Spatio-Temporal Semantic Memory:** A queryable world-state backed by vector databases that persists across tasks.
- {material-regular}`self_improvement;1.2em;sd-text-primary` **Self-Referential State:** Components can inspect and modify each other's configuration, enabling system-level awareness rather than isolated self-repair.
### From CPU Bottlenecks to GPU-Accelerated Navigation
While other stacks use GPUs only for vision, EMOS moves the entire navigation control stack to the GPU. Kompass, the EMOS navigation engine, provides **GPGPU-accelerated kernels** for motion planning and control:
- {material-regular}`speed;1.2em;sd-text-primary` **Up to 3,106x speedup** over CPU-bound stacks for trajectory evaluation
- {material-regular}`grid_on;1.2em;sd-text-primary` **1,850x speedup** for dense occupancy grid mapping
- {material-regular}`memory;1.2em;sd-text-primary` **Vendor-neutral** -- works on NVIDIA, AMD, Intel, and integrated GPUs via SYCL
- {material-regular}`developer_board;1.2em;sd-text-primary` Falls back to optimized process-level parallelism on CPU-only platforms
This enables reactive autonomy in dynamic, unstructured environments where traditional CPU-bound stacks like Nav2 simply cannot keep up.
### From Separate Backends to Auto-Generated Interaction
In traditional robotics, the automation logic is "backend" and the user interface is a separate custom project. EMOS treats the **Recipe as the single source of truth** -- defining the logic automatically generates a bespoke Web UI for real-time monitoring, configuration, and control. No separate frontend development required.
---
## The Architecture
EMOS is built on three open-source components that work in tandem:
:::{image} _static/images/diagrams/emos_diagram_light.png
:align: center
:width: 50%
:class: light-only
:::
:::{image} _static/images/diagrams/emos_diagram_dark.png
:align: center
:width: 50%
:class: dark-only
:::
| Component | Layer | What It Does |
|:---|:---|:---|
| [**EmbodiedAgents**](https://github.com/automatika-robotics/embodied-agents) | Intelligence | Agentic graphs of ML models with semantic memory, information routing, and adaptive reconfiguration |
| [**Kompass**](https://github.com/automatika-robotics/kompass) | Navigation | GPU-powered planning and control for real-world mobility across all motion models |
| [**Sugarcoat**](https://github.com/automatika-robotics/sugarcoat) | Architecture | Event-driven system primitives, lifecycle management, and the imperative launch API that underpins both layers |
Together, they provide a complete runtime: from raw sensor data to intelligent action, with adaptivity and resilience built in at every level.
---
## Who Is EMOS For
### 1. Robot Managers & End-Users
Use pre-built Recipes or write your own with the high-level Python API. Focus on your business logic -- EMOS handles the robotics complexity.
### 2. Integrators & Solution Providers
EMOS is your SDK for the physical world. Connect robot events to ERPs, building management systems, or fleet software using the event-action architecture. Spend your time on enterprise integration, not low-level robotics plumbing.
### 3. OEM Teams
Write a single Hardware Abstraction Layer plugin and instantly unlock the entire EMOS ecosystem for your chassis. Every Recipe written by any developer runs on your hardware without custom code.
---
## EMOS is Built for the Real World
EMOS is not a research prototype. It is shaped by the demands of production deployments -- autonomous inspection patrols, security operations, and field robotics on quadruped and wheeled platforms. Every feature in the stack exists because a real-world deployment needed it.
---
## Get Started
::::{grid} 1 2 2 2
:gutter: 3
:::{grid-item-card} {material-regular}`rocket_launch;1.2em;sd-text-primary` Install EMOS
:link: getting-started/installation
:link-type: doc
Get up and running in minutes.
:::
:::{grid-item-card} {material-regular}`menu_book;1.2em;sd-text-primary` Browse Recipes
:link: recipes/overview
:link-type: doc
Step-by-step tutorials from simple to production-grade.
:::
::::
```
## File: getting-started/installation.md
```markdown
# Installation
## EMOS CLI
The fastest way to get started with EMOS is through the CLI. Download the latest release:
```bash
curl -sSL https://raw.githubusercontent.com/automatika-robotics/emos/main/stack/emos-cli/scripts/install.sh | sudo bash
```
Or build from source (requires Go 1.25+):
```bash
git clone https://github.com/automatika-robotics/emos.git
cd emos/stack/emos-cli
make build
sudo make install
```
```{tip}
The CLI is a single static binary with no runtime dependencies, copy `/usr/local/bin/emos` to another machine on the same architecture and it just works.
```
## Deployment Modes
EMOS supports four deployment modes. Run `emos install` without arguments for an interactive menu, or use the `--mode` flag directly.
::::{tab-set}
:::{tab-item} Container
No ROS2 installation required. Runs EMOS inside a Docker container using the public image.
```bash
emos install --mode container
```
You will be prompted to select a ROS2 distribution (Jazzy, Humble, or Kilted). The CLI pulls the image, creates the container, and sets up the `~/emos/` directory structure.
**Requirements:** Docker installed and running.
:::
:::{tab-item} Native
Builds EMOS packages from source and installs them directly into your ROS2 installation at `/opt/ros/{distro}/`. No container needed.
```bash
emos install --mode native
```
The CLI will:
1. Detect your ROS2 installation
2. Clone the EMOS source and dependencies into a build workspace (`~/emos/ros_ws/`)
3. Install system packages (portaudio, GeographicLib, rmw-zenoh)
4. Install Python dependencies
5. Install kompass-core with GPU acceleration support
6. Build all packages with colcon and install them into `/opt/ros/{distro}/`
After installation, EMOS packages are available whenever you source `/opt/ros//setup.bash`. See [Running Recipes](running-recipes.md) for how to launch a recipe -- directly with `python` or via the `emos run` flow.
**Requirements:** A working ROS2 installation (Humble, Jazzy, or Kilted).
:::
:::{tab-item} pixi
```{note}
Currently pinned to **ROS 2 Jazzy**.
```
Installs ROS2 and all EMOS dependencies into an isolated userspace environment using [pixi](https://pixi.sh). No root privileges, no Docker, no pre-installed ROS2 required. Works on any Linux distribution.
```bash
# Install pixi first (emos install --mode pixi tells you if it's missing)
curl -fsSL https://pixi.sh/install.sh | bash
# Install EMOS in pixi mode
emos install --mode pixi
```
The CLI clones the EMOS workspace into `~/.local/share/emos`, pulls ROS 2 Jazzy and all dependencies as pre-built packages from [RoboStack](https://robostack.github.io/) and conda-forge, installs kompass-core with GPU acceleration, then builds the EMOS packages with colcon — independent of any system ROS 2.
See [Running Recipes](running-recipes.md) for how to launch a recipe -- directly from a `pixi shell` or via the `emos run` flow.
**Requirements:** Linux (amd64 or arm64). No root, Docker, or ROS2 needed.
:::
::::
See the [CLI Reference](cli.md) for the full list of commands.
## Which Mode Should I Choose?
| Scenario | Recommended Mode |
| :----------------------------------------------- | :--------------- |
| No ROS2 on host, quick evaluation | **Container** |
| ROS2 already installed, system-level integration | **Native** |
| No root, no Docker, any Linux distro | **Pixi** |
## Reach the Dashboard
During installation you were asked whether to enable the EMOS dashboard as a systemd service. Pick the path you chose below.
### If you enabled the systemd service (recommended)
The dashboard is already running and will come up automatically at every boot. The installer printed the access details once -- a six-digit pairing code, the URLs the dashboard is reachable at, and a scannable QR code. Open any of the URLs in a browser, enter the code, and the browser is paired for ~90 days.
If you missed the install output (or you've already paired and just need the URLs again), reprint the access summary at any time:
```bash
emos serve
```
When the dashboard is already running as a service, `emos serve` detects that and just shows the URLs and management commands -- it does not try to bind a second instance. Manage the service directly with:
```bash
systemctl status emos-dashboard.service
systemctl restart emos-dashboard.service
journalctl -u emos-dashboard.service -f
```
If you've lost the original pairing code, issue a fresh one with `emos config rotate-pairing`.
### If you skipped the systemd service
Start the dashboard manually whenever you want to use it:
```bash
emos serve
```
The first launch prints the pairing code, URLs, and QR code. The process runs in the foreground and stops when you `Ctrl-C` it. You can enable the service later with `emos serve install-service`.
```{seealso}
[Dashboard](dashboard.md) — full walkthrough of pairing, recipes, and run console
```
## Preparing Your Hardware
Before running recipes, you need sensor drivers publishing data on ROS2 topics. EMOS recipes declare the topics they expect (e.g. `Image` from a camera, `LaserScan` from a lidar). Run `emos info ` to see what a recipe needs.
### Installing Sensor Drivers
::::{tab-set}
:::{tab-item} Container
The EMOS container runs with `--privileged` and has access to all USB devices on the host. You can install and run sensor drivers directly **inside the container** — no ROS2 installation on the host is needed.
```bash
# Install a sensor driver inside the container:
docker exec -it emos bash -c "apt-get update && apt-get install -y ros-jazzy-usb-cam"
# Launch the driver inside the container (in a separate terminal):
docker exec -it emos bash -c "source /ros_entrypoint.sh && ros2 run usb_cam usb_cam_node_exe"
```
The driver's topics are immediately visible to recipes running in the same container.
```{tip}
If you have sensor drivers already running on the host with ROS2, they can bridge into the container automatically via Zenoh (the default RMW). Start the host driver with `export RMW_IMPLEMENTATION=rmw_zenoh_cpp`.
```
:::
:::{tab-item} Native
Install the driver package and launch it directly:
```bash
sudo apt install ros-jazzy-usb-cam
source /opt/ros/jazzy/setup.bash
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 run usb_cam usb_cam_node_exe
```
If you place a launch file at `~/emos/robot/launch/bringup_robot.py`, the CLI will start it automatically when you run `emos run`.
:::
:::{tab-item} pixi
Pixi mode assumes you have **no system ROS2 installation**, so sensor drivers are installed into the pixi environment too. The EMOS workspace already has the [RoboStack](https://robostack.github.io/) `robostack-jazzy` channel configured, so adding a driver is a **single command** — install it straight into the EMOS environment:
```bash
cd ~/.local/share/emos
pixi add ros-jazzy-usb-cam
RMW_IMPLEMENTATION=rmw_zenoh_cpp pixi run ros2 run usb_cam usb_cam_node_exe
```
The driver lives in the same environment as your recipes, and because both use Zenoh as the default RMW, its topics are visible to running recipes automatically — no system ROS2, no separate project, no extra channel setup.
```{note}
`emos update` **preserves** drivers you add this way: it stashes your local `pixi.toml` / `pixi.lock` changes around the update and reapplies them. (In the rare case a release changes `pixi.toml` itself, you get a clear conflict to resolve rather than a silent overwrite.)
```
```{tip}
If a driver package isn't on RoboStack, install it from source into the EMOS environment with `colcon`, or fall back to Native mode for that driver only.
```
:::
::::
```{important}
Match the driver's RMW implementation to the one your recipe uses, or the driver's topics won't be visible to it. EMOS recipes default to **Zenoh** -- set `export RMW_IMPLEMENTATION=rmw_zenoh_cpp` in the shell where you launch the driver. If the recipe overrides this (e.g. `emos run --rmw rmw_cyclonedds_cpp`), export the same value instead.
```
### Verifying Sensors
Before running a recipe, confirm your sensors are publishing:
```bash
# 1. See what the recipe needs
emos info vision_follower
# 2. Check topics exist
ros2 topic list
# 3. Confirm data is flowing
ros2 topic hz /image_raw
```
If `ros2 topic hz` shows a non-zero rate, the sensor is ready.
```{seealso}
If sensor verification fails during `emos run`, see [Troubleshooting](troubleshooting.md).
```
## Model Serving Platform
EMOS is agnostic to model serving platforms. You need at least one of the following available on your network:
- {material-regular}`download;1.2em;sd-text-primary` **[Ollama](https://ollama.com)** Recommended for local inference.
- {material-regular}`smart_toy;1.2em;sd-text-primary` **[RoboML](https://github.com/automatika-robotics/robo-ml)** Automatika's own open-source model serving package for quick prototyping.
- {material-regular}`api;1.2em;sd-text-primary` **OpenAI API-compatible fast inference servers** e.g. [llama.cpp](https://github.com/ggml-org/llama.cpp), [vLLM](https://github.com/vllm-project/vllm), [SGLang](https://github.com/sgl-project/sglang).
- {material-regular}`precision_manufacturing;1.2em;sd-text-primary` **[LeRobot](https://github.com/huggingface/lerobot)** For Vision-Language-Action (VLA) models.
- {material-regular}`cloud;1.2em;sd-text-primary` **Cloud endpoints** e.g. OpenAI, Claude, HuggingFace Inference etc. using an API key.
```{tip}
For larger models, run the serving platform on a GPU-equipped machine on your local network, or use a cloud endpoint, rather than running models directly on the robot.
```
## Updating
Update your installation to the latest version:
```bash
emos update
```
The CLI detects your installation mode and updates accordingly:
- **Container mode:** pulls the latest image and recreates the container.
- **Native mode:** pulls the latest source, rebuilds, and re-installs packages into `/opt/ros/{distro}/`.
- **Pixi mode:** runs `git pull` and `git submodule update` in the EMOS workspace at `~/.local/share/emos`, refreshes the pixi environment (`pixi install`), and rebuilds the EMOS packages (`pixi run setup`). Any installed robot plugin is pulled and rebuilt too.
## Uninstalling
To remove EMOS from a device:
```bash
sudo emos uninstall
```
After confirming, the CLI:
- Runs mode-specific cleanup:
- **Container mode:** removes the Docker container. The image is preserved unless `--remove-image` is passed.
- **Native mode:** The CLI prints the manual `rm` commands so you can clean them ROS packages yourself if you want.
- **Pixi mode:** removes `.pixi/`, `build/`, `install/`, `log/` under your EMOS clone. The cloned repo itself is preserved.
- Removes `~/emos/recipes`, `~/emos/logs`, and `~/.config/emos` (installed recipes, run logs, and dashboard auth state).
Pass `--keep-data` to preserve `~/emos/recipes` and `~/emos/logs`. Pass `--keep-config` to preserve `~/.config/emos` (so previously paired browsers remain valid). Pass `-y` / `--yes` to skip the confirmation prompt.
The CLI binary at `/usr/local/bin/emos` is never removed automatically. The command prints the one-liner you can run after the process exits.
```{tip}
Use `emos uninstall` before switching install modes (e.g. native -> pixi). It clears auth tokens and mode-specific state that would otherwise carry over and confuse the new install.
```
## Installing from Source (Developer Setup)
If you want to build the full EMOS stack from source for contributing or accessing the latest features, follow the steps below. This installs all three stack components: **Sugarcoat** (architecture), **EmbodiedAgents** (intelligence), and **Kompass** (navigation).
### 1. Create a unified workspace
```shell
mkdir -p emos_ws/src
cd emos_ws/src
```
### 2. Clone the stack
```shell
git clone https://github.com/automatika-robotics/emos.git
cp -r emos/stack/sugarcoat .
cp -r emos/stack/embodied-agents .
cp -r emos/stack/kompass .
```
### 3. Install Python dependencies
```shell
PIP_BREAK_SYSTEM_PACKAGES=1 pip install numpy opencv-python-headless 'attrs>=23.2.0' jinja2 httpx setproctitle msgpack msgpack-numpy platformdirs tqdm pyyaml toml websockets
```
### 4. Install the Kompass core engine
The `kompass-core` package provides optimized planning and control algorithms.
::::{tab-set}
:::{tab-item} GPU Support (Recommended)
For production robots or high-performance simulation, install with GPU acceleration:
```bash
curl -sSL https://raw.githubusercontent.com/automatika-robotics/kompass-core/refs/heads/main/build_dependencies/install_gpu.sh | bash
```
:::
:::{tab-item} CPU Only
For quick testing or lightweight environments:
```bash
pip install kompass-core
```
:::
::::
### 5. Install ROS dependencies and build
```shell
cd emos_ws
rosdep update
rosdep install -y --from-paths src --ignore-src
colcon build
source install/setup.bash
```
You now have the complete EMOS stack built and ready to use.
```
## File: getting-started/quickstart.md
```markdown
# Quick Start
## Your First EMOS Recipe
EMOS lets you describe complete robot behaviors as **recipes** -- pure Python scripts that wire together components, models, and ROS topics using a declarative style.
In this quickstart you will build a simple Visual Question Answering recipe: a robot that sees through its camera and answers questions about what it observes. By the end, you'll have run it end-to-end and (optionally) opened a small web UI to talk to it.
```{important}
This guide assumes you have already installed EMOS. If not, see the [Installation guide](installation.md) first.
```
## The Recipe
Save the following as `my_first_recipe.py`. We'll walk through what each section does, then list what needs to be running before you launch it.
```python
from agents.clients.ollama import OllamaClient
from agents.components import VLM
from agents.models import OllamaModel
from agents.ros import Topic, Launcher
# Define input and output topics (pay attention to msg_type)
text0 = Topic(name="text0", msg_type="String")
image0 = Topic(name="image_raw", msg_type="Image")
text1 = Topic(name="text1", msg_type="String")
# Define a model client (Ollama in this case)
qwen_vl = OllamaModel(name="qwen_vl", checkpoint="qwen2.5vl:latest")
qwen_client = OllamaClient(qwen_vl)
# Define a VLM component (a node with a particular functionality)
vlm = VLM(
inputs=[text0, image0],
outputs=[text1],
model_client=qwen_client,
trigger=text0,
component_name="vqa",
)
vlm.set_topic_prompt(text0, template="""You are an amazing and funny robot.
Answer the following about this image: {{ text0 }}"""
)
# Launch the component
launcher = Launcher()
launcher.add_pkg(components=[vlm])
launcher.bringup()
```
## Step-by-Step Breakdown
### Define Topics
Every EMOS recipe starts by declaring the ROS topics that connect components together. Components automatically create listeners for input topics and publishers for output topics.
```python
text0 = Topic(name="text0", msg_type="String")
image0 = Topic(name="image_raw", msg_type="Image")
text1 = Topic(name="text1", msg_type="String")
```
```{note}
On a real robot, change `image0`'s name to match the topic your camera driver actually publishes (e.g. `/camera/color/image_raw`). The "Before You Run" section below explains how to confirm what's available.
```
### Create a Model Client
EMOS is model-agnostic. Here we create a client that uses [Qwen2.5vl](https://ollama.com/library/qwen2.5vl) served by [Ollama](https://ollama.com):
```python
qwen_vl = OllamaModel(name="qwen_vl", checkpoint="qwen2.5vl:latest")
qwen_client = OllamaClient(qwen_vl)
```
````{tip}
If Ollama is running on a different machine on your network, specify the host and port:
```python
qwen_client = OllamaClient(qwen_vl, host="127.0.0.1", port=8000)
```
````
### Configure the Component
Components are the functional building blocks of EMOS recipes. The VLM component lets you set topic-level prompts using Jinja2 templates so you can shape the model's behavior per-input:
```python
vlm = VLM(
inputs=[text0, image0],
outputs=[text1],
model_client=qwen_client,
trigger=text0,
component_name="vqa",
)
vlm.set_topic_prompt(text0, template="""You are an amazing and funny robot.
Answer the following about this image: {{ text0 }}"""
)
```
### Launch
Finally, bring the recipe up:
```python
launcher = Launcher()
launcher.add_pkg(components=[vlm])
launcher.bringup()
```
## Before You Run
The recipe needs three things in place before it can do anything useful.
**1. Ollama is running and the model is pulled.**
```bash
curl http://localhost:11434/api/tags # is Ollama up?
ollama pull qwen2.5vl:latest # pre-fetch the model
```
**2. Something is publishing on the camera topic.**
On a development machine, [ROS 2 USB Cam](https://github.com/klintan/ros2_usb_camera) is a quick way to publish from a webcam. On a real robot, start your camera driver -- see [Preparing Your Hardware](installation.md#preparing-your-hardware) for mode-specific instructions, including the container-mode caveat that drivers run **inside** the container.
Confirm a publisher is up and producing frames at non-zero rate:
```bash
ros2 topic list
ros2 topic hz /image_raw # or whatever name you set in the recipe
```
If the topic name your driver publishes differs from `image_raw`, update `image0` in the recipe to match.
**3. Component-specific extras.** Some clients prompt for additional Python packages on first launch -- the script will throw an error and tell you exactly what to install.
If any of these fail when you run the recipe, see [Troubleshooting](troubleshooting.md) for sensor verification and model-server gotchas.
## Run It
Two ways to run, pick by purpose.
### Option A: just run the script
For fast iteration while you're shaping the recipe. Works in Native and Pixi modes.
::::{tab-set}
:::{tab-item} Native
```bash
source /opt/ros/jazzy/setup.bash # or your installed distro
python3 my_first_recipe.py
```
:::
:::{tab-item} Pixi
```bash
# Activate the EMOS pixi env without leaving your recipe's directory
pixi shell --manifest-path ~/.local/share/emos/pixi.toml
source ~/.local/share/emos/install/setup.sh # adds the built EMOS packages to your env
python3 my_first_recipe.py
```
:::
:::{tab-item} Container
Direct `python3` from the host **does not work** in container mode -- the `agents`, `kompass`, and `ros_sugar` Python packages live inside the container. Use Option B, or `docker exec -it emos-container bash` first and run the script from there.
:::
::::
### Option B: run it through the EMOS flow
For sensor verification, log retention, and visibility on the dashboard. **Required in container mode.**
```bash
mkdir -p ~/emos/recipes/my_first_recipe
cp my_first_recipe.py ~/emos/recipes/my_first_recipe/recipe.py
emos run my_first_recipe
```
The recipe now appears on the dashboard's **Recipes → Installed** tab, and every run is logged to `~/emos/logs/my_first_recipe_.log`.
```{seealso}
[Running Recipes](running-recipes.md) -- the full comparison of both flows, install-mode pitfalls, and the optional `manifest.json` schema (display name, description, Zenoh router config).
```
## Verify It Is Running
From a separate terminal, use standard ROS 2 commands to confirm the node and its topics are active:
```bash
ros2 node list # should list the `vqa` node
ros2 topic list # should list text0, image_raw, text1
```
To trigger a single inference manually, publish a question to `text0` and watch `text1` for the reply:
```bash
ros2 topic pub --once /text0 std_msgs/String "{data: 'what do you see?'}"
ros2 topic echo /text1
```
## Add a Web UI
EMOS can dynamically generate a web-based UI for any recipe -- so you don't need a terminal to interact with it. Add one line before `bringup()` to tell the launcher which topics to render:
```python
launcher = Launcher()
launcher.enable_ui(inputs=[text0], outputs=[text1, image0]) # <-- specify UI
launcher.add_pkg(components=[vlm])
launcher.bringup()
```
````{note}
The web UI requires two additional packages:
```bash
pip install python-fasthtml monsterui
```
````
The UI is served at **http://localhost:5001** (or **http://<ROBOT_IP>:5001** if running on a robot). Open it in a browser, send a question via the input field, and you should get a reply generated by the Qwen2.5vl model.

## Where Next
- **Checkout your EMOS dashboard** Open the EMOS Dashboard at `http://emos.local:8765` (or scan the QR printed by `emos serve`). Pair once and you can pull recipes, launch them, and watch logs from any browser. See [Dashboard](dashboard.md).
- **Customize this recipe.** [Running Recipes](running-recipes.md) covers the optional `manifest.json` and the install-mode reference for all runtimes.
- **Build something more capable.** The [Recipes & Tutorials](../recipes/overview.md) section walks through conversational agents, semantic memory, navigation, manipulation, and the Cortex agentic harness.
- **Hit a snag?** See [Troubleshooting](troubleshooting.md) for sensor verification failures, model-server timeouts, and mode-specific issues.
```
## File: getting-started/cli.md
```markdown
# EMOS CLI
The `emos` CLI manages installation, recipes, the dashboard daemon, and device configuration on a robot. Every long-form action it performs is also exposed over the dashboard's REST API (see [`internal/server/openapi.yaml`](https://github.com/automatika-robotics/emos/blob/main/stack/emos-cli/internal/server/openapi.yaml)), so anything you can do on the terminal you can also drive from a browser or an agentic skill.
## Quick Reference
| Command | Description |
| :----------------- | :---------------------------------------------------------- |
| `emos install` | Install EMOS (interactive mode selection) |
| `emos uninstall` | Remove EMOS (mode-aware cleanup) |
| `emos update` | Update EMOS to the latest version |
| `emos status` | Show installation status |
| `emos serve` | Run the dashboard daemon (REST API + web UI) |
| `emos config` | Inspect or modify device configuration, pairing tokens, TLS |
| `emos recipes` | List recipes available for download |
| `emos pull ` | Download a recipe |
| `emos ls` | List locally installed recipes |
| `emos run ` | Run a recipe (foreground, blocking) |
| `emos info ` | Show sensor/topic requirements for a recipe |
| `emos map ` | Mapping tools (record, edit) |
| `emos plugin `| Install and manage the robot plugin |
| `emos version` | Show CLI version |
```{tip}
Every command supports `-h`/`--help`. The CLI is also a single static binary — copy `/usr/local/bin/emos` to another machine and it just works (no Python, no runtime dependencies).
```
## Typical Workflows
### First-time setup
```bash
# 1. Install EMOS (interactive mode menu)
emos install
# 2. Start the dashboard (printed pairing code is shown once)
emos serve
# 3. (optional) Make the dashboard auto-start at boot
sudo emos serve install-service
```
After step 2, point a browser at `http://emos.local:8765` (or scan the QR), enter the pairing code, and you're in. See [Dashboard](dashboard.md).
### CLI-only recipe loop
```bash
emos recipes # browse the catalog
emos pull vision_follower # download a recipe
emos info vision_follower # check what sensors it needs
emos run vision_follower # launch it (blocks until exit)
```
## Running Recipes
`emos run ` adapts to the install mode -- starting the container in container mode, sourcing ROS in native, activating the pixi env in pixi -- before exec-ing the recipe and streaming logs to `~/emos/logs/_.log`. Logs are also visible from the dashboard's [Run console](dashboard.md#run-console).
For the full guide to writing, dropping in, and launching custom recipes (including the install-mode pitfalls of running them directly via `python`), see [Running Recipes](running-recipes.md).
## Recipe Layout
A recipe is a directory under `~/emos/recipes/` with the following structure:
```
~/emos/recipes/
my_recipe/
recipe.py # Main entry point (required)
manifest.json # Optional: Zenoh config / display name / description
```
### `manifest.json`
```json
{
"name": "My Recipe",
"description": "Does the thing.",
"zenoh_router_config_file": "my_recipe/zenoh_config.json5"
}
```
- {material-regular}`label;1.2em;sd-text-primary` **name** — display name for the dashboard's recipe cards. Falls back to the directory name.
- {material-regular}`description;1.2em;sd-text-primary` **description** — short blurb shown on the recipe detail page.
- {material-regular}`settings;1.2em;sd-text-primary` **zenoh_router_config_file** — path (relative to `~/emos/recipes/`) to a Zenoh router `.json5` config file. Only consulted when the recipe runs under `rmw_zenoh_cpp`.
```{note}
Sensor requirements are auto-extracted from `recipe.py` by parsing `Topic(name=..., msg_type=...)` declarations. You don't need to list them in the manifest. Run `emos info ` (or open the recipe in the dashboard) to see the inferred requirements.
```
For the full walkthrough -- writing the recipe, dropping it in, verifying discovery, and launching it via `emos run` or the dashboard -- see [Running Recipes](running-recipes.md).
## Command Reference
### `emos install`
```bash
emos install # interactive mode menu
emos install --mode container # OSS container (no ROS required on host)
emos install --mode native # native (uses host's ROS 2)
emos install --mode pixi # self-contained ROS via pixi (no system ROS)
emos install --mode licensed # licensed deployment (requires license key)
emos install --distro jazzy # pin a ROS distro for container/native mode
```
| Flag | Default | Description |
| :--------- | :--------- | :--------------------------------------------------- |
| `--mode` | _(prompt)_ | One of: `container`, `native`, `pixi`, `licensed`. |
| `--distro` | _(prompt)_ | ROS 2 distribution: `jazzy`, `humble`, `kilted`. |
The installer offers, at the end, to:
- Install a systemd unit so the dashboard auto-starts at boot (see [Make the dashboard start automatically](dashboard.md#make-the-dashboard-start-automatically)).
- Persist the chosen device name and a fresh pairing code to `~/.config/emos/config.json`.
```{note}
Pixi mode requires [pixi](https://pixi.sh) on the host (`emos install --mode pixi` errors with install instructions if it's missing). It clones the EMOS workspace and builds it under `~/.local/share/emos`, independent of any system ROS. Currently pinned to ROS 2 Jazzy. See [Installation](installation.md#deployment-modes).
```
### `emos uninstall`
```bash
sudo emos uninstall # interactive
sudo emos uninstall --yes # non-interactive
sudo emos uninstall --keep-data # preserve recipes + logs
sudo emos uninstall --keep-config # preserve dashboard auth state
sudo emos uninstall --remove-image # also docker rmi (container / licensed)
```
Stops the dashboard service and runs mode-specific cleanup. By default also removes `~/emos/recipes`, `~/emos/logs`, and `~/.config/emos`.
| Flag | Default | Description |
| :--------------- | :------ | :------------------------------------------------------------------------------------------- |
| `--keep-data` | `false` | Preserve `~/emos/recipes` and `~/emos/logs`. |
| `--keep-config` | `false` | Preserve `~/.config/emos` (keeps device name + dashboard pairing across reinstall). |
| `--remove-image` | `false` | Also `docker rmi` the EMOS image (container / licensed modes only; preserved by default). |
| `-y`, `--yes` | `false` | Skip the confirmation prompt. |
Mode-specific behavior:
- **Container / licensed:** `docker stop` + `docker rm` the EMOS container; licensed also removes the container auto-restart unit and `~/emos/robot/`.
- **Native:** removes the build workspace and `pip uninstall`s `kompass-core`. EMOS package files in `/opt/ros//` are co-mingled with ROS by colcon and **cannot** be cleanly removed -- the command prints the manual `rm` commands rather than running them, so you can review and apply if you want.
- **Pixi:** removes the EMOS-owned pixi workspace at `~/.local/share/emos` (cloned repo + env + build) wholesale. A workspace you cloned yourself elsewhere is preserved — only its build artifacts are stripped.
The CLI binary at `/usr/local/bin/emos` is never removed automatically -- a running process can't reliably unlink itself. The command prints the `sudo rm` one-liner for you.
```{tip}
Run `emos uninstall` before switching install modes (e.g. native -> pixi). It clears auth tokens and mode-specific state that would otherwise carry over and confuse the new install.
```
### `emos update`
```bash
emos update
```
Detects the install mode and updates accordingly. Container mode pulls the latest image and recreates the container; native mode pulls the latest source, rebuilds, and re-installs into `/opt/ros/{distro}/`; pixi mode runs `git pull`, refreshes the pixi env (`pixi install`), and rebuilds the EMOS packages (`pixi run setup`). If a robot plugin is installed, it is also pulled to its latest commit and rebuilt.
### `emos status`
```bash
emos status
```
Shows install mode, ROS distro, container/service state, and dashboard service health. Subset of what `emos config show` reports.
### `emos serve`
Run the dashboard daemon. See the dedicated [Dashboard](dashboard.md) page for the UX.
```bash
emos serve # foreground; HTTP on the configured port
emos serve --tls # opt into HTTPS (self-signed cert)
emos serve --addr :9000 # bind to a custom port for one run
emos serve --qr # print a QR for the dashboard URL and exit
```
| Flag | Default | Description |
| :---------------- | :-------- | :----------------------------------------------------------------------------------------------- |
| `--addr` | _(empty)_ | `host:port` to bind. Empty falls back to the configured port (`emos config set port`) or `8765`. |
| `--no-mdns` | `false` | Skip mDNS announcement. The dashboard is then only reachable by IP / explicit hostname. |
| `--no-auth` | `false` | **Dev only.** Accept all requests without a bearer token. |
| `--tls` | `false` | Serve over HTTPS using a self-signed cert under `~/.config/emos/`. |
| `--qr` | `false` | Print a QR code with the dashboard URL and exit (no daemon). |
| `-v`, `--verbose` | `false` | Log every HTTP request (including reads) at DEBUG. |
#### `emos serve install-service`
```bash
sudo emos serve install-service
```
Writes `/etc/systemd/system/emos-dashboard.service`, enables it, and starts it. The unit's `ExecStart` points at the binary you ran the command with, so it follows the active install (`/usr/local/bin/emos` if installed via the script).
#### `emos serve uninstall-service`
```bash
sudo emos serve uninstall-service
```
Stops, disables, and removes the unit. Does not remove the cert, config, or recipes.
### `emos config`
Inspect and modify everything in `~/.config/emos/config.json` — install info, device name, port, paired-device tokens, and TLS material.
```bash
emos config show # human-readable device state
emos config get [key] # print one value or the whole config as JSON
emos config set # writable keys: name, port
emos config path # print the config file path
emos config tokens # list paired browsers / agents
emos config revoke-token # revoke a single paired device
emos config rotate-pairing # issue a fresh pairing code (existing tokens stay valid)
emos config tls-fingerprint # print the dashboard TLS cert SHA-256 fingerprint
emos config tls-regenerate # re-mint the self-signed TLS cert (use after IP change)
emos config reset # reset device state (pairing/name/port); keeps install info
```
#### `emos config show`
Prints a single-pane summary:
```text
EMOS DEVICE STATE
Identity: epic-otter
Mode: native
ROS distro: jazzy
Dashboard port: 8765
Recipes: /home/you/emos/recipes
Logs: /home/you/emos/logs
Config file: /home/you/.config/emos/config.json
Pairing configured: yes
Active tokens: 2
Dashboard service: active (emos-dashboard.service)
```
#### `emos config get [key]`
With no argument, prints the full config as JSON. With a key (`name`, `mode`, `ros_distro`, `port`), prints a single value — useful in shell scripts.
#### `emos config set `
Writable keys: `name` (mDNS hostname segment, validated against `[a-z0-9-]`), `port` (1–65535). Other fields are managed by the installer / serve daemon.
```bash
emos config set name happy-robot
emos config set port 9000
sudo systemctl restart emos-dashboard.service # if running as a service
```
#### `emos config tokens` and `revoke-token`
Lists paired browsers without leaking the underlying token hash:
```text
ID LABEL ISSUED EXPIRES
4d0e9c01 phone 2026-04-12 10:32 2026-07-11 10:32
71a3f82b laptop 2026-04-12 11:07 2026-07-11 11:07
```
Revoke one device:
```bash
emos config revoke-token 4d0e9c01 # by short id (prefix of hash)
emos config revoke-token phone # by exact label
```
#### `emos config rotate-pairing`
Issues a new six-digit pairing code, **without** revoking already-paired tokens. Useful when a code may have been seen by someone who shouldn't get further access.
```bash
emos config rotate-pairing
# ✓ New pairing code (shown once): 829471
```
#### `emos config tls-fingerprint` / `tls-regenerate`
See [HTTPS (Optional)](#https-optional) below.
### `emos pull`
```bash
emos pull
```
Downloads a recipe from the Automatika catalog and extracts it to `~/emos/recipes//`. Overwrites the existing version if present. Requires internet.
### `emos ls`
```bash
emos ls
```
Lists everything under `~/emos/recipes/`. The dashboard's **Recipes → Installed** tab shows the same set.
### `emos info`
```bash
emos info
```
Inspects a recipe's Python source via AST and prints its sensor and topic requirements. Accepts either a recipe name (looked up in `~/emos/recipes/`) or a path to a `.py` file:
```bash
emos info vision_follower # ~/emos/recipes/vision_follower/recipe.py
emos info ./my_recipe.py # explicit path
```
The output groups topics into:
- **Required Sensors** — `Image`, `LaserScan`, `Imu`, `Audio`, `Odometry`, `RGBD`, `PointCloud2`, `CompressedImage`. Hardware label and suggested apt packages tailored to your distro.
- **Other Topics** — non-sensor topics declared by the recipe.
### `emos run`
```bash
emos run
emos run --rmw rmw_cyclonedds_cpp
emos run --skip-sensor-check
```
| Flag | Default | Description |
| :-------------------- | :-------------- | :----------------------------------------------------------------- |
| `--rmw` | `rmw_zenoh_cpp` | One of: `rmw_zenoh_cpp`, `rmw_fastrtps_cpp`, `rmw_cyclonedds_cpp`. |
| `--skip-sensor-check` | `false` | Skip the 10-second sensor-topic verification. |
#### What happens during `emos run`
1. Reads `recipe.py` and extracts `Topic(...)` declarations.
2. Identifies sensor topics.
3. Starts the Zenoh router (only when using `rmw_zenoh_cpp`).
4. Launches `~/emos/robot/launch/bringup_robot.py` if it exists (native / pixi only; container mode uses an in-container bringup).
5. Verifies each sensor topic is publishing (polls `ros2 topic list` for up to 10 s).
6. Executes the recipe — output streams to the terminal and is saved to `~/emos/logs/`.
#### When to use `--skip-sensor-check`
- Sensors that publish on-demand (service-triggered cameras).
- Replaying a rosbag whose topic names differ from the recipe.
- Pure AI recipes (LLM chat, TTS) that don't require sensor data.
```{warning}
If you skip the check and a sensor topic never arrives, the recipe may hang silently waiting for data. Use `ros2 topic hz /topic_name` to diagnose.
```
```{seealso}
[Troubleshooting](troubleshooting.md) for common errors during recipe execution.
```
### `emos plugin`
Install and manage the robot plugin — a ROS package that adapts a specific robot to the EMOS stack. A robot runs **one plugin at a time**. See [Robot Plugins](plugins.md) for the full guide.
```bash
emos plugin list # browse the catalog (active plugin marked)
emos plugin install # install + activate (replaces any active plugin)
emos plugin inspect # show the active plugin's feedbacks/commands/actions/events
emos plugin remove # remove the active plugin
```
| Subcommand | Description |
| :------------------ | :------------------------------------------------------------------------------------------- |
| `list` | List plugins available in the Automatika catalog; flags the currently active one. |
| `install ` | Clone, build (for your install mode), and activate a plugin. Prompts before replacing one. |
| `inspect` | Pretty-print the active plugin's introspection tree (same data the dashboard System page shows). |
| `remove` | Remove the active plugin from the robot. |
Installing a plugin makes it importable; a recipe opts in with `Launcher(robot_plugin=MyRobotPlugin())`. The dashboard's [Plugins page](dashboard.md#plugins) drives the same flow from a browser.
### `emos map`
Mapping subcommands for creating and editing environment maps:
```bash
emos map record # record mapping data on the robot
emos map install-editor # install the map editor container (one-time)
emos map edit # process a ROS bag into a PCD map
```
### `emos version`
```bash
emos version
# emos vX.Y.Z
```
Prints the CLI version. The dashboard's `/api/v1/info` returns the same value.
## HTTPS (Optional)
The dashboard serves plain HTTP by default. That's the right default on a trusted LAN — bearer-token auth gates every write, so eavesdropping yields nothing useful unless someone can also intercept the pairing handshake.
You can opt in to HTTPS with a self-signed certificate:
```bash
emos serve --tls
```
### What the flag does
On first launch under `--tls`, the daemon mints a 2-year ECDSA P-256 certificate and persists it under `~/.config/emos/`:
| Path | Mode | Purpose |
| :----------------------- | :----- | :------------------------------------------------------ |
| `~/.config/emos/tls.crt` | `0644` | PEM-encoded leaf certificate (also acts as its own CA). |
| `~/.config/emos/tls.key` | `0600` | PEM-encoded private key. Never copy this off the robot. |
The certificate's SubjectAltName covers:
- `localhost`, `.local`, `emos.local`
- `127.0.0.1`, `::1`
- Every LAN IPv4 the device sees at mint time (excluding loopback / docker / veth / tailscale-style virtual interfaces).
It re-uses the persisted cert on subsequent launches and auto-rotates inside the 30-day-before-expiry window. Network-level changes (new IP, new mDNS name) don't trigger automatic rotation — see `tls-regenerate` below.
### Trusting the cert
A self-signed cert produces a "Not Secure" warning the first time a browser hits it. The warning is encryption-preserving — TLS still negotiates a session — but the trust chain is empty, so browsers refuse to call the connection authenticated. Two paths from there:
1. **Click through, every time.** Fine for a workshop / one-off install. The encryption protects against passive sniffing on the LAN, the bearer token still protects against unauthenticated access, and the warning is mostly cosmetic. Verify the cert before clicking through:
```bash
emos config tls-fingerprint
```
Compare the printed SHA-256 fingerprint with the one the browser shows under "View Certificate" before trusting it.
2. **Add the cert to a trust store** so the warning goes away permanently for that device:
- **Firefox** keeps its own trust store: _Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import_ `~/.config/emos/tls.crt`, then tick _"Trust this CA to identify websites."_
- **Chrome / Edge** follow the OS trust store. On Linux: `sudo cp tls.crt /usr/local/share/ca-certificates/emos-.crt && sudo update-ca-certificates`. On macOS, drop the cert into Keychain Access and mark it Always Trust. On Windows, `certmgr.msc → Trusted Root Certification Authorities → Import`.
### When to regenerate
The SAN list is fixed at mint time. If the robot's primary IP changes (new network, new DHCP lease) or you renamed it (`emos config set name`), the cert no longer matches the new address and the browser will show a different error (`NET::ERR_CERT_COMMON_NAME_INVALID`). Regenerate:
```bash
emos config tls-regenerate
sudo systemctl restart emos-dashboard.service # if running as a service
```
Inspect the active fingerprint at any time:
```bash
emos config tls-fingerprint
# TLS CERTIFICATE
# Fingerprint (SHA-256):
# 8B:F2:0C:...:E7
# Expires: 2028-04-28
# Certificate: /home/you/.config/emos/tls.crt
# Private key: /home/you/.config/emos/tls.key
```
### When you actually need HTTPS
Most EMOS deployments are happy on HTTP. Reach for `--tls` when:
- You're building dashboard features that need a **secure context** — `getUserMedia` (microphone / camera), Web Audio capture, Service Workers, Web Bluetooth. Browsers won't expose these APIs over plain HTTP.
- You're running on a network you don't fully control (a venue Wi-Fi, a colocated factory) where token-only auth feels too thin.
- An organisational policy requires HTTPS end-to-end.
```{tip}
Per-recipe Sugarcoat web UIs that need a secure context handle their own TLS independently of the dashboard. See the [Dynamic Web UI](../concepts/web-ui.md) page.
```
### Running as a service with TLS
The systemd unit installed by `emos serve install-service` runs plain HTTP. To switch the service to HTTPS, edit its `ExecStart`:
```bash
sudo systemctl edit --full emos-dashboard.service
# add --tls to the ExecStart line, e.g.
# ExecStart=/usr/local/bin/emos serve --addr :8765 --tls
sudo systemctl daemon-reload
sudo systemctl restart emos-dashboard.service
```
## Files & Paths
```{list-table}
:header-rows: 1
:widths: 35 65
* - Path
- Purpose
* - `~/.config/emos/config.json`
- Single source of truth: install info, device name, port, paired-device tokens (hashed). Mode `0600`.
* - `~/.config/emos/tls.crt` / `tls.key`
- Self-signed TLS material (created by `emos serve --tls` or `emos config tls-regenerate`).
* - `~/emos/recipes/`
- Installed recipes. Each subdirectory is one recipe.
* - `~/emos/logs/`
- Per-run log files: `_.log`. Streamed by `emos run` and the dashboard.
* - `~/emos/ros_ws/`
- Native-mode build workspace.
* - `~/.local/share/emos/`
- Pixi-mode install: cloned EMOS workspace, pixi env, and colcon overlay (`emos install --mode pixi`).
* - `~/emos/workspace/`
- Robot-plugin source and build overlay (`emos plugin install`).
* - `/etc/systemd/system/emos-dashboard.service`
- Dashboard auto-start unit (created by `emos serve install-service`).
* - `/etc/systemd/system/emos.service`
- Container auto-restart unit (created by licensed install).
```
```{tip}
`~/.config/emos/config.json` is **the** persistent state. `emos config reset` clears the dashboard's device-side state (paired browsers, custom name, custom port) but **preserves install info** (mode, ROS distro, license key) so the dashboard keeps recognising the device as installed afterwards.
```
```
## File: concepts/architecture.md
```markdown
# Architecture
**The unified orchestration layer for Physical AI.**
EMOS (The Embodied Operating System) is the software layer that transforms quadrupeds, humanoids, and mobile robots into **Physical AI Agents**. Just as Android standardized the smartphone hardware market, EMOS provides a bundled, hardware-agnostic runtime that allows robots to see, think, move, and adapt in the real world.
## The Body/Mind Split
At its core, EMOS decouples the robot's **Body** from its **Mind**, creating a standard interface for intelligence.
- {material-regular}`precision_manufacturing;1.2em;sd-text-primary` **The Body** encompasses the physical hardware: motors, sensors, actuators, and the low-level drivers that control them. EMOS abstracts over the specifics of any particular robot platform, whether it is a wheeled AMR, a quadruped, or a humanoid.
- {material-regular}`psychology;1.2em;sd-text-primary` **The Mind** is the software intelligence that perceives the world, reasons about it, and decides how to act. EMOS provides the cognitive and navigational primitives that turn raw sensor data into purposeful behavior.
This separation means that the same application logic --- a "Recipe" --- can be written once and deployed across entirely different robot bodies without rewriting code. EMOS handles the translation between intent and hardware.
## The Three Layers
EMOS is built on three open-source, publicly developed core components that work in tandem. Each layer addresses a distinct concern of the robotic software stack.
:::{image} ../_static/images/diagrams/emos_diagram_light.png
:align: center
:width: 500px
:class: light-only
:::
:::{image} ../_static/images/diagrams/emos_diagram_dark.png
:align: center
:width: 500px
:class: dark-only
:::
### Intelligence Layer: EmbodiedAgents
[EmbodiedAgents](https://github.com/automatika-robotics/embodied-agents) is the orchestration framework for building agentic graphs of ML models. It provides:
- {material-regular}`visibility;1.2em;sd-text-primary` **Multi-modal perception** using vision-language models, object detectors, and speech processing.
- {material-regular}`memory;1.2em;sd-text-primary` **Hierarchical spatio-temporal memory** for contextual reasoning about the robot's environment over time.
- {material-regular}`alt_route;1.2em;sd-text-primary` **Semantic routing** that directs user commands to the correct capability (navigation, vision, conversation) based on intent.
- {material-regular}`sync;1.2em;sd-text-primary` **Adaptive reconfiguration** that allows the robot to switch between cloud APIs and local models at runtime based on connectivity and latency requirements.
### Navigation Layer: Kompass
[Kompass](https://github.com/automatika-robotics/kompass) is the event-driven navigation stack responsible for real-world mobility. It provides:
- {material-regular}`speed;1.2em;sd-text-primary` **GPGPU-accelerated planning** that moves heavy geometric computation to the GPU, achieving up to 3,106x speedups over CPU-based approaches and freeing the CPU for application logic.
- {material-regular}`settings;1.2em;sd-text-primary` **Hardware-agnostic control** that works across wheeled, legged, and tracked platforms.
- {material-regular}`bolt;1.2em;sd-text-primary` **Event-driven architecture** where planners and controllers react to environmental changes (obstacles, terrain shifts, emergency stops) rather than running in fixed polling loops.
### Architecture Layer: Sugarcoat
[Sugarcoat](https://github.com/automatika-robotics/sugarcoat) is the meta-framework that provides the foundational system design primitives on which both EmbodiedAgents and Kompass are built. It provides:
- {material-regular}`autorenew;1.2em;sd-text-primary` **Lifecycle-managed Components** that replace standard ROS2 nodes with self-healing, health-aware execution units.
- {material-regular}`flash_on;1.2em;sd-text-primary` **An Event-Driven system** that enables dynamic behavior switching based on real-time environmental context.
- {material-regular}`rocket_launch;1.2em;sd-text-primary` **A Launcher and Monitor** that orchestrate multi-process or multi-threaded deployments with automatic lifecycle management.
- {material-regular}`code;1.2em;sd-text-primary` **A beautifully imperative Python API** for specifying system configurations as "Recipes" rather than XML launch files.
## How the Layers Work Together
The three layers form a vertical stack where each layer builds on the one below it:
1. **Sugarcoat (Architecture)** provides the execution primitives: Components, Topics, Events, Actions, Fallbacks, and the Launcher. Every node in the system --- whether it handles perception, planning, or control --- is a Sugarcoat Component with lifecycle management, health reporting, and self-healing capabilities.
2. **Kompass (Navigation)** builds on Sugarcoat's Component model to implement specialized navigation nodes: path planners, motion controllers, and drivers. These nodes communicate through Sugarcoat Topics, react to Sugarcoat Events, and recover from failures using Sugarcoat Fallbacks.
3. **EmbodiedAgents (Intelligence)** builds on the same Component model to implement cognitive nodes: vision-language models, semantic routers, and memory systems. These nodes can trigger navigation behaviors in Kompass, respond to navigation events, and share data through the common Topic infrastructure.
At runtime, all three layers are unified by the **Launcher**, which brings the complete system to life in a single Python script --- the Recipe. The Recipe declares which components to run, how they are wired together, what events to monitor, and what actions to take when conditions change. The result is a robot that can see, think, move, and adapt, all orchestrated from one coherent system.
## Recipes: The Developer Interface
A Recipe is a standard Python script that uses the EMOS API to declare an entire robotic application. Recipes are not just scripts; they are complete agentic workflows that combine intelligence, navigation, and system orchestration into a single, readable specification.
```python
from ros_sugar import Launcher
from ros_sugar.core import Event, Action
from ros_sugar.io import Topic
# Define components from any EMOS layer
# ... intelligence components from EmbodiedAgents
# ... navigation components from Kompass
# ... custom components built on Sugarcoat
# Wire them together with Topics, Events, and Actions
# Launch everything with a single call
launcher = Launcher(multi_processing=True)
launcher.add_pkg(components=[...], events_actions={...})
launcher.bringup()
```
This imperative, Pythonic approach replaces the traditional ROS2 workflow of XML launch files and YAML configurations with a single source of truth that is easy to read, version, and share.
```
## File: concepts/components.md
```markdown
# Components
**Stop writing boilerplate. Start writing core logic.**
In EMOS, a `Component` is the fundamental unit of execution. It replaces the standard ROS2 Node with a robust, **Lifecycle-Managed**, and **Self-Healing** entity designed for production-grade autonomy.
While a standard ROS2 node requires you to manually handle parameter callbacks, error catching, and state transitions, an EMOS Component handles this plumbing automatically, letting you focus entirely on your algorithm.
## Why Build with EMOS Components?
EMOS Components come with "superpowers" out of the box.
- {material-regular}`autorenew;1.5em;sd-text-primary` Lifecycle Native - Every component is a **Managed Lifecycle Node**. It supports `Configure`, `Activate`, `Deactivate`, and `Shutdown` states automatically, ensuring deterministic startup and shutdown.
- {material-regular}`healing;1.5em;sd-text-primary` Self-Healing - Components have a built-in "Immune System." If an algorithm fails or a driver disconnects, the component can trigger **[Fallbacks](status-and-fallbacks.md#fallback-strategies)** to restart or reconfigure itself without crashing the stack.
- {material-regular}`monitor_heart;1.5em;sd-text-primary` Health Aware - Components actively report their **[Health Status](status-and-fallbacks.md#health-status)** (Healthy, Algorithm Failure, etc.) to the system, enabling system-wide reflexes and alerts.
- {material-regular}`verified;1.5em;sd-text-primary` Type-Safe Config - Component configurations are validated using `attrs` models, catching type errors before runtime, and allowing easy Pythonic configuration in your recipe.
- {material-regular}`hub;1.5em;sd-text-primary` Auto-Wiring - Inputs and Outputs are declarative. Define a `Topic` as an input or output to your component, and EMOS automatically handles the subscription, serialization, and callback plumbing for you.
- {material-regular}`bolt;1.5em;sd-text-primary` Event-Driven - Components are reactive by design. They can be configured to execute their main logic only when triggered by an **[Event](events-and-actions.md)** or a Service call, rather than running in a continuous loop.
```{figure} /_static/images/diagrams/component_dark.png
:class: dark-only
:alt: component structure
:align: center
```
```{figure} /_static/images/diagrams/component_light.png
:class: light-only
:alt: component structure
:align: center
Component Architecture
```
## Execution Modes (Run Types)
A Component isn't just a `while(True)` loop. You can configure *how* its main functionality executes using the `run_type` property.
```{list-table}
:widths: 15 50 35
:header-rows: 1
* - Run Type
- Description
- Best For...
* - **Timed**
- Executes the main step in a fixed-frequency loop (e.g., 10Hz).
- Controllers, Planners, Drivers
* - **Event**
- Dormant until triggered by a specific Topic or Event.
- Image Processors, Detectors
* - **Server**
- Dormant until a ROS2 Service Request is received.
- Calibration Nodes, Compute Servers
* - **ActionServer**
- Dormant until a ROS2 Action Goal is received.
- Long-running tasks (Navigation, Arms)
```
**Configuration Example:**
```python
from ros_sugar.config import ComponentRunType
from ros_sugar.core import BaseComponent
# Can set from Component
comp = BaseComponent(component_name='test')
comp.run_type = "Server" # or ComponentRunType.SERVER
```
:::{tip} All the functionalities implemented in ROS2 nodes can be found in the Component.
:::
## Declarative Inputs & Outputs
Wiring up data streams shouldn't be tedious. EMOS allows you to define inputs and outputs declaratively.
When the component launches, it automatically creates the necessary publishers, subscribers, and type converters based on your definitions.
```python
from ros_sugar.core import BaseComponent
from ros_sugar.io import Topic
# 1. Define your interface
map_topic = Topic(name="map", msg_type="OccupancyGrid")
voice_topic = Topic(name="voice_cmd", msg_type="Audio")
image_topic = Topic(name="camera/rgb", msg_type="Image")
# 2. Auto-wire the component
# EMOS handles the QoS, callback groups, and serialization automatically
comp = BaseComponent(
component_name='audio_processor',
inputs=[map_topic, image_topic],
outputs=[voice_topic]
)
```
:::{tip}
EMOS provides built-in "Converters" for common ROS2 types (Images, Pose, etc.), so you can work with native Python objects instead of raw ROS2 messages.
:::
:::{seealso} Check the full configuration options of Topics [here](topics.md)
:::
## The Component Immune System: Health & Fallbacks
A robust robot doesn't just crash when an error occurs; it degrades gracefully.
### Health Status
Instead of printing a log message and dying, a Component reports its **Health Status**. This status is both:
- {material-regular}`settings;1.2em;sd-text-primary` **Internal:** Used immediately by the component to trigger local recovery strategies.
- {material-regular}`cell_tower;1.2em;sd-text-primary` **External:** Broadcasted to alert other parts of the system.
### Fallbacks (Self-Healing)
You can define **reflexes** that trigger automatically when health degrades.
* {material-regular}`restart_alt;1.2em;sd-text-danger` *Is the driver dead?* **Restart** the node.
* {material-regular}`tune;1.2em;sd-text-warning` *Is the planner stuck?* **Reconfigure** the tolerance parameters.
* {material-regular}`swap_horiz;1.2em;sd-text-primary` *Is the sensor noisy?* **Switch** to a different algorithm.
> **Learn More:** [Status & Fallbacks Guide](./status-and-fallbacks.md).
## Pro Tips for Component Devs
:::{admonition} Best Practices
:class: tip
* **Keep `__init__` Light:** Do not open heavy resources (cameras, models) in `__init__`. Use `custom_on_configure` or `custom_on_activate`. This allows your node to be introspected and configured *before* it starts consuming resources.
* **Always Report Status:** Make it a habit to call `self.health_status.set_healthy()` at the end of a successful `_execution_step`. This acts as a heartbeat for the system.
* **Catch, Don't Crash:** Wrap your main logic in `try/except` blocks. Instead of raising an exception, catch it and report `set_fail_algorithm`, for example. This keeps the process alive and allows your [Fallbacks](status-and-fallbacks.md) to kick in and save the day.
:::
```
## File: concepts/topics.md
```markdown
# Topics
**The connective tissue of your system.**
Topics are defined in EMOS with a `Topic` class that specifies the **Data Contract** (Type/Name of the ROS2 topic), the **Behavior** (QoS), and the **Freshness Constraints** (Timeout) for a specific stream of information.
Topics act as the bridge for both:
1. **Component I/O:** They define what data a Component produces or consumes.
2. **Event Triggers:** They act as the "Sensors" for the Event-Driven system, feeding data into the Blackboard.
## Why Use EMOS Topics?
- {material-regular}`link;1.5em;sd-text-primary` Declarative Wiring - No more hardcoded strings buried in your components. Define your Topics as shared resources and pass them into Components during configuration.
- {material-regular}`timer;1.5em;sd-text-primary` Freshness Monitoring - An EMOS Topic can enforce a `data_timeout`. If the data is too old, the Event system knows to ignore it, preventing "Stale Data" bugs.
- {material-regular}`auto_awesome;1.5em;sd-text-primary` Lazy Type Resolution - You don't need to import message classes at the top of every file. EMOS resolves types like `'OccupancyGrid'` or `'Odometry'` at runtime, keeping your code clean and decoupling dependencies.
- {material-regular}`tune;1.5em;sd-text-primary` QoS Abstraction - Quality of Service profiles are configured via simple Python objects directly in your recipe.
## Usage Example
```python
from ros_sugar.config import QoSConfig
from ros_sugar.io import Topic
qos_conf = QoSConfig(
history=qos.HistoryPolicy.KEEP_LAST,
queue_size=20,
reliability=qos.ReliabilityPolicy.BEST_EFFORT,
durability=qos.DurabilityPolicy.TRANSIENT_LOCAL
)
topic = Topic(name='/local_map', msg_type='OccupancyGrid', qos_profile=qos_conf)
```
## Advanced: Smart Type Resolution
One of EMOS's most convenient features is **String-Based Type Resolution**. In standard ROS2, you must import the specific message class (`from geometry_msgs.msg import Twist`) to create a publisher or subscriber. EMOS handles this import for you dynamically.
```python
from ros_sugar.io import Topic
from std_msgs.msg import String
# Method 1: The Standard Way (Explicit Class)
# Requires 'from std_msgs.msg import String'
topic_1 = Topic(name='/chatter', msg_type=String)
# Method 2: The EMOS Way (String Literal)
# No import required. EMOS finds 'std_msgs/msg/String' automatically.
topic_2 = Topic(name='/chatter', msg_type='String')
```
:::{seealso}
See the full list of automatically supported message types in the advanced types reference.
:::
## Component Integration
Once defined, Topics are passed to [Components](./components.md) to automatically generate the ROS2 infrastructure.
```python
from ros_sugar.core import BaseComponent
from ros_sugar.io import Topic
# When this component starts, it automatically creates:
# - A Subscriber to '/scan' (LaserScan)
# - A Publisher to '/cmd_vel' (Twist)
my_node = BaseComponent(
component_name="safety_controller",
inputs=[Topic(name="/scan", msg_type="LaserScan")],
outputs=[Topic(name="/cmd_vel", msg_type="Twist")]
)
```
```
## File: concepts/events-and-actions.md
```markdown
# Events & Actions
**Dynamic behavior switching based on real-time environmental context.**
EMOS's Event-Driven architecture enables dynamic behavior switching based on real-time environmental context. This allows robots to react instantly to changes in their internal state or external environment without complex, brittle if/else chains.
## Events
An Event in EMOS monitors a specific **ROS2 Topic**, and defines a triggering condition based on the incoming topic data. You can write natural Python expressions (e.g., `topic.msg.data > 5`) to define exactly when an event should trigger the associated Action(s).
- {material-regular}`hub;1.5em;sd-text-primary` Compose Logic - Combine triggers using simple Pythonic syntax (`(lidar_clear) & (goal_seen)`).
- {material-regular}`sync;1.5em;sd-text-primary` Fuse Data - Monitor multiple topics simultaneously via a synchronized **Blackboard** that ensures data freshness.
- {material-regular}`speed;1.5em;sd-text-primary` Stay Fast - All evaluation happens asynchronously in a dedicated worker pool. Your main component loop **never blocks**.
:::{admonition} Think in Behaviors
:class: tip
Events are designed to be read like a sentence:
*"If the battery is low AND we are far from home, THEN navigate to the charging dock."*
:::
:::{tip} Events can be paired with EMOS [`Action`](#actions)(s) or with any standard [ROS2 Launch Action](https://docs.ros.org/en/kilted/Tutorials/Intermediate/Launch/Using-Event-Handlers.html)
:::
### Defining Events
The Event API uses a fluent, expressive syntax that allows you to access ROS2 message attributes directly via `topic.msg`.
#### Basic Single-Topic Event
```python
from ros_sugar.core import Event
from ros_sugar.io import Topic
# 1. Define the Source
# `data_timeout` parameter is optional. It ensures data is considered "stale" after 0.5s
battery = Topic(name="/battery_level", msg_type="Float32", data_timeout=0.5)
# 2. Define the Event
# Triggers when percentage drops below 20%
low_batt_event = Event(battery.msg.data < 20.0)
```
#### Composed Conditions (Logic & Multi-Topic)
You can combine multiple conditions using standard Python bitwise operators (`&`, `|`, `~`) to create complex behavioral triggers. Events can also span multiple different topics. EMOS automatically manages a "Blackboard" of the latest messages from all involved topics, ensuring synchronization and data "freshness".
- **Example**: Trigger a "Stop" event only if an obstacle is detected AND the robot is currently in "Auto" mode.
```python
from ros_sugar.core import Event
from ros_sugar.io import Topic
lidar_topic = Topic(name="/person_detected", msg_type="Bool", data_timeout=0.5)
status_topic = Topic(name="/robot_mode", msg_type="String", data_timeout=60.0)
# Complex Multi-Topic Condition
emergency_stop_event = Event((lidar_topic.msg.data.is_true()) & (status_topic.msg.data == "AUTO"))
```
:::{admonition} Handling Stale Data
:class: warning
When combining multiple topics, data synchronization is critical. Use the `data_timeout` parameter on your `Topic` definition to ensure you never act on old sensor data.
:::
### Event Configuration
Refine *when* and *how* the event triggers using these parameters:
* {material-regular}`change_circle` On Change (`on_change=True`) - Triggers **only** when the condition transitions from `False` to `True` (Edge Trigger). Useful for state transitions (e.g., "Goal Reached") rather than continuous firing.
* {material-regular}`all_inclusive` On Any (`Topic`) - If you pass the `Topic` object itself as the condition, the event triggers on **every received message**, regardless of content.
* {material-regular}`looks_one` Handle Once (`handle_once=True`) - The event will fire exactly one time during the lifecycle of the system. Useful for initialization sequences.
* {material-regular}`timer` Event Delay (`keep_event_delay=2.0`) - Prevents rapid firing (debouncing). Ignores subsequent triggers for the specified duration (in seconds).
### Supported Conditional Operators
You can use standard Python operators or specific helper methods on any topic attribute to define the event triggering condition.
| Operator / Method | Description | Example |
| :--- | :--- | :--- |
| **`==`**, **`!=`** | Equality checks. | `topic.msg.status == "IDLE"` |
| **`>`**, **`>=`**, **`<`**, **`<=`** | Numeric comparisons. | `topic.msg.temperature > 75.0` |
| **`.is_true()`** | Boolean True check. | `topic.msg.is_ready.is_true()` |
| **`.is_false()`**, **`~`** | Boolean False check. | `topic.msg.is_ready.is_false()` or `~topic.msg.is_ready` |
| **`.is_in(list)`** | Value exists in a list. | `topic.msg.mode.is_in(["AUTO", "TELEOP"])` |
| **`.not_in(list)`** | Value is not in a list. | `topic.msg.id.not_in([0, 1])` |
| **`.contains(val)`** | String/List contains a value. | `topic.msg.description.contains("error")` |
| **`.contains_any(list)`** | List contains *at least one* of the values. | `topic.msg.error_codes.contains_any([404, 500])` |
| **`.contains_all(list)`** | List contains *all* of the values. | `topic.msg.detections.labels.contains_all(["window", "desk"])` |
| **`.not_contains_any(list)`** | List contains *none* of the values. | `topic.msg.active_ids.not_contains_any([99, 100])` |
### Event Usage Examples
#### Automatic Adaptation (Terrain Switching)
Scenario: A perception or ML node publishes a string to `/terrain_type`. We want to change the robot's gait when the terrain changes.
```{code-block} python
:caption: quadruped_controller.py
:linenos:
from typing import Literal
from ros_sugar.component import BaseComponent
class QuadrupedController(BaseComponent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Some logic
def switch_gait_controller(self, controller_type: Literal['stairs', 'sand', 'snow', 'gravel']):
self.get_logger().info("New terrain detected! Switching gait.")
# Logic to change controller parameters...
```
```{code-block} python
:caption: quadruped_controller_recipe.py
:linenos:
from my_pkg.components import QuadrupedController
from ros_sugar.core import Event, Action
from ros_sugar.io import Topic
from ros_sugar import Launcher
quad_controller = QuadrupedController(component_name="quadruped_controller")
# Define the Event Topic
terrain_topic = Topic(name="/terrain_type", msg_type="String")
# Define the Event
# Logic: Trigger when the detected terrain changes
# on_change=True ensures we only trigger the switch the FIRST time stairs are seen.
# Add an optional delay to prevent rapid event triggering
event_terrain_changed = Event(terrain_topic, on_change=True, keep_event_delay=60.0)
# Define the Action
# Call self.switch_gait_controller() when triggered and pass the detected terrain to the method
change_gait_action = Action(method=self.activate_stairs_controller, args=(terrain_topic.msg.data))
# Register
my_launcher = Launcher()
my_launcher.add_pkg(
components=[quad_controller],
events_actions={stairs_event: change_gait_action},
)
```
#### Autonomous Drone Safety
Scenario: An autonomous drone **stops** if an obstacle is close OR the bumper is hit. It also sends a warning if the battery is low AND we are far from the land.
```python
from ros_sugar.core import Event, Action
from ros_sugar.io import Topic
# --- Topics ---
proximity_sensor = Topic(name="/radar_front", msg_type="Float32", data_timeout=0.2)
bumper = Topic(name="/bumper", msg_type="Bool", data_timeout=0.1)
battery = Topic(name="/battery", msg_type="Float32")
location = Topic(name="/pose", msg_type="Pose")
# --- Conditions ---
# 1. Safety Condition (Composite OR)
# Stop if proximity_sensor < 0.2m OR Bumper is Hit
is_danger = (proximity_sensor.msg.data < 0.2) | (bumper.msg.data.is_true())
# 2. Return Home Condition (Composite AND)
# Return if Battery < 20% AND Distance > 100m
needs_return = (battery.msg.data < 20.0) & (location.position.z > 100.0)
# --- Events ---
safety_event = Event(is_danger)
return_event = Event(needs_return, on_change=True)
```
---
## Events from Internal State (a.k.a Generic Events)
Most events watch a topic. Sometimes you need an event whose firing condition is **internal state** that is not being published as a ROS topic -- for example a hardware monitor or a compound signal that requires arbitrary calculation. For these cases, an `Event` accepts a third kind of condition alongside topic predicates: **any Python callable that returns `bool`**, polled at a configurable rate.
```python
from ros_sugar.event import Event
from ros_sugar.actions import log
def is_overheating() -> bool: # must be type-annotated as bool
return read_temperature() > 75.0 # any internal state goes here
# Fires when ``is_overheating()`` returns True; polled twice a second.
event_overheat = Event(is_overheating, check_rate=2.0)
events_actions = {
event_overheat: log(msg="Overheating -- backing off"),
}
```
Two rules: the callable's return type annotation **must be `bool`**, and the callable **cannot be a `@component_action`** method bound to a managed component. Use a plain function or a regular instance method. The polling loop runs on the central Monitor; `check_rate` is in Hz and defaults to the Monitor's loop rate when omitted.
```{seealso}
For a complete worked recipe -- including how to feed the predicate's state from elsewhere in the graph and what the event looks like in the Web UI -- see [Internal-State Events](../recipes/events-and-resilience/internal-state-events.md).
```
Use this when:
- The trigger condition is **internal state** that has no business being a topic.
- You want **derived predicates** that span multiple sources, easier to express as a Python callable that makes arbitrary calculations on them rather than as a chain of topic conditions.
- You want **encapsulation** -- keep the predicate state inside the recipe rather than pushing it onto a topic just so an event can watch it.
---
## Actions
**Executable context-aware behaviors for your robotic system.**
Actions are not just static function calls; they are **dynamic, context-aware routines** that can adapt their parameters in real-time based on live system data.
They can represent:
- {material-regular}`smart_toy;1.2em;sd-text-primary` Component Behaviors — Routines defined within your components. *e.g., Stopping the robot, executing a motion pattern, or saying a sentence.*
- {material-regular}`settings;1.2em;sd-text-primary` System Behaviors — Lifecycle management, configuration and plumbing. *e.g., Reconfiguring a node, restarting a driver, or re-routing input streams.*
- {material-regular}`extension;1.2em;sd-text-primary` User Custom Behaviors — Arbitrary Python functions. *e.g., Calling an external REST API, logging to a file, or sending a slack notification.*
### Trigger Mechanisms
Actions sit dormant until activated by one of two mechanisms:
- {material-regular}`flash_on;1.2em;sd-text-primary` Event-Driven (Reflexive) - Triggered instantly when a specific **Event** condition is met.
**Example:** "Obstacle Detected" $\rightarrow$ `stop_robot()`
- {material-regular}`healing;1.2em;sd-text-primary` Fallback-Driven (Restorative) - Triggered automatically by a Component when its internal **Health Status** degrades.
**Example:** "Camera Driver Failed" $\rightarrow$ `restart_driver()`
### The `Action` Class
At its core, the `Action` class is a wrapper around any Python callable. It packages a function along with its arguments, preparing them for execution at runtime.
But unlike standard Python functions, EMOS Actions possess a superpower: [Dynamic Data Injection](#dynamic-data-injection). You can bind their arguments directly to live ROS2 Topics, allowing the Action to fetch the latest topic message or a specific message argument the moment it triggers.
```python
class Action:
def __init__(self, method: Callable, args: tuple = (), kwargs: Optional[Dict] = None):
```
- `method`: The function or routine to execute.
- `args`: Positional arguments (can be static values OR dynamic Topic values).
- `kwargs`: Keyword arguments (can be static values OR dynamic Topic values).
### Basic Usage
```python
from ros_sugar.component import BaseComponent
from ros_sugar.core import Action
import logging
def custom_routine():
logging.info("I am executing an action!")
my_component = BaseComponent(node_name='test_component')
# 1. Component Method
action1 = Action(method=my_component.start)
# 2. Method with keyword arguments
action2 = Action(method=my_component.update_parameter, kwargs={"param_name": "fallback_rate", "new_value": 1000})
# 3. External Function
action3 = Action(method=custom_routine)
```
### Dynamic Data Injection
**This is EMOS's superpower.**
You can create complex, context-aware behaviors without writing any "glue code" or custom parsers.
When you bind an Action argument to a `Topic`, the system automatically resolves the binding at runtime, fetching the current value from the topic attributes and injecting it into your function.
#### Example: Cross-Topic Data Access
**Scenario**: An event occurs on Topic 1. You want to log a message that includes the current status from Topic 2 and a sensor reading from Topic 3.
```python
from ros_sugar.core import Event, Action
from ros_sugar.io import Topic
# 1. Define Topics
topic_1 = Topic(name="system_alarm", msg_type="Bool")
topic_2 = Topic(name="robot_mode", msg_type="String")
topic_3 = Topic(name="battery_voltage", msg_type="Float32")
# 2. Define the Event
# Trigger when Topic 1 becomes True
event_on_first_topic = Event(topic_1.msg.data.is_true())
# 3. Define the Target Function
def log_context_message(mode, voltage):
print(f"System Alarm! Current Mode: {mode}, Voltage: {voltage}V")
# 4. Define the Dynamic Action
# We bind the function arguments directly to the data fields of Topic 2 and Topic 3
my_action = Action(
method=log_context_message,
# At runtime, these are replaced by the actual values from the topics
args=(topic_2.msg.data, topic_3.msg.data)
)
```
### Pre-defined Actions
EMOS provides a suite of pre-defined, thread-safe actions for managing components and system resources via the `ros_sugar.actions` module.
:::{admonition} Import Note
:class: tip
All pre-defined actions are **keyword-only** arguments. They can be imported directly:
`from ros_sugar.actions import start, stop, reconfigure`
:::
#### Component-Level Actions
These actions directly manipulate the state or configuration of a specific `BaseComponent` derived object.
| Action Method | Arguments | Description |
| :-------------------------------------- | :------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
| **`start`** | `component` | Triggers the component's Lifecycle transition to **Active**. |
| **`stop`** | `component` | Triggers the component's Lifecycle transition to **Inactive**. |
| **`restart`** | `component`
`wait_time` (opt) | Stops the component, waits `wait_time` seconds (default 0), and Starts it again. |
| **`reconfigure`** | `component`
`new_config`
`keep_alive` | Reloads the component with a new configuration object or file path.
`keep_alive=True` (default) keeps the node running during update. |
| **`update_parameter`** | `component`
`param_name`
`new_value`
`keep_alive` | Updates a **single** configuration parameter. |
| **`update_parameters`** | `component`
`params_names`
`new_values`
`keep_alive` | Updates **multiple** configuration parameters simultaneously. |
| **`send_component_service_request`** | `component`
`srv_request_msg` | Sends a request to the component's main service with a specific message. |
| **`trigger_component_service`** | `component` | Triggers the component's main service.
Creates the request message dynamically during runtime from the incoming Event topic data. |
| **`send_component_action_server_goal`** | `component`
`request_msg` | Sends a goal to the component's main action server with a specific message. |
| **`trigger_component_action_server`** | `component` | Triggers the component's main action server.
Creates the request message dynamically during runtime from the incoming Event topic data. |
#### System-Level Actions
These actions interact with the broader ROS2 system and are executed by the central `Monitor`.
| Action Method | Arguments | Description |
| :-------------------------- | :---------------------------------------------- | :----------------------------------------------------------------------- |
| **`log`** | `msg`
`logger_name` (opt) | Logs a message to the ROS console. |
| **`publish_message`** | `topic`
`msg`
`publish_rate`/`period` | Publishes a specific message to a topic. Can be single-shot or periodic. |
| **`send_srv_request`** | `srv_name`
`srv_type`
`srv_request_msg` | Sends a request to a ROS 2 Service with a specific message. |
| **`trigger_service`** | `srv_name`
`srv_type` | Triggers the a given ROS2 service. |
| **`send_action_goal`** | `server_name`
`server_type`
`request_msg` | Sends a specific goal to a ROS 2 Action Server. |
| **`trigger_action_server`** | `server_name`
`server_type` | Triggers a given ROS2 action server. |
:::{admonition} Automatic Data Conversion
:class: note
When using **`trigger_*`** actions paired with an Event, EMOS attempts to create the required service/action request from the incoming Event topic data automatically via **duck typing**.
If automatic conversion is not possible, or if the action is not paired with an Event, it sends a default (empty) request.
:::
```
## File: concepts/status-and-fallbacks.md
```markdown
# Status & Fallbacks
**All robots can fail, but smart robots recover.**
EMOS components are **Self-Aware** and **Self-Healing** by design. The Health Status system allows every component to explicitly declare its operational state --- not just "Alive" or "Dead," but _how_ it is functioning. When failures are detected, the Fallback system automatically triggers pre-configured recovery strategies, keeping the robot operational without human intervention.
---
## Health Status
The **Health Status** is the heartbeat of an EMOS component. Unlike standard ROS2 nodes, EMOS components differentiate between a math error (Algorithm Failure), a hardware crash (Component Failure), or a missing input (System Failure).
These reports are broadcast back to the system to trigger:
- {material-regular}`notifications;1.2em;sd-text-warning` **Alerts:** Notify the operator of specific issues.
- {material-regular}`flash_on;1.2em;sd-text-primary` **Reflexes:** Trigger [Events](events-and-actions.md) to handle the situation.
- {material-regular}`healing;1.2em;sd-text-success` **Self-Healing:** Execute automatic [Fallbacks](#fallback-strategies) to recover the node.
### Status Hierarchy
EMOS defines distinct failure levels to help you pinpoint the root cause of an issue.
- {material-regular}`check_circle;1.5em;sd-text-success` HEALTHY
**"Everything is awesome."**
The component executed its main loop successfully and produced valid output.
- {material-regular}`warning;1.5em;sd-text-warning` ALGORITHM_FAILURE
**"I ran, but I couldn't solve it."**
The node is healthy, but the logic failed.
_Examples:_ Path planner couldn't find a path; Object detector found nothing; Optimization solver did not converge.
- {material-regular}`error;1.5em;sd-text-danger` COMPONENT_FAILURE
**"I am broken."**
An internal crash or hardware issue occurred within this specific node.
_Examples:_ Memory leak; Exception raised in a callback; Division by zero.
- {material-regular}`link_off;1.5em;sd-text-primary` SYSTEM_FAILURE
**"I am fine, but my inputs are broken."**
The failure is caused by an external dependency.
_Examples:_ Input topic is empty or stale; Network is down; Disk is full.
### Reporting Status
Every `BaseComponent` has an internal `self.health_status` object. You interact with this object inside your `_execution_step` or callbacks to declare the current state.
#### The Happy Path
Always mark the component as healthy at the end of a successful execution. This resets any previous error counters.
```python
self.health_status.set_healthy()
```
#### Declaring Failures
When things go wrong, be specific. This helps the Fallback system decide whether to _Retry_ (Algorithm), _Restart_ (Component), or _Wait_ (System).
**Algorithm Failure:**
```python
# Optional: List the specific algorithm that failed
self.health_status.set_fail_algorithm(algorithm_names=["A_Star_Planner"])
```
**Component Failure:**
```python
# Report that this component crashed
self.health_status.set_fail_component()
# Or blame a sub-module
self.health_status.set_fail_component(component_names=["Camera_Driver_API"])
```
**System Failure:**
```python
# Report missing data on specific topics
self.health_status.set_fail_system(topic_names=["/camera/rgb", "/odom"])
```
### Automatic Broadcasting
You do not need to manually publish the status message.
EMOS automatically broadcasts the status at the start of every execution step. This ensures a consistent "Heartbeat" frequency, even if your algorithm blocks or hangs (up to the threading limits).
:::{tip}
If you need to trigger an immediate alert from a deeply nested callback or a separate thread, you _can_ force a publish:
`self.health_status_publisher.publish(self.health_status())`
:::
### Implementation Pattern
Here is the robust pattern for writing an execution step using Health Status. This pattern enables the **Self-Healing** capabilities of EMOS.
```python
def _execution_step(self):
try:
# 1. Check Pre-conditions (System Level)
if self.input_image is None:
self.get_logger().warn("Waiting for video stream...")
self.health_status.set_fail_system(topic_names=[self.input_image.name])
return
# 2. Run Logic
result = self.ai_model.detect(self.input_image)
# 3. Check Logic Output (Algorithm Level)
if result is None or len(result.detections) == 0:
self.health_status.set_fail_algorithm(algorithm_names=["yolo_detector"])
return
# 4. Success!
self.publish_result(result)
self.health_status.set_healthy()
except ConnectionError:
# 5. Handle Crashes (Component Level)
# This will trigger the 'on_component_fail' fallback (e.g., Restart)
self.get_logger().error("Camera hardware disconnected!")
self.health_status.set_fail_component(component_names=["hardware_interface"])
```
---
## Fallback Strategies
Fallbacks are the **Self-Healing Mechanism** of an EMOS component. They define the specific set of [Actions](events-and-actions.md#actions) to execute automatically when a failure is detected in the component's Health Status.
Instead of crashing or freezing when an error occurs, a Component can be configured to attempt intelligent recovery strategies:
- {material-regular}`swap_horiz;1.2em;sd-text-warning` _Algorithm stuck?_ $\rightarrow$ **Switch** to a simpler backup.
- {material-regular}`restart_alt;1.2em;sd-text-danger` _Driver disconnected?_ $\rightarrow$ **Re-initialize** the hardware.
- {material-regular}`autorenew;1.2em;sd-text-primary` _Sensor timeout?_ $\rightarrow$ **Restart** the node.
```{figure} /_static/images/diagrams/fallbacks_dark.png
:class: dark-only
:alt: fig-fallbacks
:align: center
```
```{figure} /_static/images/diagrams/fallbacks_light.png
:class: light-only
:alt: fig-fallbacks
:align: center
The Self-Healing Loop
```
### The Recovery Hierarchy
When a component reports a failure, EMOS doesn't just panic. It checks for a registered fallback strategy in a specific order of priority.
This allows you to define granular responses for different types of errors.
- {material-regular}`link_off;1.5em;sd-text-primary` 1. System Failure `on_system_fail`
**The Context is Broken.**
External failures like missing input topics or disk full.
_Example Strategy:_ Wait for data, or restart the data pipeline.
- {material-regular}`error;1.5em;sd-text-danger` 2. Component Failure `on_component_fail`
**The Node is Broken.**
Internal crashes or hardware disconnects.
_Example Strategy:_ Restart the component lifecycle or re-initialize drivers.
- {material-regular}`warning;1.5em;sd-text-warning` 3. Algorithm Failure `on_algorithm_fail`
**The Logic is Broken.**
The code ran but couldn't solve the problem (e.g., path not found).
_Example Strategy:_ Reconfigure parameters (looser tolerance) or switch algorithms.
- {material-regular}`help_center;1.5em;sd-text-secondary` 4. Catch-All `on_fail`
**Generic Safety Net.**
If no specific handler is found above, this fallback is executed.
_Example Strategy:_ Log an error or stop the robot.
### Recovery Strategies
A Fallback isn't just a single function call. It is a robust policy defined by **Actions** and **Retries**.
#### The Persistent Retry (Single Action)
_Try, try again._
The system executes the action repeatedly until it returns `True` (success) or `max_retries` is reached.
```python
# Try to restart the driver up to 3 times
driver.on_component_fail(fallback=restart(component=driver), max_retries=3)
```
#### The Escalation Ladder (List of Actions)
_If at first you don't succeed, try something stronger._
You can define a sequence of actions. If the first one fails (after its retries), the system moves to the next one.
1. **Clear Costmaps** (Low cost, fast)
2. **Reconfigure Planner** (Medium cost)
3. **Restart Planner Node** (High cost, slow)
```python
# Tiered Recovery for a Navigation Planner
planner.on_algorithm_fail(
fallback=[
Action(method=planner.clear_costmaps), # Step 1
Action(method=planner.switch_to_fallback), # Step 2
restart(component=planner) # Step 3
],
max_retries=1 # Try each step once before escalating
)
```
#### The "Give Up" State
If all strategies fail (all retries of all actions exhausted), the component enters the **Give Up** state and executes the `on_giveup` action. This is the "End of Line", usually used to park the robot safely or alert a human.
### How to Implement Fallbacks
#### Method A: In Your Recipe (Recommended)
You can configure fallbacks externally without touching the component code. This makes your system modular and reusable.
```python
from ros_sugar.actions import restart, log
# 1. Define component
lidar = BaseComponent(component_name='lidar_driver')
# 2. Attach Fallbacks
# If it crashes, restart it (Unlimited retries)
lidar.on_component_fail(fallback=restart(component=lidar))
# If data is missing (System), just log it and wait
lidar.on_system_fail(fallback=log(msg="Waiting for Lidar data..."))
# If all else fails, scream
lidar.on_giveup(fallback=log(msg="LIDAR IS DEAD. STOPPING ROBOT."))
```
#### Method B: In Component Class (Advanced)
For tightly coupled recovery logic (like re-handshaking a specific serial protocol), you can define custom fallback methods inside your class.
:::{tip}
Use the `@component_fallback` decorator. It ensures the method is only called when the component is in a valid state to handle it.
:::
```python
from ros_sugar.core import BaseComponent, component_fallback
from ros_sugar.core import Action
class MyDriver(BaseComponent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Register the custom fallback internally
self.on_system_fail(
fallback=Action(self.try_reconnect),
max_retries=3
)
def _execution_step(self):
try:
self.hw.read()
self.health_status.set_healthy()
except ConnectionError:
# This trigger starts the fallback loop!
self.health_status.set_fail_system()
@component_fallback
def try_reconnect(self) -> bool:
"""Custom recovery logic"""
self.get_logger().info("Attempting handshake...")
if self.hw.connect():
return True # Recovery Succeeded!
return False # Recovery Failed, will retry...
```
---
## Process-Level Recovery
The Health-Status / Fallback system above operates **inside a running component**. If a component goes further than that and the _entire process_ crashes -- a segfault in a native dependency, a Python `os._exit`, an OOM kill -- there is nothing left running to dispatch a fallback. EMOS adds a layer underneath for exactly this case.
### Respawning crashed processes -- `Launcher.on_process_fail`
`Launcher.on_process_fail()` enables process-level respawning for every component the launcher started in multiprocessing mode. If a component process exits with a non-zero status outside of normal shutdown, the launcher relaunches it.
```python
launcher = Launcher()
launcher.add_pkg(components=[...], multiprocessing=True, package_name="...")
launcher.on_process_fail(max_retries=3) # respawn up to 3 times per component
launcher.bringup()
```
User-initiated shutdowns (Ctrl-C, SIGTERM) do not count as failures, so this won't fight you when you stop the recipe yourself.
```{tip}
Pair `on_process_fail` with `executor_spin_timeout` (see [Launcher](launcher.md)) when running latency-sensitive callbacks -- the latter prevents a slow callback from looking like a stalled process and tripping the respawn unnecessarily.
```
```
## File: concepts/launcher.md
```markdown
# Launcher & Orchestration
**Recipes: One script to rule them all.**
The `Launcher` is your entry point to the EMOS ecosystem. It provides a clean, Pythonic API to configure, spawn, and orchestrate your ROS2 nodes without writing XML or complex launch files.
Under the hood, every Launcher spawns an internal **Monitor** node. This hidden "Brain" is responsible for tracking component health, listening for events, and executing the orchestration logic.
## Execution Architectures
The Launcher supports two execution modes, configured via the `multi_processing` flag.
::::{tab-set}
:::{tab-item} Multi-Threaded
:sync: threaded
**Default for Debugging** (`multi_processing=False`)
All components run in the same process as the Launcher and Monitor.
* **Pros:** Fast startup, shared memory, easy debugging (breakpoints work everywhere).
* **Cons:** The Global Interpreter Lock (GIL) can bottleneck performance if you have many heavy nodes.
```{figure} /_static/images/diagrams/multi_threaded_dark.png
:class: dark-only
:alt: multi-threaded architecture
:align: center
```
```{figure} /_static/images/diagrams/multi_threaded_light.png
:class: light-only
:alt: multi-threaded architecture
:align: center
Multi-threaded Execution
```
:::
:::{tab-item} Multi-Process
:sync: process
**Production Mode** (`multi_processing=True`)
Each component runs in its own isolated process. The Monitor still runs in the same process as the Launcher.
* **Pros:** True parallelism, crash isolation (one node crashing doesn't kill the system).
* **Cons:** Higher startup overhead.
```{figure} /_static/images/diagrams/multi_process_dark.png
:class: dark-only
:alt: multi-process architecture
:align: center
```
```{figure} /_static/images/diagrams/multi_process_light.png
:class: light-only
:alt: multi-process architecture
:align: center
Multi-process Execution
```
:::
::::
## Launcher Features
### 1. Package & Component Loading
You can add components from your current script or external packages.
```python
# Add from an external entry point (for multi-process separation)
launcher.add_pkg(
package_name="my_robot_pkg",
components=[vision_component] # Pass config/events here
multiprocessing=True
)
```
### 2. Lifecycle Management
EMOS components are Lifecycle nodes. The Launcher handles the transition state machine for you.
* `activate_all_components_on_start=True`: Automatically transitions all nodes to **Active** after spawning.
### 3. Global Fallbacks
Define "Catch-All" policies for the entire system.
```python
# If ANY component reports a crash, restart it.
launcher.on_component_fail(action_name="restart")
```
For the further case where an entire **process** crashes (segfault, OOM, native exception) and there is nothing left in-process to dispatch a Fallback, the launcher provides `Launcher.on_process_fail()`. The launcher relaunches any component process that exits with a non-zero status outside of normal shutdown:
```python
launcher.on_process_fail(max_retries=3) # respawn up to 3 times per component
```
See [Process-Level Recovery](status-and-fallbacks.md#process-level-recovery) for the full picture.
### 4. Executor Spin Timeout
```python
launcher = Launcher(
multi_processing=True,
executor_spin_timeout=0.05, # seconds
)
```
Each component runs a ROS2 executor that spins its callbacks. The executor blocks for at most this many seconds per spin call regardless of how fast the component's main loop is running. Lower values reduce callback latency at the cost of CPU; the default works for most recipes. Tune this when running latency-sensitive callbacks alongside `on_process_fail` to avoid mistaking a slow callback for a stalled process.
### 5. Events Orchestration
Pass your events/actions dictionary **once** to the `Launcher` and it will handle delegating the event monitoring to the concerned component.
## Complete Usage Example
```python
from ros_sugar.core import BaseComponent, Event, Action
from ros_sugar.actions import log, restart
from ros_sugar.io import Topic
from ros_sugar import Launcher
# 1. Define Components
# (Usually imported from your package)
driver = BaseComponent(component_name='lidar_driver')
planner = BaseComponent(component_name='path_planner')
# Set Fallback Policy
# If the driver crashes, try to restart it automatically
driver.on_component_fail(fallback=restart(component=driver))
# 2. Define Logic for Events
battery = Topic(name="/battery", msg_type="Float32")
low_batt_evt = Event(battery.msg.data < 15.0)
log_action = log(msg="WARNING: Battery Low!")
# 3. Initialize Launcher
launcher = Launcher(
config_file='config/robot_params.toml', # Can optionally pass a configuration file
activate_all_components_on_start=True,
multi_processing=True # Use separate processes
)
# 4. Register Components
# You can attach specific events to specific groups of components
launcher.add_pkg(
components=[driver, planner],
ros_log_level="error",
events_actions={low_batt_evt: log_action}
)
# 5. Launch!
# This blocks until Ctrl+C is pressed
launcher.bringup()
```
## The Monitor (Internal Engine)
:::{note}
The Monitor is configured automatically. You do not need to instantiate or manage it manually.
:::
The **Monitor** is a specialized, non-lifecycle ROS2 node that acts as the central management node.
**Responsibilities:**
1. {material-regular}`play_arrow;1.2em;sd-text-primary` **Custom Actions Execution:** Handles executing custom Actions defined in the recipe.
2. {material-regular}`monitor_heart;1.2em;sd-text-primary` **Health Tracking:** Subscribes to the `/status` topic of every component.
3. {material-regular}`hub;1.2em;sd-text-primary` **Orchestration:** Holds clients for every component's Lifecycle and Parameter services, allowing it to restart, reconfigure, or stop nodes on demand.
**Architecture:**
::::{tab-set}
:::{tab-item} Configuration
:sync: config
How the Launcher configures the Monitor with Events and Actions at startup.
```{figure} /_static/images/diagrams/events_actions_config_dark.png
:class: dark-only
:alt: Monitoring events diagram
:align: center
:scale: 70
```
```{figure} /_static/images/diagrams/events_actions_config_light.png
:class: light-only
:alt: Monitoring events diagram
:align: center
:scale: 70
Monitoring events
```
:::
:::{tab-item} Execution
:sync: exec
How the Monitor processes triggers and executes actions at runtime.
```{figure} /_static/images/diagrams/events_actions_exec_dark.png
:class: dark-only
:alt: An Event Trigger diagram
:align: center
:scale: 70
```
```{figure} /_static/images/diagrams/events_actions_exec_light.png
:class: light-only
:alt: An Event Trigger diagram
:align: center
:scale: 70
An Event Trigger
```
:::
::::
```
## File: concepts/robot-plugins.md
```markdown
# Robot Plugins
EMOS is built to be **robot-agnostic** -- recipes are written against standard, hardware-independent interfaces, so the same behaviour runs on any robot. In practice, though, robot manufacturers often expose their hardware through **custom interfaces**: bespoke ROS 2 messages and services, or entirely non-ROS protocols like UDP, HTTP, or a vendor SDK.
{material-regular}`extension;1.2em;sd-text-primary` **Robot Plugins exist to bridge that gap.** A plugin adapts a manufacturer's custom interface to EMOS's standard one, making it trivial to bring a new robot online without touching your recipes. A plugin can also ship the robot's **preconfigured actions** -- a gait change on a quadruped, a docking routine, an arm stow -- ready for recipes (and [Cortex](../intelligence/cortex.md)) to call by name.
---
## What Are Robot Plugins?
A Robot Plugin is the **translation layer** between an EMOS recipe and a specific robot. It maps the robot's real interfaces onto the standard component I/O that recipes use (`Twist`, `Odometry`, `Imu`, …), so your recipe code never changes when the hardware does -- and it surfaces the robot's high-level **actions** (`stand_up`, `dock`, gait change) and **events** (`low_battery`) for recipes and Cortex to consume directly.
```{note}
This page describes the current plugin framework (Sugarcoat 0.7+): a plugin is a Python **class** you pass to the `Launcher`. It supersedes the earlier dictionary-based `robot_feedback` / `robot_action` API.
```
---
## Why Robot Plugins?
- {material-regular}`swap_horiz;1.2em;sd-text-primary` **Portability** -- Write the recipe once against standard types; switch robots by swapping a single plugin object.
- {material-regular}`auto_fix_high;1.2em;sd-text-primary` **Simplicity** -- The plugin hides all the type conversions, sockets, and service calls behind the scenes.
- {material-regular}`widgets;1.2em;sd-text-primary` **Modularity** -- Keep hardware-specific logic isolated in its own package.
---
## How a Plugin Works
A plugin is a subclass of `ros_sugar.robot.RobotPlugin`. Its `__init__` is **declarative** -- it only *describes* the robot (endpoints, transports, decoders) and does no I/O, so it can be serialized and rebuilt inside component subprocesses. The `Launcher` then runs it in one of two roles automatically:
- {material-regular}`dns;1.2em;sd-text-primary` **HOST** -- lives in the launcher process. Owns the real transports (binds sockets, opens sessions, runs heartbeats), decodes telemetry once, and publishes it on a feedback bus.
- {material-regular}`memory;1.2em;sd-text-primary` **CLIENT** -- lives in each component subprocess. Opens no sockets; it consumes decoded feedback from the bus and sends commands.
During activation, every component's standard input/output topics are matched against the plugin: a ROS-topic match re-points the native subscriber/publisher, and any other transport is bridged through the feedback bus. Components stay unaware their data isn't plain ROS.
---
## Anatomy of a Plugin
| Building block | Role |
| :------------- | :--- |
| `RobotPlugin` | The plugin itself -- subclass this. |
| `Transport` | Where data comes from / goes to: `UdpTransport`, `HttpTransport`, `SdkCallbackTransport`, `RosTopicTransport`, `RosServiceTransport`. |
| `Feedback` | One telemetry stream -- a standard type, a transport, and a decoder (raw payload → ROS message). |
| `RobotCommand` | One command surface -- a standard type, a transport, and an encoder (component output → wire payload). |
| `ActionRegistry` / `EventRegistry` | Named factories that produce the robot's `Action` / `Event` objects. |
| `create_supported_type` | Wraps a robot's custom ROS message as a standard `SupportedType`. |
`Feedback` and `RobotCommand` are keyed by the **standard message-type name** they stand in for (`Twist`, `Odometry`, …) -- that is how the framework matches them to component I/O.
---
## Using a Plugin in a Recipe
Hand a plugin instance to the `Launcher` -- that is the only recipe change:
```python
from ros_sugar.launch import Launcher
from myrobot_plugin import MyRobotPlugin
plugin = MyRobotPlugin() # declarative, zero-arg
launcher = Launcher(robot_plugin=plugin)
launcher.add_pkg(components=[planner, controller], multiprocessing=True)
# Plugin-provided events and actions wire up like anything else
launcher.on(plugin.events.low_battery(0.15), plugin.actions.dock())
launcher.bringup()
```
EMOS hosts the plugin, propagates it to every component, and translates all topic I/O through it -- every subscription and publication in your recipe is routed via the plugin automatically.
---
## Installing a Plugin
You don't have to build a plugin to use one -- the Automatika catalog ships ready-made plugins you install with one command (or one click in the dashboard):
```bash
emos plugin install emos-plugin-example
```
See [Robot Plugins (install & manage)](../getting-started/plugins.md) for the full flow.
---
## Writing Your Own
The reference plugin, [`emos-plugin-example`](https://github.com/automatika-robotics/emos-plugin-example), implements one robot across **all** the transport families (UDP telemetry + velocity command, a ROS-topic battery feedback, and a ROS-service docking action), with a mock robot and an end-to-end test suite. Copy it and adapt.
The full step-by-step authoring guide -- wrapping custom message types, defining transports/feedbacks/commands, contributing actions and events, and introspection -- lives in the Sugarcoat docs: [**Creating a Robot Plugin**](https://github.com/automatika-robotics/sugarcoat/blob/main/docs/development/custom_robot_plugin.md).
You can introspect any plugin's exposed surface from the command line:
```bash
python -m ros_sugar.robot inspect myrobot_plugin:MyRobotPlugin
```
(`emos plugin inspect` prints the same tree for the active plugin.)
---
```{seealso}
- [Robot Plugins (install & manage)](../getting-started/plugins.md) -- install a plugin from the catalog with `emos plugin`.
- [Creating a Robot Plugin](https://github.com/automatika-robotics/sugarcoat/blob/main/docs/development/custom_robot_plugin.md) -- the full authoring guide.
- [Extending EMOS](../advanced/extending.md) -- custom components and deploying them as system services.
```
```
## File: intelligence/overview.md
```markdown
# EmbodiedAgents
**The intelligence layer of EMOS --** production-grade orchestration for Physical AI
[EmbodiedAgents](https://github.com/automatika-robotics/embodied-agents) enables you to create interactive, physical agents that do not just chat, but **understand**, **move**, **manipulate**, and **adapt** to their environment. It bridges the gap between foundation AI models and real-world robotic deployment, offering a structured yet flexible programming model for building adaptive intelligence.
- {material-regular}`smart_toy;1.2em;sd-text-primary` Production-Ready Physical Agents -- Designed for autonomous systems in dynamic, real-world environments. Components are built around ROS2 Lifecycle Nodes with deterministic startup, shutdown, and error-recovery. Health monitoring, fallback behaviors, and graceful degradation are built in from the ground up.
- {material-regular}`autorenew;1.2em;sd-text-primary` Self-Referential and Event-Driven -- Agents can start, stop, or reconfigure their own components based on internal and external events. Switch from cloud to local inference, swap planners based on vision input, or adjust behavior on the fly. In the spirit of [Godel machines](https://en.wikipedia.org/wiki/G%C3%B6del_machine), agents become capable of introspecting and modifying their own execution graph at runtime.
- {material-regular}`hub;1.2em;sd-text-primary` Semantic Memory & Agentic Planning -- Hierarchical spatio-temporal memory and semantic routing for arbitrarily complex agentic information flow. The graph-backed [Memory](memory.md) component keeps an episodic, entity-aware record of what the robot perceives *and* of its own internal state, while [Cortex](cortex.md) turns plain-language goals into ordered calls against every component in the graph -- no bloated GenAI frameworks required.
- {material-regular}`code;1.2em;sd-text-primary` Pure Python, Native ROS2 -- Define complex asynchronous execution graphs in standard Python without touching XML launch files. Underneath, everything is pure ROS2 -- fully compatible with the entire ecosystem of hardware drivers, simulation tools, and visualization suites.
## What You Can Build
::::{grid} 1 2 3 3
:gutter: 3
:::{grid-item-card} {material-regular}`record_voice_over;1.2em;sd-text-primary` Robots You Hold a Conversation With
:link: ../recipes/foundation/conversational-agent
:link-type: doc
Robots that listen, see, and speak -- microphone in, visually-grounded answer out, all in one Python recipe. Ask *"what's on the table?"* and get a real answer in real time.
:::
:::{grid-item-card} {material-regular}`alt_route;1.2em;sd-text-primary` Robots That Pick the Right Brain
:link: ../recipes/foundation/semantic-routing
:link-type: doc
One sentence in, the right capability fires. *"How tall is Everest?"* wakes the LLM. *"What do you see?"* wakes the VLM. *"Take me to the kitchen."* dispatches the navigation stack. Behavior emerges from intent.
:::
:::{grid-item-card} {material-regular}`precision_manufacturing;1.2em;sd-text-primary` Robots That Pick Up What You Mean
:link: ../recipes/planning-and-manipulation/vla-manipulation
:link-type: doc
A robot arm that grabs *"the red mug next to the laptop"* without you writing a state machine for which mug. A VLM grounds the description; a VLA model translates straight to joint commands.
:::
:::{grid-item-card} {material-regular}`memory;1.2em;sd-text-primary` Robots That Remember
:link: ../recipes/foundation/semantic-map
:link-type: doc
Every detection, every scene caption, every internal reading is folded into a graph indexed by *meaning*, *place*, and *time* -- and persists across reboots. The robot starts knowing your space the way you do.
:::
:::{grid-item-card} {material-regular}`smart_toy;1.2em;sd-text-primary` Robots You Give Missions To
:link: ../recipes/planning-and-manipulation/cortex-agent
:link-type: doc
Drop a [Cortex](cortex.md) component into your recipe and your robot starts running *missions*, not commands. *"Patrol the workshop and tell me if any lights are off."* Cortex auto-discovers every capability as an LLM tool, plans the steps, dispatches them, watches feedback, and replans on failure -- with no orchestration code from you.
:::
:::{grid-item-card} {material-regular}`route;1.2em;sd-text-primary` Robots That Reason About the World
:link: ../recipes/planning-and-manipulation/cortex-navigation
:link-type: doc
Compound goals like *"go to the kitchen and tell me what's on the counter"* fall out of a single recipe. The robot recalls where the kitchen is from memory, navigates there with Kompass, looks at the counter, narrates the answer. End-to-end embodied reasoning, no behavior trees.
:::
::::
## Next Steps
- {material-regular}`widgets;1.2em;sd-text-primary` {doc}`ai-components` -- The core building blocks: components and topics.
- {material-regular}`psychology;1.2em;sd-text-primary` {doc}`cortex` -- The agentic planner-executor that drives the rest of the graph from natural-language goals.
- {material-regular}`memory;1.2em;sd-text-primary` {doc}`memory` -- Graph-backed spatio-temporal memory with perception and interoception layers.
- {material-regular}`cloud;1.2em;sd-text-primary` {doc}`clients` -- How inference backends connect to components.
- {material-regular}`model_training;1.2em;sd-text-primary` {doc}`models` -- Available model wrappers and vector databases.
```
## File: intelligence/ai-components.md
```markdown
# AI Components
A **Component** is the primary execution unit in EmbodiedAgents, the EMOS intelligence framework. Components represent functional behaviors -- for example, the ability to process text, understand images, or synthesize speech. Components can be combined arbitrarily to create more complex systems such as multi-modal agents with perception-action loops.
Most EmbodiedAgents components are **capabilities** -- a single thing the robot can do. LLM, VLM, VLA, Vision, SpeechToText, TextToSpeech, SemanticRouter, VideoMessageMaker each wrap a particular modality or model surface; [Memory](memory.md) is a capability too, giving the robot a graph-backed spatio-temporal record of what it has seen and felt. [Cortex](cortex.md) is the one component that doesn't sit in that family: it's a high-level planner-executor that *uses* the capabilities, turning natural-language goals into ordered calls against the available component capabilities.
```{note}
To learn more about the internal structure and lifecycle behavior of components, check out the concept [here](../concepts/components.md).
```
## Available Components
EmbodiedAgents provides a suite of ready-to-use components. These can be composed into flexible execution graphs for building autonomous, perceptive, and interactive robot behavior. Each component focuses on a particular modality or functionality, from vision and speech to map reasoning and VLA-based manipulation.
```{list-table}
:widths: 20 80
:header-rows: 1
* - Component Name
- Description
* - **LLM**
- Uses large language models (e.g., LLaMA) to process text input. Can be used for reasoning, tool calling, instruction following, or dialogue. It can also utilize vector DBs for storing and retrieving contextual information. Supports built-in local LLM fallback for offline operation.
* - **VLM**
- Leverages multimodal LLMs (e.g., Llava) for understanding and processing both text and image data. Inherits all functionalities of the LLM component. It can also utilize multimodal LLM based planning models for task-specific outputs (e.g. pointing, grounding, affordance etc.). Supports built-in local VLM fallback for offline operation. **This component is also called MLLM**.
* - **VLA**
- Provides an interface to utilize Vision Language Action (VLA) models for manipulation and control tasks. It can use VLA Policies (such as SmolVLA, Pi0 etc.) served with HuggingFace LeRobot Async Policy Server and publish them to common topic formats in MoveIt Servo and ROS2 Control.
* - **SpeechToText**
- Converts spoken audio into text using speech-to-text models (e.g., Whisper). Suitable for voice command recognition. It also implements small on-board models for Voice Activity Detection (VAD) and Wakeword recognition, using audio capture devices onboard the robot. Supports built-in local STT for on-device transcription.
* - **TextToSpeech**
- Synthesizes audio from text using HuggingFace Transformers TTS models (Bark, VITS, SpeechT5, SeamlessM4T, etc.) via the unified `TransformersTTS` wrapper. Output audio can be played using the robot's speakers or published to a topic. Implements `say(text)` and `stop_playback` functions to play/stop audio based on events from other components or the environment. Supports built-in local TTS for on-device speech synthesis.
* - **Memory**
- Provides a graph-backed spatio-temporal memory powered by [eMEM](https://github.com/automatika-robotics/emem). Encodes perception layers (e.g. detections, scene captions) and interoception layers (e.g. battery, internal flags) into an episodic, entity-aware graph and exposes ten retrieval tools as component actions. Replaces the deprecated **MapEncoding** -- see the dedicated [Memory page](memory.md).
* - **Cortex**
- The agentic core. An AI-powered planner-executor that inspects the rest of the recipe, decomposes a natural-language goal into a sequence of component-action calls, and runs them while monitoring the outputs. See [Cortex](cortex.md).
* - **SemanticRouter**
- Routes information between topics based on semantic content and predefined routing rules. Uses a vector DB for semantic matching or an LLM for decision-making. This allows for creating complex graphs of components where a single input source can trigger different information processing pathways.
* - **Vision**
- An essential component in all vision-powered robots. Performs object detection and tracking on incoming images. Outputs object classes, bounding boxes, and confidence scores. It implements a low-latency small on-board classification model as well. Supports a built-in local ONNX classifier for on-device detection.
* - **VideoMessageMaker**
- Generates ROS video messages from input image messages. A video message is a collection of image messages that have a perceivable motion. The primary task of this component is to make intentionality decisions about what sequence of consecutive images should be treated as one coherent temporal sequence. The chunking method used for selecting images for a video can be configured in component config. Useful for sending videos to ML models that take image sequences.
```
```{seealso}
For details on Topics, component configuration, run types, health checks, and fallback behaviors, see the [Core Concepts](../concepts/components.md) section.
```
```
## File: intelligence/cortex.md
```markdown
# Cortex
**The agentic harness for embodied intelligence.** Cortex is the EmbodiedAgents component that turns the rest of your recipe into an agent: it discovers every component you added, registers their methods as LLM tools, and lets the user address the whole system in plain language. If [Claude Code](https://claude.com/claude-code) is an agentic harness for software engineering, Cortex is its analogue for robots -- the same primitives (read the environment, plan, dispatch tools, watch results, replan) applied to a physical system.
A recipe with a Cortex stops being a programmed pipeline and starts being something you talk to.
```{seealso}
For the introductory walkthrough, start with [Cortex: The Agentic Harness](../recipes/planning-and-manipulation/cortex-agent.md). For Cortex paired with spatio-temporal memory, see [Memory and Cortex](../recipes/planning-and-manipulation/cortex-memory.md). For the full multi-system showcase that adds navigation on top, see [Cortex Driving the Full Stack](../recipes/planning-and-manipulation/cortex-navigation.md).
```
---
## What Cortex replaces
A non-Cortex EMOS recipe earns each capability by hand-wiring it: a vision component publishes detections, an event matches the detection class, a fallback restarts the camera if it stalls, a separate LLM component parses the user's input into structured goals, and so on. Every link is yours to author and maintain.
A Cortex recipe is the same components -- minus the wiring. Cortex inspects the running graph at activation, registers every available capability as a callable tool, and accepts the user's intent directly:
| Without Cortex | With Cortex |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| One event-action pair per behaviour, hand-wired in the recipe. | The recipe has no behavioural wiring. Cortex plans the behaviour at runtime from the user's goal. |
| Each capability needs an explicit handler that knows when to trigger it. | Each capability is a `@component_action` on its component. Cortex discovers them all on activation. |
| User input is parsed by a bespoke LLM step into a typed goal. | User input is a free-form string sent to Cortex's main action server. |
| Fallback policies wired per component. | Cortex's confirmation step (EXECUTE / SKIP / ABORT / CONTINUE) handles per-step recovery; replan handles plan-level recovery. |
| You write the orchestration code. | You write the components. Cortex writes the recipe. |
---
## How it works
Cortex runs a **two-phase loop** for every task.
### Phase 1 -- Planning (multi-step)
The planner LLM is handed two tool sets:
- **Planning tools** -- read-only research tools. The built-in `inspect_component` plus any `@component_action(phase=ActionPhase.PLANNING)` methods on managed components.
- **Execution tools** -- everything that _does_ something. Custom `Action` objects, `@component_action(phase=ActionPhase.EXECUTION)` methods, action-server goal tools, service-request tools, and the built-in `update_parameter`.
On each iteration, the LLM may:
1. Call **planning tools** to gather information -- inspect a component, query memory, look up a fact in a vector DB. Results are appended to the conversation and the loop continues.
2. Call **execution tools** -- this commits a plan as an ordered list of steps, and the loop ends.
3. Respond with **text only** -- no actions needed; the text is published on `output` and the task is done.
Up to `max_planning_steps` iterations of research are allowed before the planner must commit. Plans longer than `max_execution_steps` are truncated.
### Phase 2 -- Execution with confirmation
Each step is dispatched in turn. **Before** each one, a brief confirmation LLM call returns one of:
| Decision | Effect |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EXECUTE` | Run the next step. The confirmation may also return a tool call with **resolved arguments** -- e.g. binding a placeholder like `