# Stretch Docs

<figure><img src="/files/iN6vFh03nEB5vXOS56pl" alt=""><figcaption></figcaption></figure>

***

## Overview

Looking to learn about Stretch 4? You've come to the right place.

{% hint style="info" %}

### Documentation for Stretch 3 | 2 | RE1  has moved!

<a href="http://docs-arch.hello-robot.com/0.3/" class="button primary" data-icon="user-robot">Stretch 3 Docs</a><a href="http://docs-arch.hello-robot.com/0.2/" class="button primary" data-icon="robot">Stretch 2 | RE1 Docs</a>
{% endhint %}

### Ask an Agent

The Stretch Docs Agent (via GitBook) can answer questions about the documentation.

<button type="button" class="button primary" data-action="ask" data-icon="openai">Stretch Docs Agent</button>

### Agent Endpoint&#x20;

Agents can connect to this endpoint to browse and query Stretch 4 documentation. Use it in any MCP-compatible client.

\
`https://docs.hello-robot.com/~gitbook/mcp`

{% tabs %}
{% tab title="Claude Code CLI" icon="claude" %}
Open a terminal and run this command to register your site as a user-scoped MCP server in Claude Code.

\
`claude mcp add gitbook-documentation --scope user --transport http https://docs.hello-robot.com/~gitbook/mcp`

\
Claude can call into the Hello Robot documentation server directly when answering questions or exploring your docs.
{% endtab %}

{% tab title="Codex CLI" icon="openai" %}
Open a terminal and run this command to add your site as an MCP server in Codex.

\
`codex mcp add gitbook-documentation --url https://docs.hello-robot.com/~gitbook/mcp`<br>

Codex can keep your docs available alongside the rest of your local development tools.
{% endtab %}

{% tab title="Antigravity" icon="vscode" %}

Antigravity handles remote MCP hosting using a global or workspace configuration file.

1. Inside **Antigravity**, open the command palette (`Cmd/Ctrl+Shift+P`), type `mcp`, and select **Antigravity: Manage MCP Servers**.
2. Click **View raw config** to open the `mcp_config.json` configuration file.
3. Paste the following configuration snippet inside your `mcpServers` object:

```json
{
  "mcpServers": {
    "hello-robot-docs": {
      "serverUrl": "https://docs.hello-robot.com/~gitbook/mcp"
    }
  }
}
```

> ⚠️ **Important:** Google Antigravity uses `serverUrl` as its configuration key, not `url` or `httpUrl` like other IDEs. If you change this key, the environment will throw an initialization error.

4. Save the file and return to the MCP server manager interface. Click **Refresh** to activate the tools.
   {% endtab %}
   {% endtabs %}

### Other Resources

The Stretch 4 documentation is spread across numerous Git repositories. This Stretch Docs site provides an organized view of this information. In addition, you can learn more at these resources:

<a href="https://hello-robot.com/" class="button secondary" data-icon="robot">Hello Robot</a><a href="https://forum.hello-robot.com" class="button secondary" data-icon="discourse">Stretch User Forum</a><a href="https://github.com/hello-robot" class="button secondary" data-icon="square-github">GitHub Repos</a>

{% hint style="info" %}
**This site is still under development.** If you don't see what you need, email us at <support@hello-robot.com>.
{% endhint %}

<br>

***

## Getting Started

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><h4><i class="fa-leaf" style="color:$primary;">:leaf:</i></h4></td><td><strong>Day One Guide</strong></td><td>New to Stretch? Start here!</td><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/r1iGUi4spGcVoyWWDtsq">/spaces/A7odvp6Q3OcylK96Pt41/pages/r1iGUi4spGcVoyWWDtsq</a></td><td><a href="/files/JTYDEiLUcY7RODUspbQt">/files/JTYDEiLUcY7RODUspbQt</a></td></tr><tr><td><h4><i class="fa-head-side-gear" style="color:$primary;">:head-side-gear:</i></h4></td><td><strong>Hardware Guide</strong></td><td>Learn about hardware specifications, use, and care.</td><td><a href="/spaces/5az5ocOmcIzrlNNAT5eH">/spaces/5az5ocOmcIzrlNNAT5eH</a></td><td><a href="/files/QCzjbqk9QZAi6RdI3yZ0">/files/QCzjbqk9QZAi6RdI3yZ0</a></td></tr><tr><td><h4><i class="fa-terminal" style="color:$primary;">:terminal:</i></h4></td><td><strong>Safety Guide</strong></td><td>Learn best practices for safe operation</td><td><a href="/spaces/Fw3I6nfQuCkv1r6I5Ty9">/spaces/Fw3I6nfQuCkv1r6I5Ty9</a></td><td><a href="/files/poRtTgYXf39I6YUI86To">/files/poRtTgYXf39I6YUI86To</a></td></tr></tbody></table>

***

<br>

{% columns %}
{% column width="50%" %}

<div align="left"><figure><img src="/files/Iaqk0KE6p94O1rWS7VKm" alt=""><figcaption></figcaption></figure></div>

{% endcolumn %}

{% column width="50%" valign="middle" %}

## Working with Stretch

Learn the basics —  like keeping the robot charged, logging data, and contrlling the joints.
{% endcolumn %}
{% endcolumns %}

<details>

<summary><i class="fa-robot" style="color:$primary;">:robot:</i> Getting Started</summary>

<table data-header-hidden><thead><tr><th data-type="content-ref"></th><th></th></tr></thead><tbody><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/r1iGUi4spGcVoyWWDtsq">/spaces/A7odvp6Q3OcylK96Pt41/pages/r1iGUi4spGcVoyWWDtsq</a></td><td>Recommended all new users start here with Stretch - safety info, basics, and gamepad teleoperation</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/wjJQ1QtzOzqbXue00ram">/spaces/A7odvp6Q3OcylK96Pt41/pages/wjJQ1QtzOzqbXue00ram</a></td><td>Connect to Stretch and set it up for successful development</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/Fk71kMGZHgSNsZ2S3I2D">/spaces/A7odvp6Q3OcylK96Pt41/pages/Fk71kMGZHgSNsZ2S3I2D</a></td><td>Explore Stretch's joints and sensors</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/Du7j6IwJt2HfhwwRZr0r">/spaces/A7odvp6Q3OcylK96Pt41/pages/Du7j6IwJt2HfhwwRZr0r</a></td><td>Map a room and navigate autonomously using Nav2</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/xy4SIlhuBH8U0KRKMN7x">/spaces/A7odvp6Q3OcylK96Pt41/pages/xy4SIlhuBH8U0KRKMN7x</a></td><td>Control Stretch from a phone or PC</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/xsI64TNWpqrT65iKwqM0">/spaces/A7odvp6Q3OcylK96Pt41/pages/xsI64TNWpqrT65iKwqM0</a></td><td>Software development basics for Stretch</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/KzaAzLfRjPWE13LQ2dMq">/spaces/A7odvp6Q3OcylK96Pt41/pages/KzaAzLfRjPWE13LQ2dMq</a></td><td>Get some support, or share your work with the community</td></tr></tbody></table>

</details>

<details>

<summary><i class="fa-gears" style="color:$primary;">:gears:</i> General Use</summary>

<table data-header-hidden><thead><tr><th data-type="content-ref"></th><th></th></tr></thead><tbody><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/J0NLvY6STlFIOesvIifs">/spaces/A7odvp6Q3OcylK96Pt41/pages/J0NLvY6STlFIOesvIifs</a></td><td>Unboxing your new Stretch</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/y1tPSiwM7A9JRJpOGler">/spaces/A7odvp6Q3OcylK96Pt41/pages/y1tPSiwM7A9JRJpOGler</a></td><td>How to keep your battery charged and healthy</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/LApYW3VeICngbcdLKQw5">/spaces/A7odvp6Q3OcylK96Pt41/pages/LApYW3VeICngbcdLKQw5</a></td><td>How to re-pack your Stretch for return shipping or transporting in a car.</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/Rsb21zltdXGCVGpA7aQT">/spaces/A7odvp6Q3OcylK96Pt41/pages/Rsb21zltdXGCVGpA7aQT</a></td><td>How to develop on Stretch remotely</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/HHaMKgOgLUj2I6U9N5PN">/spaces/A7odvp6Q3OcylK96Pt41/pages/HHaMKgOgLUj2I6U9N5PN</a></td><td>Configuring the Stretch LAN, etc</td></tr></tbody></table>

</details>

<details>

<summary><i class="fa-camera" style="color:$primary;">:camera:</i> Sensor Basics</summary>

<table data-header-hidden><thead><tr><th data-type="content-ref"></th><th></th></tr></thead><tbody><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/qx6xw5qVWUMKDCqh19Sg">/spaces/A7odvp6Q3OcylK96Pt41/pages/qx6xw5qVWUMKDCqh19Sg</a></td><td>How to access the RGB cameras </td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/GO8gRv4ebEZab8OWxMLa">/spaces/A7odvp6Q3OcylK96Pt41/pages/GO8gRv4ebEZab8OWxMLa</a></td><td>How to access the dual 3D Lidars</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/kkqX8KxZ2iM0b7gftFQL">/spaces/A7odvp6Q3OcylK96Pt41/pages/kkqX8KxZ2iM0b7gftFQL</a></td><td>How to access the base laser-line sensor array</td></tr></tbody></table>

</details>

<details>

<summary><i class="fa-gear" style="color:$primary;">:gear:</i> Motion Basics</summary>

<table data-header-hidden><thead><tr><th data-type="content-ref"></th><th></th></tr></thead><tbody><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/JRnFNmlNA301A26Fso62">/spaces/A7odvp6Q3OcylK96Pt41/pages/JRnFNmlNA301A26Fso62</a></td><td>How to program simple joint motions</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/LevKhpFqocY4j2VQFEoP">/spaces/A7odvp6Q3OcylK96Pt41/pages/LevKhpFqocY4j2VQFEoP</a></td><td>Common CLI tools for joint motion</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/ZU17sizHE5uEbIczKtAu">/spaces/A7odvp6Q3OcylK96Pt41/pages/ZU17sizHE5uEbIczKtAu</a></td><td>Ways to adjust the motion characteristics</td></tr></tbody></table>

</details>

<details>

<summary><i class="fa-gamepad-modern" style="color:$primary;">:gamepad-modern:</i> Teleoperation</summary>

<table data-header-hidden><thead><tr><th data-type="content-ref"></th><th></th></tr></thead><tbody><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/826wuxDKQ5m9HMLTZt3J">/spaces/A7odvp6Q3OcylK96Pt41/pages/826wuxDKQ5m9HMLTZt3J</a></td><td>Telop via the mobile phone controller</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/YHLQYSelRBPe5Pmu0Goq">/spaces/A7odvp6Q3OcylK96Pt41/pages/YHLQYSelRBPe5Pmu0Goq</a></td><td>Teleop via the gamepad controller</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/0ZXgTaokHBYxLX5qzhFR">/spaces/A7odvp6Q3OcylK96Pt41/pages/0ZXgTaokHBYxLX5qzhFR</a></td><td>Teleop via the puppet controller</td></tr></tbody></table>

</details>

<details>

<summary><i class="fa-map" style="color:$primary;">:map:</i> Navigation</summary>

<table data-header-hidden><thead><tr><th data-type="content-ref"></th><th></th></tr></thead><tbody><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/1WBZGXnGyedBumTw8Zo5">/spaces/A7odvp6Q3OcylK96Pt41/pages/1WBZGXnGyedBumTw8Zo5</a></td><td>Quick start mapping and navigation</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/iwslVDpH8IEM7fipZYRZ">/spaces/A7odvp6Q3OcylK96Pt41/pages/iwslVDpH8IEM7fipZYRZ</a></td><td>Tutorial on working with the Nav2 system</td></tr><tr><td></td><td></td></tr></tbody></table>

</details>

<details>

<summary><i class="fa-layer-plus" style="color:$primary;">:layer-plus:</i> Common Tasks</summary>

<table data-header-hidden><thead><tr><th data-type="content-ref"></th><th></th></tr></thead><tbody><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/rfWX1lFJJGj9RKd7LZro">/spaces/A7odvp6Q3OcylK96Pt41/pages/rfWX1lFJJGj9RKd7LZro</a></td><td>How to change the end-of-arm tool</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/r8DR4vz5jm6cg55CLbdk">/spaces/A7odvp6Q3OcylK96Pt41/pages/r8DR4vz5jm6cg55CLbdk</a></td><td>How to log status data from the server</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/XIWgBhrbecu71LnWD9J5">/spaces/A7odvp6Q3OcylK96Pt41/pages/XIWgBhrbecu71LnWD9J5</a></td><td>Understanding the base frames</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/Zvl5ThEwcpuR5lAYTdNg">/spaces/A7odvp6Q3OcylK96Pt41/pages/Zvl5ThEwcpuR5lAYTdNg</a></td><td>How to manage the robot model</td></tr><tr><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/Ua7GaMDFoxXrkmT0qAWw">/spaces/A7odvp6Q3OcylK96Pt41/pages/Ua7GaMDFoxXrkmT0qAWw</a></td><td>Working with ArUCo IDs</td></tr><tr><td></td><td></td></tr></tbody></table>

</details>

***

## Demos and Tutorials

We are still developing exciting new demos and tutorials for Stretch 4. Check back periodically for new documentation.

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Data Collection with Puppet Teleop</strong></td><td>Learn how to use the puppet tool to collect on-robot manipulation data</td><td><a href="/files/akl2IRfMEskRNrIrOaSW">/files/akl2IRfMEskRNrIrOaSW</a></td><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/0ZXgTaokHBYxLX5qzhFR">/spaces/A7odvp6Q3OcylK96Pt41/pages/0ZXgTaokHBYxLX5qzhFR</a></td></tr><tr><td><strong>VLM Based Grasping</strong></td><td>Learn how to use a the Molmo2 Vision Language Model to grasp objects</td><td data-object-fit="fill"><a href="/files/nJG9rvqKeFNIYcpPYanw">/files/nJG9rvqKeFNIYcpPYanw</a></td><td><a href="/spaces/9DmMukkLxY3zG5WEzi3S">/spaces/9DmMukkLxY3zG5WEzi3S</a></td></tr><tr><td><strong>Sense and Follow People</strong></td><td>Learn how to do real-time human pose detection and use that to have Stretch follow people.</td><td><a href="/files/hg7LJdffP9fgKypx9ann">/files/hg7LJdffP9fgKypx9ann</a></td><td><a href="/spaces/VDRzQppzRi0RGMCHcT0O">/spaces/VDRzQppzRi0RGMCHcT0O</a></td></tr><tr><td><strong>Flying Gripper Control</strong></td><td>Learn how to use the Jacobian to control the robot from the tool frame</td><td><a href="/files/FVSfbBYePrQBXSoZic31">/files/FVSfbBYePrQBXSoZic31</a></td><td><a href="/spaces/HfMeDcxEWF9hyYdOhtvN">/spaces/HfMeDcxEWF9hyYdOhtvN</a></td></tr><tr><td><strong>Hybrid Marker Demo</strong></td><td>Learn how to integrate the reflective ArUco markers into a real-time controller</td><td><a href="/files/q0S5NcyuUKe3A8d5Wjxz">/files/q0S5NcyuUKe3A8d5Wjxz</a></td><td><a href="/spaces/i6YjBUfk6eBB9NOVUrrA">/spaces/i6YjBUfk6eBB9NOVUrrA</a></td></tr><tr><td><strong>Navigation University</strong></td><td>Learn the details of working with Nav2 and Stretch 4</td><td><a href="/files/lIToytEe8Tlpg5lyGkMH">/files/lIToytEe8Tlpg5lyGkMH</a></td><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/iwslVDpH8IEM7fipZYRZ">/spaces/A7odvp6Q3OcylK96Pt41/pages/iwslVDpH8IEM7fipZYRZ</a></td></tr></tbody></table>

***

<br>

{% columns %}
{% column width="50%" %}

<figure><picture><source srcset="/files/ELD4Hb054gfoQWXavwV5" media="(prefers-color-scheme: dark)"><img src="/files/sWtfRnCR18rMy0YMjyIA" alt=""></picture><figcaption></figcaption></figure>
{% endcolumn %}

{% column width="50%" valign="middle" %}

## Explore the Open Source Software Stack

The Stretch 4 software is organized into a multi-layered stack designed to transition seamlessly from low-level hardware control to high-level autonomous behaviors.

The [Hello Robot GitHub ](https://github.com/hello-robot)page provide an overview of the key repositories. Each repository is intended to be self-documenting through its internal README.md and other Markdown files.

<a href="https://github.com/hello-robot" class="button primary" data-icon="code">Explore the Code</a>&#x20;
{% endcolumn %}
{% endcolumns %}

***

<br>

{% columns %}
{% column width="50%" %}

<h2 align="center">Join the community of 100s of developers</h2>

Join our the Stretch community to post questions, get help, and share resources with over 100s of like-minded developers.

<a href="https://forum.hello-robot.com" class="button primary" data-icon="discourse">Join the Forum</a>&#x20;
{% endcolumn %}

{% column width="50%" valign="middle" %}

<figure><picture><source srcset="/files/pClFTUZ1xv2TJymG4KWL" media="(prefers-color-scheme: dark)"><img src="/files/7iHgbg4penRLIofqv1Al" alt="" width="188"></picture><figcaption></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

&#x20;


# Quick Start Guide

<table data-view="cards"><thead><tr><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>Getting Started with Stretch</td><td><a href="/files/tWQnrZtwDTf4ob0Ownso">/files/tWQnrZtwDTf4ob0Ownso</a></td><td><a href="/spaces/A7odvp6Q3OcylK96Pt41/pages/r1iGUi4spGcVoyWWDtsq">/spaces/A7odvp6Q3OcylK96Pt41/pages/r1iGUi4spGcVoyWWDtsq</a></td></tr></tbody></table>


# Setting Up Stretch

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

### Connecting to Stretch - Tethered

In order to work with Stretch's software, or to develop and test your own code, you'll first need to connect to the computer inside. The simplest way to do this is to directly connect a monitor, keyboard, and mouse to the robot using the exposed ports in the robot trunk.

<figure><img src="/files/nGTBU1JyDeuolx8x5HCY" alt=""><figcaption></figcaption></figure>

Stretch 4 contains a mini PC (model: Asus NUC 15) that can be accessed using the ports as shown above. Connect a monitor to the HDMI port, and a mouse and keyboard (we like to use a wireless dongle) to any of the USB ports, and make sure the robot is powered on (power button illuminated green). The Ubuntu desktop environment should appear on the connected monitor. Stretch 4 is running the Ubuntu 24.04 operating system.

The default user login credentials came in the box with the robot. By default, the robot is not configured to ask for your password on boot, but may ask for it later if the NUC goes to sleep.<br>

{% columns %}
{% column %}

<figure><img src="/files/rcE9BGKj6UQcw5PVUGsg" alt="robot trunk"><figcaption></figcaption></figure>
{% endcolumn %}

{% column %}

<figure><img src="/files/313MIbvfmskphTbjneSm" alt=""><figcaption></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

#### Setting up Wi-Fi

One of the first things you'll probably want to do with Stretch is to connect it to the internet. While there is an Ethernet port in the trunk as well, its much more likely that you'll want to use Wi-Fi. A Wi-Fi connection will also enable you to use untethered connections to the robot in the future (more details below).

This is as simple as opening the Wi-Fi menu at the top right, selecting your network from the list, and inputting your network password if necessary. We recommend selecting the option to auto-connect to the network, in order to make working with the robot easier in the future.

<figure><img src="/files/qSCEv3zTgbaj0BofGpG5" alt=""><figcaption></figcaption></figure>

### Connecting to Stretch - Untethered

Once your robot is on the network, you can access it remotely from another computer. This is the preferred way to work with Stretch for most users. There are multiple options for achieving this, including remote desktop software like RustDesk or TeamViewer, SSH connections, Tailscale, ROS2 networking, PyZMQ, etc. For the purposes of this tutorial, we will focus on Ubuntu Remote Desktop Protocol (RDP) over a local network.

#### RDP with Remote Login

Ubuntu 24.04 ships with a built-in Remote Login feature that allows you to access your Ubuntu User Account remotely using Remote Desktop Protocol (RDP). Multiple users can even log into the robot at the same time for development, if necessary.

The official Ubuntu docs for setting up Remote Login are here: <https://help.ubuntu.com/stable/ubuntu-help/remote-login.html.ro>

You do not need a HDMI Dummy Dongle to use Remote Login. Note that Remote Login is different from Remote Desktop, which shares the same menu in Ubuntu's Settings. Remote Desktop does require a Dummy Dongle and requires you to remain logged in.

#### Robot setup instructions

The following instructions will walk you through setting up Remote Login.

{% hint style="info" %}
Please follow these steps while you are on the hello-robot or default user account - setting up Remote Login on multiple accounts might cause conflicts if the username-password pair is the same.
{% endhint %}

1. On your robot, navigate to Settings -> System -> Remote Desktop <br>

   <figure><img src="/files/Mlki2CgsXIwJ4qZPvDen" alt=""><figcaption></figcaption></figure>
2. Select the Remote Login tab, click Unlock and type in your password. Toggle to Enable Remote Login <br>

   <figure><img src="/files/McAgWA2t8ex7fLHv9LnH" alt=""><figcaption></figcaption></figure>
3. At the bottom of the window, enter a username and strong password combination that will be shared by all users remotely connecting to this robot; it’s a good idea to NOT use the password for your User Account because anyone on the network can connect to the login screen after setting up Remote Login. This password will be shared by all users for remote access to the Login Screen (not a particular user account).&#x20;
4. Retrieve the ip address of this robot using `hostname -I`.

{% hint style="warning" %}
It is important to follow your institution or organization's security best practices for setting up remote access and Ubuntu User Account Credentials.
{% endhint %}

#### Client Setup Instructions

On the computer you want to connect to the robot, follow the below instructions based on your operating system:

{% tabs %}
{% tab title="Linux Client Setup" %}

1. Run `sudo apt install remmina`
2. Open Remmina and click the "+" icon. Enter your robot's IP, username and password you configured in the previous step. Click Save and Connect to test your connection
3. Click the Toggle Dynamic Resolution button on the sidebar to make the RDP window use your monitor’s resolution.
4. Login to your User Account to start using your robot.
5. To configure audio: on the client, right click the Remmina connection -> Edit -> go to the Advanced tab -> Audio output mode. Choose Remote to play audio through the robot’s speakers. Local to play it using the client speakers. Then click Save.
   {% endtab %}

{% tab title="MacOS Client Setup" %}

1. Get the Windows App, offered by Microsoft Corporation, from the [Apple Store](https://apps.apple.com/us/app/windows-app/id1295203466)
2. Create a new Computer connection and change the PC name to the IP Address of the robot. You can add a Friendly Name to help identify the robot. Under Credentials, you can click to “Add Credentials” and save your password so you do not have to enter it every time you connect.

> Note: if you encounter a blank screen error on MacOS, export your connection and change the line that says `use redirection server name:i:1`, and re-import your configuration, as suggested in: <https://askubuntu.com/a/1528263>
> {% endtab %}

{% tab title="Windows Client Setup" %}

1. Open a Command Prompt window by pressing Win + R, then run  `mstsc`&#x20;
2. In the "Computer" field, type the IP address as retrieved from the robot above
3. Click "Connect". You may receive a certificate warning; click Yes to proceed.
4. When the login screen appears, enter your robot username and password
   {% endtab %}
   {% endtabs %}

You are now connected remotely to Stretch! This can be a convenient way to use and develop on the robot. To learn more about different ways of connecting to Stretch, see the full guide here: [Connecting to Stretch](/stretch4_docs/working-with-stretch/general_use/connecting-to-stretch).

## Turning off Gamepad Teleoperation

Out of the box, Stretch is configured to launch the gamepad teleoperation demo in the background at startup. While this is running, other code cannot use the robot. You will need to free the robot process so that your code can use it.

Run the below command in the terminal to kill the server and stop the startup script.

```bash
stretch_body_server --kill
```

You can also disable the autostart feature entirely. Search for "**Startup Applications**" from the Apps menu and uncheck the box for `hello_robot_xbox_teleop`.<br>

In the next section, we'll learn about Stretch's joints and sensors and the basic commands for controlling them.


# Robot Overview

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

## System Check

Run a full hardware and software check to confirm the system is ready:

```bash
stretch_system_check
```

If all checks pass, your robot is ready to go. Use `stretch_about` to print robot identity and configuration, and `stretch_params` to inspect all robot parameters.

## Homing

If your robot needs to be homed, the system check will tell you. The homing procedure takes approximately 30 seconds and finds the zero position of all joints. It must be run once every time the robot is powered on:

To home the robot:

```bash
stretch_robot_home
```

\--# insert video of robot homing

The robot will beep when finished. The motors will remember their homing state while the robot remains powered on — using the runstop, backdriving the robot, or restarting the NUC will not require rehoming. Powering the robot down completely will require homing again on the next boot.

To stow the robot to its compact travel pose:

```bash
stretch_robot_stow
```

\--# insert video of robot stowing

## Runstop

The physical runstop button on the side of the robot's head immediately cuts power to all motors. The `stretch_runstop` tool lets you toggle the runstop programmatically from the terminal.

\--# we already mentioned this in quick start should we point to it?

## Monitoring

To monitor all robot devices, sensors, and activity in real time, including joint states, battery, and subsystem status:

```bash
stretch_monitor_devices
```

This tool provides a unified live dashboard of the robot's state.

\--# this tool exists in stretch\_production\_tools\_ii, worth it to port to stretch4\_body

## Motors and Joints

\--# add videos for each

### Omnibase

Stretch 4 has a triangular omnibase, consisting of three holonomic closed-loop stepper motors each driving one wheel. Wheel numbering increases counter-clockwise, with wheel 0 to the left of the forward direction. This is consistent with the ROS frame convention where X+ is forward and Y+ is to the left, forming a right-handed coordinate frame.

The omnibase supports full planar motion — forward, sideways, diagonal, and rotation in place, all simultaneously. It also supports guarded contact sensitivity, which can be calibrated and tuned per use case.

To jog the base:

```bash
stretch_omni_base_jog
```

### Lift

The lift provides vertical translation of the arm, reaching up to 47 inches high and all the way down to the ground. It is driven by a closed-loop stepper motor through a low gear-ratio belt drive, providing smooth and precise motion.

```bash
stretch_lift_jog
```

### Arm

The arm comprises 4 telescoping links set on rollers, extending 21.6 inches beyond the base footprint and retracting to stow within it. Its proprietary drivetrain is driven by a stepper motor with closed-loop control and current sensing, enabling contact detection during motion. In combination, the lift, arm, and mobile base provide three orthogonal axes of motion — a Cartesian system for end-effector placement.

```bash
stretch_arm_jog
```

### Contact Sensitivity (Guarded Contact)

Stretch's lift, arm, and omnibase joints have a contact detection system called Guarded Contact. This safety feature limits the forces Stretch can apply to a person or its environment, and can be tuned for your application.

Guarded Contact uses current sensing to detect when actuator effort exceeds a user-specified threshold during motion. When triggered, the safety controller halts the joint until a new movement command is received.

### Feetech Motors

The wrist and gripper joints use Feetech servo motors, controlled over a TTL serial bus. To monitor Feetech motor status:

```bash
stretch_feetech_monitor
```

#### Feetech Errors and Reset

Feetech motors can enter an error state from over-force or over-temperature events. When in this state, the motor becomes backdrivable, stops responding to commands, and the LED on the motor body blinks red.

Powering the robot down completely will clear this error. Note: rebooting only the NUC will NOT clear it, as the Feetech motors remain powered. A faster way to clear the error:

```bash
stretch_feetech_reboot
```

This reboots all Feetech motors and resets their error status. You will need to re-run homing after doing this since the wrist\_yaw and gripper joints lose their homed positions.

## Head Cameras

Stretch 4 has a Luxonis OAK-FFC-3P camera module mounted in the head with three cameras:

| Camera | Type                  | Resolution | Target FPS |
| ------ | --------------------- | ---------- | ---------- |
| Left   | Fish-eye wide angle   | 1920×1200  | 30         |
| Right  | Fish-eye wide angle   | 1920×1200  | 30         |
| Center | High-resolution color | 4032×3040  | 10         |

The left and right cameras are fish-eye wide-angle cameras providing a broad environmental field of view. The center camera is a high-resolution color camera. All three are angled to maximize combined visual coverage in every frame.

To display any combination of camera feeds and open them in `rerun` or `opencv` :

```bash
stretch_camera_show --left --rerun
stretch_camera_show --center --opencv
stretch_camera_show --right --rerun
stretch_camera_show --left_right_center --opencv
```

{% hint style="info" %}

1. use either --rerun or --opencv (not both) in conjunction with the choice of camera combination.
2. use the flag -h to see all options
   {% endhint %}

### Emulated RGBD from Head Cameras

The head cameras can be fused with the head Hesai lidars to produce RGBD (color + depth) point clouds. To visualize this:

```bash
stretch_rgbd_show
```

### Camera Focus and Calibration

Camera focus is adjusted and intrinsics are calibrated using the factory calibration pipeline. After calibration, cross-calibration between cameras and lidars is also performed during the bringup process to align all sensor modalities spatially.

## Dexterous Wrist

Stretch 4 has a three degree-of-freedom wrist with yaw, pitch, and roll actuation — all driven by Feetech actuators.

| Axis        | Raw Servo Range | Notes                                                                      |
| ----------- | --------------- | -------------------------------------------------------------------------- |
| Wrist Yaw   | 310°            | `range_deg: [-65, 245]`                                                    |
| Wrist Pitch | 310°            | `range_deg: [-65, 245]` — effective range reduced by self-collision limits |
| Wrist Roll  | 310°            | `range_deg: [-65, 245]`                                                    |

To jog individual wrist joints:

```bash
stretch_dex_wrist_jog
```

To home all wrist joints:

```bash
stretch_dex_wrist_home
```

Individual axis homing is also available with `stretch_wrist_yaw_home`, `stretch_wrist_pitch_home`, and `stretch_wrist_roll_home`.

## Gripper

The compliant gripper is a robust single-degree-of-freedom end-effector. A Feetech actuator drives the center of the spring mechanism, which causes the outer fingers to flex and provide a grasping force.

```bash
stretch_gripper_jog
stretch_gripper_home
```

### Gripper Cameras

Stretch 4 has two OAK-D-SR (Short Range) cameras mounted at the gripper, providing stereo depth for in-hand manipulation tasks. These are detected as a separate Luxonis device from the head cameras (2-sensor device vs. the 3-sensor head device).

```bash
stretch_camera_show --gripper
```

## Sensors

Stretch 4 includes the following sensors:

* Hesai JT128 3D LiDAR × 2 (Left and Right, mounted on the head)
* Luxonis OAK-FFC-3P Camera Module (Head: 3-camera array) + OAK-D-SR (Gripper: stereo pair)
* Speaker (mounted at the bottom of the head)
* Pixart J3 Line Sensor Array (base, floor-facing)

### Hesai JT128 3D LiDAR (Head)

Stretch 4 has two Hesai JT128 3D LiDAR units mounted on the head — a left lidar and a right lidar — providing full 3D point cloud coverage of the environment. These are used for mapping, navigation, and RGBD fusion with the head cameras.

Each lidar communicates over Ethernet. The NUC holds a single network profile with two IP addresses to communicate with both lidars and the Jetson simultaneously:

<table><thead><tr><th width="205">Device</th><th width="174">IP Address</th><th>Description</th></tr></thead><tbody><tr><td>Left lidar</td><td>192.168.1.202</td><td>Hesai JT128 left</td></tr><tr><td>Right lidar</td><td>192.168.1.201</td><td>Hesai JT128 right</td></tr><tr><td>NUC (Lidar subnet)</td><td>192.168.1.2</td><td>Onboard computer — lidar communications</td></tr><tr><td>NUC (Jetson subnet)</td><td>192.168.1.100</td><td>Onboard computer — Jetson communications</td></tr><tr><td>Jetson</td><td>192.168.1.101</td><td>AI co-processor</td></tr></tbody></table>

### Speaker and Microphone

The robot has a speaker and noise-cancelling microphone mounted at the bottom of the head, allowing Stretch to communicate from across a room. To test audio output:

```bash
stretch_audio_test
```

### Line Sensor Array

Stretch 4 includes a floor-facing GreatScott GS2 line sensor array on the base. This array continuously scans the floor in front of the robot and uses an on-robot model to classify the surface as floor or obstacle, enabling low-latency hazard detection independent of the lidar.

The line sensor runs in a dedicated background worker process at approximately 30 Hz, and its output is used by the omnibase to automatically limit velocity when an obstacle is detected in the direction of travel. To visualize the line sensor:

```bash
stretch_line_sensor_viz_3d
```

## LED Eyes and Lightbar

The robot head has two LED eye displays. These support a set of built-in animations including idle glow, blinking, directional gaze, rainbow spin, alert, and happy states:

```bash
stretch_eye_animations
```

The head also has a programmable RGB LED lightbar. The lightbar can be triggered and tested via `stretch_power_periph_jog`.

## NVIDIA Jetson (AI Co-processor)

Stretch 4 includes an NVIDIA Jetson Orin module as a dedicated AI co-processor, directly connected to the NUC via Ethernet.

### Software Environment

The Jetson runs with:

* **OS:** Ubuntu 22.04 + JetPack 6.1
* **Custom Docker container** with: ROS 2 Jazzy, PyTorch, CUDA (cu129), Zenoh, Ultralytics, and OpenCV Bridge for NVIDIA Jetson

### Accessibility

The NVIDIA Jetson boots up automatically whenever the robot is powered on. Passwordless SSH is pre-configured, so you can seamlessly access the Jetson from the NUC using the following command:

```bash
ssh jetson1@192.168.1.101
```

{% hint style="warning" %}
**Note:** *If the Jetson fails to boot, you can manually power it on from the NUC using `stretch_power_periph_jog -d`. To forcefully power it off from the NUC, use `stretch_power_periph_jog -c`)*
{% endhint %}

### Internet Connectivity

To enable system updates and software package installations, there is a Wi-Fi dongle attached directly to the Jetson processor. You can connect the Jetson's Wi-Fi dongle to your local internet using the command line interface (CLI).

Once logged into the Jetson via SSH, use the `nmcli` network manager to connect to your Wi-Fi network:

```bash
sudo nmcli device wifi connect "<your_ssid>" password "<your_password>"
```

Once connected, you can verify internet access by pinging an external server:

```bash
ping 8.8.8.8
```

### GPU Offloading

The Jetson communicates with the NUC via Zenoh for ROS 2 topic bridging. This enables offloading compute-heavy tasks such as YOLO object detection or pose estimation to the Jetson's GPU, while the NUC handles robot control. The Jetson subscribes to image topics published by the NUC and returns inference results.

## Software Architecture

Stretch 4 uses a client/server architecture in `stretch4_body`:

* **`stretch_body_server`** runs a 100 Hz control loop on the NUC, managing all hardware state
* Application code connects via `RobotClient` to issue commands
* A C++ transport backend handles non-blocking USB communication to all motor controllers in parallel

The 100 Hz control loop follows this sequence every tick:

1. Pull status from all devices
2. Update sentries (safety watchdogs)
3. Ingest commands from the Robot Client
4. Run active controllers and behaviors
5. Compute safe motion limits
6. Push safe commands to motor controllers

To launch the body server:

```bash
stretch_body_server --launch
```

## Gamepad Teleoperation

Stretch 4 ships with an Xbox controller and supports two control mappings:

### 1. Joint Space (default)

Direct joint-level control of the robot. All controls are summarized below:

<table data-search="false"><thead><tr><th>Control</th><th>Action</th></tr></thead><tbody><tr><td><strong>Base Controls</strong></td><td></td></tr><tr><td>Left Stick</td><td>Translate base (XY)</td></tr><tr><td>LB / RB</td><td>Rotate base</td></tr><tr><td>Hold LB + RB, Right Stick</td><td>Analog base rotation</td></tr><tr><td>RT + Left Stick</td><td>Straight-line base movement</td></tr><tr><td><strong>Arm Controls</strong></td><td></td></tr><tr><td>Right Stick</td><td>Wrist Pitch (Y) and Yaw (X)</td></tr><tr><td>D-Pad Up / Down</td><td>Lift up / down</td></tr><tr><td>D-Pad Left / Right</td><td>Arm retract / extend</td></tr><tr><td>A / B Buttons</td><td>Close / Open gripper</td></tr><tr><td>RT + LB / RB</td><td>Wrist Roll</td></tr><tr><td><strong>Modifiers</strong></td><td></td></tr><tr><td>LT</td><td>Precision mode (reduce speed)</td></tr></tbody></table>

### 2. Flying Gripper IK

IK-based Cartesian control of the gripper. Point the gripper toward your target with the Right Stick, then move toward it with the Left Stick:

| Control            | Action                                |
| ------------------ | ------------------------------------- |
| Left Stick         | Move toward target (translation)      |
| Right Stick        | Point gripper at target (orientation) |
| D-Pad Up / Down    | Lift up / down                        |
| D-Pad Left / Right | Wrist Roll                            |
| A / B Buttons      | Close / Open gripper                  |
| LT                 | Precision mode (reduce speed)         |

### Special Functions

<table data-search="false"><thead><tr><th>Input</th><th>Action</th></tr></thead><tbody><tr><td><strong>Y Button</strong> (tap)</td><td>Cycle control mapping (Joint Space ↔ Flying Gripper IK)</td></tr><tr><td><strong>RT + A</strong> (tap)</td><td>Cycle motion speed profile (Slow / Medium / Fast)</td></tr><tr><td><strong>RT + B</strong> (tap)</td><td>Cycle contact sensitivity profile</td></tr><tr><td><strong>Start Button</strong> (tap, unhomed)</td><td>Home the robot</td></tr><tr><td><strong>Start Button</strong> (tap, homed)</td><td>Switch gripper handedness (without motion)</td></tr><tr><td><strong>Start Button</strong> (hold 3s, homed)</td><td>Switch gripper handedness (with motion)</td></tr><tr><td><strong>RT + Select</strong> (tap)</td><td>Announce current settings (handedness, speed, sensitivity, mapping)</td></tr><tr><td><strong>RT + Select</strong> (hold 2s)</td><td>Stow the robot</td></tr><tr><td><strong>X Button</strong> (hold 0.5s)</td><td>Execute custom function command</td></tr></tbody></table>

To launch gamepad teleoperation:

```bash
stretch_gamepad_teleop
```

## Developer I/O

Stretch 4 contains additional ports connected to the onboard NUC that can be used for accessories:

* **Trunk:** 1× USB-A 3.0 ports, 2x USB-A 2.0 ports, 1× Ethernet port, 1× HDMI port.
* **Head (top):** 1× USB-A 2.0 port, 1x USC-C Port
* **End-of-arm:** 1× USB-A 2.0 port
* **Wrist:** Quick-connect mechanism for tool attachment.

There are also threaded mounting points on the head to add additional sensors.

## Key Tools Reference

<table data-search="false"><thead><tr><th width="285">Tool</th><th>Description</th></tr></thead><tbody><tr><td><code>stretch_body_server</code></td><td>Start / stop / release the 100 Hz robot control server</td></tr><tr><td><code>stretch_robot_home</code></td><td>Home all robot joints</td></tr><tr><td><code>stretch_robot_stow</code></td><td>Stow the robot to its compact travel pose</td></tr><tr><td><code>stretch_runstop</code></td><td>Toggle the runstop programmatically</td></tr><tr><td><code>stretch_system_check</code></td><td>Full hardware and software system check</td></tr><tr><td><code>stretch_about</code></td><td>Print robot identity and configuration</td></tr><tr><td><code>stretch_params</code></td><td>Print all robot parameters</td></tr><tr><td><code>stretch_battery_check</code></td><td>Check battery state of charge</td></tr><tr><td><code>stretch_status</code></td><td>Live robot status display</td></tr><tr><td><code>stretch_joint_viz</code></td><td>Joint state visualization</td></tr><tr><td><code>stretch_collision_viz</code></td><td>Visualize self-collision safety margins</td></tr><tr><td><code>stretch_gamepad_teleop</code></td><td>Launch Xbox gamepad teleoperation</td></tr><tr><td><code>stretch_puppet_teleop</code></td><td>Launch puppet (backdriving) teleoperation</td></tr><tr><td><code>stretch_omni_base_jog</code></td><td>Jog the omnibase</td></tr><tr><td><code>stretch_lift_jog</code></td><td>Jog the lift</td></tr><tr><td><code>stretch_arm_jog</code></td><td>Jog the arm</td></tr><tr><td><code>stretch_arm_home</code></td><td>Home the arm</td></tr><tr><td><code>stretch_lift_home</code></td><td>Home the lift</td></tr><tr><td><code>stretch_gripper_jog</code></td><td>Jog the gripper</td></tr><tr><td><code>stretch_gripper_home</code></td><td>Home the gripper</td></tr><tr><td><code>stretch_dex_wrist_jog</code></td><td>Jog dex wrist joints</td></tr><tr><td><code>stretch_dex_wrist_home</code></td><td>Home all dex wrist joints</td></tr><tr><td><code>stretch_feetech_reboot</code></td><td>Reboot Feetech motors and clear errors</td></tr><tr><td><code>stretch_feetech_monitor</code></td><td>Monitor Feetech motor status</td></tr><tr><td><code>stretch_camera_show</code></td><td>Live camera feed viewer (configurable)</td></tr><tr><td><code>stretch_rgbd_show</code></td><td>Emulated RGBD from head cameras + lidar</td></tr><tr><td><code>stretch_line_sensor_viz_3d</code></td><td>3D visualization of line sensor data</td></tr><tr><td><code>stretch_power_periph_jog</code></td><td>Manually control power periph (Jetson, fans, lidar, etc.)</td></tr><tr><td><code>stretch_eoa_power</code></td><td>Toggle end-of-arm power</td></tr><tr><td><code>stretch_eye_animations</code></td><td>Run LED eye animations</td></tr><tr><td><code>stretch_audio_test</code></td><td>Test speaker output</td></tr><tr><td><code>stretch_pose_play</code></td><td>Play back recorded robot poses</td></tr><tr><td><code>stretch_pose_record</code></td><td>Record robot poses</td></tr><tr><td><code>stretch_pose_edit</code></td><td>Edit recorded pose files</td></tr></tbody></table>


# Writing Code for Stretch

This tutorial introduces the two primary ways to develop software with Stretch SE4 — Python and ROS 2 — and walks you through writing your first Python programs using the Stretch 4 Body API. By the end, you will be able to command every joint on the robot, read sensor status, control the omnibase holonomically, and use the client/server architecture.

***

### Background

Stretch 4 supports two approaches to software development:

**Python (Stretch 4 Body)** is the low-level direct interface to the robot hardware. It gives you fine-grained control over every joint and sensor, and is the fastest way to get started. The `stretch4_body` package is pre-installed on every Stretch SE4.

**ROS 2 (Robot Operating System 2)** is a robotics middleware framework providing a collection of tools, libraries, and conventions for building robot applications. Stretch SE4 ships with ROS 2 Jazzy and a full driver stack. It is well suited for navigation, SLAM, MoveIt, and multi-node architectures.

You can learn more about when to use each approach in the Developing with Stretch guide. This tutorial focuses on the Python API. ROS 2 examples are covered in the Demos section.

***

### Software Architecture

All application code on Stretch SE4 uses the `RobotClient` class to communicate with a background `stretch_body_server` process running a 100 Hz control loop on the NUC.

The 100 Hz control loop follows this sequence every tick:

1. Pulls status from all hardware devices
2. Updates safety sentries (watchdogs)
3. Ingests commands from the Robot Client
4. Runs active controllers and behaviors
5. Computes safe motion limits
6. Pushes safe commands to motor controllers

Your code connects via `RobotClient`, which queues commands that the server loop ingests on the next tick. This means **motion is asynchronous by default** — your code keeps running while the robot moves. Use `push_command()` to flush the command queue and `wait_on_motion_finish()` to block until motion is done.

***

### Prerequisites

Before running any code, the robot must be set up and ready.

**1. Make sure the body server is running.**

The body server is launched automatically at startup. Verify it is active:

```bash
stretch_body_server --status
```

If it is not running, start it:

```bash
stretch_body_server --daemon
```

**2. Free up robot control from the gamepad demo.**

If the Xbox gamepad teleoperation demo is running at startup, your code cannot control the robot. Free the robot:

```bash
stretch_body_server --free_up_control
```

**3. Confirm the system is healthy.**

```bash
stretch_system_check
```

**4. Home the robot (required once per power cycle).**

```bash
stretch_robot_home
```

The robot will beep when homing is complete. Joints will not accept motion commands until they are homed. Once homed, the motors remember their homed state as long as the robot remains powered on.

***

### Your First Program with RobotClient

Open a terminal and launch iPython, an interactive Python console where each line runs immediately:

```bash
ipython3
```

Import and start the client:

```python
from stretch4_body.robot.robot_client import RobotClient

robot = RobotClient()
robot.startup()
```

`startup()` connects to the running body server. If successful, you now have full access to all robot subsystems.

Stow the robot to its compact travel pose:

```python
robot.stow()
```

This is a blocking call — it returns only when the robot is fully stowed.

Check if the robot is homed:

```python
robot.is_homed()
```

When you are done with a session, always call:

```python
robot.stop()
```

This cleanly disconnects from the server. **Skipping `stop()` can leave stale connections** that block other processes from controlling the robot.

#### Using RobotClient as a Context Manager

For scripts, the cleanest pattern is to use `RobotClient` as a context manager. This ensures `stop()` is always called, even if an exception occurs:

```python
from stretch4_body.robot.robot_client import RobotClient

with RobotClient() as robot:
    robot.stow()
    # ... your code ...
# robot.stop() is called automatically here
```

***

### Moving Individual Joints

#### Arm

The arm telescopes horizontally, extending up to \~0.52 m (21.6 inches) beyond the base. Position is in meters (0.0 = fully retracted, \~0.52 = fully extended).

Move the arm to an absolute position:

```python
robot.arm.move_to(0.25)
robot.push_command()
```

Move the arm by a relative amount:

```python
robot.arm.move_by(0.05)
robot.push_command()
```

Move the arm back:

```python
robot.arm.move_by(-0.05)
robot.push_command()
```

#### Lift

The lift translates the arm vertically, from floor level up to \~1.20 m (47 inches). Position is in meters.

Move the lift to mid height:

```python
robot.lift.move_to(0.6)
robot.push_command()
```

Move the lift up a little:

```python
robot.lift.move_by(0.1)
robot.push_command()
```

#### Commanding Multiple Joints Simultaneously

You can queue commands to several joints before pushing. They will execute simultaneously:

```python
robot.lift.move_to(0.7)
robot.arm.move_to(0.3)
robot.push_command()
```

> \[!NOTE] If you queue two motion commands for the **same joint** before pushing, only the last one executes. Earlier commands are overwritten.

#### Velocity and Acceleration Limits

Every motion command accepts optional velocity and acceleration limits (in m/s and m/s² respectively):

```python
robot.arm.move_to(0.2, v_m=0.05, a_m=0.1)
robot.push_command()
```

If not specified, the robot uses its default motion profile.

***

### The Omnibase (Holonomic Base)

Stretch SE4 has a triangular holonomic omnibase — three holonomic closed-loop stepper motors each driving one omnidirectional wheel. Unlike a differential drive, the omnibase can move **forward, sideways, diagonally, and rotate in place — all simultaneously**.

Wheel numbering increases counter-clockwise, with wheel 0 to the left of the forward direction. This is consistent with the ROS convention where X+ is forward and Y+ is left.

The base is accessed as `robot.base` or `robot.omnibase`.

#### Translate by a Relative Amount

Move the base 0.3 m forward (X):

```python
robot.base.translate_by(x_m=0.3, y_m=0.0)
robot.push_command()
```

Move sideways to the left (positive Y):

```python
robot.base.translate_by(x_m=0.0, y_m=0.15)
robot.push_command()
```

#### Rotate in Place

Rotate counter-clockwise by 90 degrees (π/2 radians):

```python
import math
robot.base.rotate_by(w_r=math.pi / 2)
robot.push_command()
```

#### Set Continuous Velocity

Set a continuous velocity (useful for teleoperation or reactive control). Units: m/s for linear, rad/s for rotation:

```python
# Drive forward at 0.2 m/s while rotating at 0.1 rad/s
robot.base.set_velocity(vx_m=0.2, vy_m=0.0, w_r=0.1)
robot.push_command()

# Stop
robot.base.set_velocity(vx_m=0.0, vy_m=0.0, w_r=0.0)
robot.push_command()
```

#### Hard Stop

Immediately stop all base motion:

```python
robot.base.hard_stop()
robot.push_command()
```

***

### The Dexterous Wrist and Gripper

Stretch SE4 has a 3-DOF dexterous wrist (yaw, pitch, roll) driven by Feetech servo motors over a TTL serial bus, plus a compliant spring-mechanism gripper.

All wrist and gripper joints are accessed through `robot.end_of_arm`. Positions are in **radians** unless noted.

| Joint             | Range                                         |
| ----------------- | --------------------------------------------- |
| `wrist_yaw`       | ±170 deg (340 deg total)                      |
| `wrist_pitch`     | \~100 deg                                     |
| `wrist_roll`      | ±170 deg (340 deg total)                      |
| `stretch_gripper` | -100 to +100 (percent, where positive = open) |

#### Move Wrist Joints

Move wrist yaw to 0.5 rad:

```python
robot.end_of_arm.move_to('wrist_yaw', 0.5)
robot.push_command()
```

Move wrist pitch:

```python
robot.end_of_arm.move_to('wrist_pitch', -0.5)
robot.push_command()
```

Move wrist roll by a relative amount:

```python
robot.end_of_arm.move_by('wrist_roll', 1.0)
robot.push_command()

robot.end_of_arm.move_by('wrist_roll', -1.0)
robot.push_command()
```

#### Gripper

Open the gripper (positive values = open):

```python
robot.end_of_arm.move_to('stretch_gripper', 100) # or `parallel_gripper`
robot.push_command()
```

Close the gripper (negative values = closed):

```python
robot.end_of_arm.move_to('stretch_gripper', -100)
robot.push_command()
```

Move to neutral (zero):

```python
robot.end_of_arm.move_to('stretch_gripper', 0)
robot.push_command()
```

> Note: To use the parallel jaw gripper or a custom tool, replace `stretch_gripper` with `parallel_gripper` or the name of your custom tool.

#### Disable/Enable Torque on a Wrist Joint

Making a joint backdrivable (torque off):

```python
robot.end_of_arm.disable_torque('wrist_yaw')
robot.push_command()
```

Re-enabling:

```python
robot.end_of_arm.enable_torque('wrist_yaw')
robot.push_command()
```

#### Homing the Wrist

If a wrist joint loses its homed state (e.g., after a Feetech motor error and reboot), home it:

```python
robot.routines.routine_wrist_joint_home('wrist_yaw')
```

Or home the entire end of arm at once:

```python
robot.routines.routine_end_of_arm_home()
```

> \[!NOTE] **After a Feetech error:** If a wrist or gripper motor enters an error state (LED blinks red, motor becomes limp), clear it with `stretch_feetech_reboot` from a terminal, then re-home the wrist.

***

### Reading Robot Status

#### Print Full Robot Status

Print all subsystem statuses in a human-readable format:

```python
robot.pretty_print()
```

This outputs a lot of data. To read individual subsystem statuses, access `robot.status` directly:

```python
print(robot.status['lift'])
print(robot.status['arm'])
print(robot.status['omnibase'])
print(robot.status['power_periph'])
print(robot.status['end_of_arm'])
```

#### Key Status Fields

**Lift/Arm:**

```python
robot.status['lift']['pos']                     # Current position (m)
robot.status['lift']['vel']                     # Current velocity (m/s)
robot.status['arm']['pos']                      # Current position (m)
robot.status['arm']['motor']['pos_calibrated']  # True if homed
```

**Omnibase:**

```python
robot.status['omnibase']['x']      # X position estimate (m)
robot.status['omnibase']['y']      # Y position estimate (m)
robot.status['omnibase']['theta']  # Heading (rad)
robot.status['omnibase']['x_vel']  # X velocity (m/s)
robot.status['omnibase']['y_vel']  # Y velocity (m/s)
```

**Power Periph (IMU, battery, runstop):**

```python
robot.status['power_periph']['voltage_cpu']       # NUC supply voltage (V)
robot.status['power_periph']['current_cpu']       # NUC current (A)
robot.status['power_periph']['battery_soc']       # Battery state of charge (%)
robot.status['power_periph']['runstop_event']     # True if runstop active
robot.status['power_periph']['imu']['ax']         # IMU accelerometer X (m/s²)
robot.status['power_periph']['imu']['gravity_tilt']  # Gravity tilt angle (rad)
robot.status['power_periph']['over_tilt_type']    # Tilt direction if tilted (e.g. 'Left Tilt')
```

**Wrist Joint (via end\_of\_arm):**

```python
robot.status['end_of_arm']['wrist_yaw']['pos']             # Position (rad)
robot.status['end_of_arm']['wrist_yaw']['effort']          # Effort (%)
robot.status['end_of_arm']['wrist_yaw']['temp']            # Motor temperature
robot.status['end_of_arm']['wrist_yaw']['overtemp_error']  # Overtemp flag
robot.status['end_of_arm']['wrist_yaw']['pos_calibrated']  # True if homed
```

***

### Waiting for Motion to Complete

Motion commands are asynchronous. Use these methods to synchronize:

#### Wait for Specific Subsystems to Finish Moving

```python
robot.arm.move_to(0.4)
robot.push_command()
robot.wait_on_motion_finish(['arm'], timeout=10.0)
print('Arm done moving')
```

Wait for multiple joints at once:

```python
robot.lift.move_to(0.8)
robot.arm.move_to(0.2)
robot.push_command()
robot.wait_on_motion_finish(['lift', 'arm'], timeout=15.0)
```

#### Wait for All Motion to Complete

To wait for the arm, lift, base, and end of arm to all finish moving:

```python
robot.arm.move_to(0.3)
robot.push_command()
robot.wait_command(timeout=10.0)
```

***

### Guarded Contact Sensitivity

Stretch SE4's lift, arm, and omnibase joints have a **Guarded Contact** safety system. This uses current sensing to detect when actuator effort exceeds a configurable threshold. When triggered, the joint halts until a new command is received.

This protects people and objects from excessive force. By default, contacts are set to a moderate sensitivity. You can change this per use case:

```python
# See available modes
print(robot.get_guarded_contact_modes())

# Set a robot-wide mode
robot.set_guarded_contact_sensitivity('high_sensitivity_manipulation')

# Or set per-joint
robot.arm.set_guarded_contact_sensitivity('high_sensitivity_manipulation')
robot.base.set_guarded_contact_sensitivity('default')
```

You can also pass per-move contact thresholds directly:

```python
# Move arm with high contact sensitivity in the positive direction
robot.arm.move_to(0.3, contact_sensitivity_pos=0.9)
robot.push_command()
```

***

### Power Periph: Beeps, Eyes, and Fan

The Power Periph board controls the speaker buzzer, LED eye animations, RGB lightbar, and chassis fans.

#### Trigger a Beep

```python
robot.power_periph.trigger_beep()
robot.push_command()
```

#### Control LED Eyes

Set animations on the left and right eye rings. Animation indices are defined in the firmware (0 = off, see `stretch_eye_animations` for the full set):

```python
# Set left eye animation index 3, right eye animation index 3
robot.power_periph.set_eye_animation(left_idx=3, right_idx=3)
robot.push_command()
```

#### Toggle the Runstop Programmatically

```python
robot.power_periph.trigger_runstop()
robot.push_command()

# Later, clear it
robot.power_periph.clear_runstop()
robot.push_command()
```

#### Control the Fan

```python
robot.power_periph.set_fan_on()
robot.push_command()

robot.power_periph.set_fan_off()
robot.push_command()
```

***

### Writing a Complete Script

Here is a complete standalone Python script that demonstrates a sequence of moves on Stretch SE4. Save this as `my_first_stretch_script.py`:

```python
#!/usr/bin/env python3
"""
Simple Stretch SE4 demo script.
Run with: python3 my_first_stretch_script.py

Prerequisites:
  - stretch_body_server must be running
  - Robot must be homed
"""

import time
import math
from stretch4_body.robot.robot_client import RobotClient

def main():
    with RobotClient() as robot:

        if not robot.is_homed():
            print('Robot is not homed. Homing now...')
            robot.home()

        print('Stowing robot...')
        robot.stow()

        # --- Lift ---
        print('Raising lift to 0.6 m...')
        robot.lift.move_to(0.6)
        robot.push_command()
        robot.wait_on_motion_finish(['lift'], timeout=15.0)

        # --- Arm ---
        print('Extending arm to 0.3 m...')
        robot.arm.move_to(0.3)
        robot.push_command()
        robot.wait_on_motion_finish(['arm'], timeout=10.0)

        # --- Simultaneous move ---
        print('Moving lift and arm together...')
        robot.lift.move_to(0.8)
        robot.arm.move_by(0.1)
        robot.push_command()
        robot.wait_on_motion_finish(['lift', 'arm'], timeout=15.0)

        # --- Wrist ---
        print('Moving wrist yaw...')
        robot.end_of_arm.move_to('wrist_yaw', 0.5)
        robot.push_command()

        print('Opening gripper...')
        robot.end_of_arm.move_to('stretch_gripper', 100)
        robot.push_command()
        time.sleep(1.0)

        print('Closing gripper...')
        robot.end_of_arm.move_to('stretch_gripper', -50)
        robot.push_command()
        time.sleep(1.0)

        # --- Base ---
        print('Translating base forward 0.2 m...')
        robot.base.translate_by(x_m=0.2, y_m=0.0)
        robot.push_command()
        robot.wait_on_motion_finish(['omnibase'], timeout=10.0)

        print('Rotating base 90 degrees...')
        robot.base.rotate_by(w_r=math.pi / 2)
        robot.push_command()
        robot.wait_on_motion_finish(['omnibase'], timeout=10.0)

        # --- Beep to signal completion ---
        robot.power_periph.trigger_beep()
        robot.push_command()

        print('Demo complete. Stowing...')
        robot.stow()

if __name__ == '__main__':
    main()
```

Run it with:

```bash
python3 my_first_stretch_script.py
```

***

### Feetech Motor Errors

Wrist and gripper Feetech motors can enter an error state after over-force or over-temperature events. When this happens:

* The motor becomes limp and backdrivable
* The LED on the motor body blinks red
* Motion commands are ignored

Clear the error without powering down:

```bash
stretch_feetech_reboot
```

After rebooting, re-home the wrist and gripper:

```bash
stretch_dex_wrist_home
stretch_gripper_home
```

You can also check Feetech motor health at any time:

```bash
stretch_feetech_monitor
```

***

### ROS 2 with Stretch SE4

Stretch SE4 ships with ROS 2 Jazzy. The ROS 2 driver communicates with `stretch_body_server` via the same `RobotClient` interface, making Python and ROS 2 development fully interoperable.

Key ROS 2 topics published by the driver include:

* `/joint_states` — positions and velocities for all joints
* `/wheel_odom` — odometry from the omnibase
* `/scan_filtered` — point clouds from the Hesai QT128 lidars
* `/color/image_raw` — RGB images from the head cameras

Key ROS 2 services include:

* `/home` — trigger full robot homing
* `/stow` — stow the robot

The Jetson add-on (when present) subscribes to camera topics and returns inference results via Zenoh topic bridging. This enables GPU-accelerated perception (e.g., YOLO, pose estimation) without burdening the NUC.

For full ROS 2 examples, refer to the ROS 2 with Stretch tutorial track on Stretch Docs.

***

### Learn More

* [**Robot Overview**](/stretch-4-quick-start-guide/robot-overview) — complete reference for all hardware, sensors, and CLI tools on Stretch SE4
* [**Stretch Safety Guide**](https://docs.hello-robot.com/stretch-4-safety-guide/) — essential safety information before operating the robot

***

### Next Steps

In the next tutorial, **Demo #1 - Mapping & Navigation**, we will look at two ways that Stretch can navigate within a map.


# Demo - Mapping and Navigation

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

A fundamental feature of mobile robots like Stretch is the ability to create a map of an environment and then navigate around it autonomously. The following tutorial will explain how to quickly build a 2-dimensional map that the robot can use to navigate around your current environment, using ROS 2 software called Nav2.

## Building a Map

Building a map of the environment allows the robot to understand where it is currently located, and where you would like it to travel to. Stretch builds maps using the 3D LiDAR sensors located in the robot's head. These sensors allow Stretch to understand the structure of the space around the robot, including the floor, walls, and obstacles. By driving the robot slowly around the space, Stretch builds its understanding of the space, and will create a 2D map of all of the area that it can traverse freely.

{% stepper %}
{% step %}

### Home and Stow the Robot

To make a map accurately, it is important that Stretch's arm and gripper are tucked out of the way so that they do not block the view of the sensors. To do this, run the following commands on Stretch, making sure the arm and wrist have free space to move.

```
stretch_robot_home
stretch_robot_stow
```

{% endstep %}

{% step %}

### Launch the ROS 2 Mapping Node

Start the offline mapping launch file by running the following command in a terminal:

```
ros2 launch stretch_nav2 offline_mapping.launch.py
```

If you want to display the live feed from the robot cameras while driving, you can also run the following in a separate terminal window:

```
stretch_camera_show --opencv --right
```

{% endstep %}

{% step %}

### Begin Teleoperation

In a new terminal, start gamepad teleop by running:

```
stretch_gamepad_teleop
```

Just as you did in the [Quick Start Guide](/stretch-4-quick-start-guide), use the gamepad left stick and bumpers to move the robot around the space. You can watch the laser scan in RViz to understand what areas the robot has already scanned. Blank sections should be filled in by driving the robot around to get a better view.

Once the map of your space looks fairly complete, you can move to the next step. We recommend starting with just one or two rooms for this initial demonstration.
{% endstep %}

{% step %}

### Save the Map

Open a new terminal (leaving the one with the mapping node running) and use the following command to save the map to your fleet directory, replacing `<map_name>` with the name you want to use (eg `stretch_demo_map`)

```
ros2 run nav2_map_server map_saver_cli -f ${HELLO_FLEET_PATH}/maps/<map_name>
```

{% endstep %}

{% step %}

### Close the Mapping Node

Go to the terminal window with the mapping launch file (the one from Step 2), and press Ctrl+C to close the stop the mapping script.
{% endstep %}
{% endstepper %}

{% embed url="<https://player.vimeo.com/video/1205615528>" %}

## Navigating the Map <a href="#phase-2-navigating-the-generated-map" id="phase-2-navigating-the-generated-map"></a>

#### Phase 2: Navigating the Generated Map <a href="#phase-2-navigating-the-generated-map" id="phase-2-navigating-the-generated-map"></a>

{% hint style="warning" %}
Avoid initiating navigation while the robot is docked in the **Stretch Docking Station**. Always undock the robot completely before sending a navigation goal.
{% endhint %}

{% hint style="danger" %}
This demo does **NOT** utilize the line sensors for small object or cliff detection. Keep the area clear, and **keep the robot away from stairs or ledges at all times** during this demo.
{% endhint %}

Now that we have saved a map, we can use it to tell the robot to autonomously navigate through your environment. Stretch will still look for new obstacles around its body, so if an object or person moves through the space, it will adjust and adapt to avoid collisions and plan its movements intelligently.

{% stepper %}
{% step %}

### Launch the ROS 2 Navigation Node

Use the following terminal command to start Nav2 and point it to the .YAML file for the map you just created. Don't forget to update the `<map_name>` parameter to the name you chose above.

```
ros2 launch stretch_nav2 navigation_mppi.launch.py map:=${HELLO_FLEET_PATH}/maps/<map_name>.yaml
```

{% endstep %}

{% step %}

### Set the Initial Robot Position

You should now see the map open on Stretch in the RViz2 software. However, the robot doesn't yet know where it is currently positioned on the map. We'll need to tell it approximately where it is located and which direction it is facing.

In the RViz2 window showing the map, click on the "2D Pose Estimate" button, then click on the robot's current location in the map, and hold and drag in the direction the robot is currently facing. Release to set the robot's orientation.

\#this definitely needs a gif or short video
{% endstep %}

{% step %}

### Set a Navigation Goal

In RViz2, click the "Nav2 Goal" button, then click a position on the map where you want the robot to navigate to. Make sure the position is inside the free area on the map.

Stretch should now start driving to the location you selected autonomously! Try sending Stretch to different points in the room, then interact with it by moving objects around or standing in front of the robot to force it to react to these new obstacles and find alternative routes.
{% endstep %}
{% endstepper %}

<details>

<summary><strong>Troubleshooting and Debugging</strong></summary>

1. **Isolate node errors:** If navigation fails at startup, try adding `use_composition:=false` to the navigation launch command. This starts nodes outside a shared container, which makes errors easier to spot.
2. **Remote operation:** If you use a remote desktop session, you may need to plug in a dummy HDMI adapter so the display server starts correctly. One of these shipped in the accessory box along with your robot
3. **Navigation does not start after setting the pose:** If too much time passes before you set the robot's initial pose, the navigation stack may need to be reset. A common symptom is that the local costmap appears in RViz2, but the global costmap does not.

   To recover:

   1. In the Navigation panel in RViz2, click **Startup**.
   2. Click **Reset**.
   3. Set the robot's initial pose again.

   Once the global costmap appears, the robot is ready to accept navigation goals.

</details>

#### Learn More <a href="#troubleshooting-and-debugging" id="troubleshooting-and-debugging"></a>

To learn more information about ROS 2 Nav2 software, check out the official [Nav2 Getting Started guide](https://docs.nav2.org/getting_started/index.html) to learn the concepts in simulation. For more advanced topics and tuning guides for navigation on Stretch, you can also explore the tutorials in [Navigation University](/stretch4_docs/working-with-stretch/nav_u). In the next section, we'll learn how to launch the Web Teleoperation demo to drive Stretch with a user-friendly interface from a remote PC or mobile phone.


# Demo - Web Teleoperation

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

A common use case for Stretch is to operate the robot remotely, either on a local network or across the Internet. Hello Robot's Web Teleop software makes this possible, allowing an operator to control the robot while streaming the view through any of the robot's cameras. In the following demo, you will learn how to launch and use the Web Teleop demo that comes with Stretch.

### Launching the Demo

On the robot, web teleop can be launched in two ways.

{% stepper %}
{% step %}

### Command Line

Open a Terminal window and type the following to navigate to the correct folder:

```
colcon_cd stretch4_web_teleop
```

Then to launch the demo:

```
./launch_interface.sh
```

{% endstep %}

{% step %}

### Stretch Tray

Click the Hello Robot icon at the top right of the screen, then select "Launch Web Teleop" from the dropdown menu. A Terminal window will open and launch the demo.
{% endstep %}
{% endstepper %}

When the demo launches, the URL for connecting to the robot will be displayed at the bottom of the window (example: <https://192.0.2.1/operator>). Record this for use in the next step.

### Connecting over a local network

On a mobile phone or PC that is connected to the same Wi-Fi network as Stretch, open a web browser (Firefox recommended for best compatibility) and type in the URL as it appeared in the robot's Terminal window. This will connect you to the robot.

{% hint style="info" %}
You might receive an 'invalid certificate' error that asks you if you want to proceed with the connection - this is expected, and it is OK to proceed.
{% endhint %}

After a few seconds, you should see the Web Teleop interface display in your browser:<br>

\#Insert web teleop default image

{% hint style="info" %}
Add hint about homing the robot if unhomed?
{% endhint %}

Using this interface, you can directly control all of the robot's joints, switch between camera views, change speed settings, .

\#Labeled diagram of interface options

\#Insert video screengrab of \~ 1 minute of web teleop

### Advanced capabilities - autonomous navigation

{% hint style="info" icon="book-open" %}
Documentation Coming Soon
{% endhint %}

### Advanced capabilities - click-to-pregrasp

{% hint style="info" icon="book-open" %}
Documentation Coming Soon
{% endhint %}

### Connecting remotely over the Internet

It is possible to use Web Teleop entirely remotely over an internet connection, using a service like ngrok for secure tunneling. Instructions and tips can be found in the [stretch4\_web\_teleop](https://docs.hello-robot.com/stretch-4-web-teleop/) repository.


# Additional Resources

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

While we hope the documentation found here gets you successfully started developing with your robot, there are a number of other resources worth noting that provide additional information and ways to interact with Stretch and the larger Hello Robot community.

### :speech\_balloon: Forum <a href="#forum" id="forum"></a>

Stretch has a diverse and vibrant user community, and one of our goals at Hello Robot is to help our customers connect, collaborate, and share with one another. To this end, we have created a [public Hello Robot Forum](https://forum.hello-robot.com/) where you can ask questions, post about your work, request support, or search an archive of resolved issues. Our engineers regularly read and respond to threads here.

### :computer: GitHub <a href="#github" id="github"></a>

Nearly all the code we write for Stretch is open-source and freely available on our [Hello Robot GitHub](https://github.com/hello-robot). Feel free to browse through the repos or dig deeper into the code. For information on how to contribute to Stretch software, see the \[Contribution Guide].

#### Simulating Stretch

Stretch 4 is simulated in Google Deepmind's [MuJoCo](https://mujoco.org/) using `stretch4_mujoco`: <https://github.com/hello-robot/stretch4_mujoco>

You can also control Stretch 4's stretch4\_mujoco in ROS2 using the `stretch_simulation` ROS2 package: <https://github.com/hello-robot/stretch4_ros2/tree/jazzy/stretch_simulation>

#### Additional Demos

There are many demos that are published to GitHub that are not shipped on the robot. Typically, you would clone these demos to your robot's `~/repos` directory and follow the instructions in the README to set them up.

| [`stretch4_kinematics`](https://github.com/hello-robot/stretch4_kinematics)                 | Kinematics and task-space control library for Stretch 4                 | Python |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ |
| [`stretch4_grasping_demo`](https://github.com/hello-robot/stretch4_grasping_demo)           | AI-powered demo utilizing VLMs and object tracking for visual servoing. | Python |
| [`stretch4_hybrid_marker_demo`](https://github.com/hello-robot/stretch4_hybrid_marker_demo) | LiDAR-reflective material + visible light ArUco marker demo.            | Python |
| [`stretch4_rgbd`](https://github.com/hello-robot/stretch4_rgbd)                             | Methods to enhance and process RGB-D imagery from onboard cameras.      | Python |
| [`stretch4_human_perception`](https://github.com/hello-robot/stretch4_human_perception)     | Specialized tools to enable the robot to perceive and track humans.     | Python |
| [`stretch4_compliant_gripper`](https://github.com/hello-robot/stretch4_compliant_gripper)   | Modeling and control code for the standard Stretch 4 compliant gripper. | Python |

### :people\_holding\_hands: Community Updates <a href="#community-updates" id="community-updates"></a>

We love to help spread the work about the great work our community is constantly publishing! Every month, we publish a \[Stretch Community Update] with some highlights of the recent research done by Stretch customers on our website, mailing list and social media. When you have work that is ready to make public, \[let us know] and we'll make sure it's seen by our community!

### :wrench: Get Some Support

If you've run into a hardware problem, need urgent assistance, or just can't find the answer to your questions and want to chat directly with a Hello Robot engineer, please feel free to contact us directly at support \[at] hello-robot.com. We love talking to Stretch users, and your feedback helps us improve the product for all our users.

<br>


# Stretch 4 Safety Guide

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

{% hint style="warning" %}
Warning: There are many [safety features](#safety-features) that are built into Stretch. At the time of writing, two important features are still not implemented to run on most demo's by default:&#x20;

1. Cliff detection: Stretch will not stop if driven off a threshold or a flight of stairs. Please do not navigate or teleoperate the robot near stairs or sharp drops.
2. Ramps: Stretch will classify a ramp as a tilt event, and the wheels will go into freewheel. This means that if the robot is at the top of the ramp, it will roll down freely and quickly. It will not be able to climb ramps.
   {% endhint %}

\
Stretch is a potentially dangerous machine with safety hazards. If improperly used it can cause injury or death.

Stretch is intended for use by researchers to conduct research in controlled indoor environments. This product is not intended for other uses and lacks the required certifications for other uses, such as use in the home by consumers.

### Safety Hazards

We have designed Stretch to be safer than previous commercially-available human-scale mobile manipulators, so that researchers can explore the future of mobile manipulation. For example, we have made it smaller and lighter weight with backdrivable torque-sensing joints that can stop when they detect contact.

Nonetheless, Stretch is a research robot that can be dangerous. It is essential that researchers use Stretch carefully to avoid damage, injury, or death. Here, we list a number of safety hazards that researchers must consider prior to use and during use of Stretch.

#### **Stretch Can Put People And Animals At Risk**

As described in more detail later, Stretch can put people and animals at risk. People and animals near the robot must be closely supervised at all times. At all times, an experienced researcher must carefully monitor the robot and be prepared to stop it. Any people near the robot must be made aware that the robot could be dangerous. Prior to any use of the robot near people or animals, researchers must carefully assess and minimize risks.

#### **Stretch Can Topple Onto A Person**

The robot may drive off stairs, push or pull itself over with its telescoping arm, fall over while attempting to traverse a threshold, or encounter obstacles that cause it to fall on or otherwise collide with people, causing injury.

Operate the robot only on flat surfaces away from stairs or other obstacles that may cause it to topple, and do not allow the robot to push or pull itself over.

#### **Stretch Should Not Be Lifted By A Single Person**

Stretch weighs 85 lbs (\~60lbs with the battery removed), so two or more people should lift and carry the robot. A single person can move the robot around by enabling the runstop button and rolling it on flat ground, or using a hand truck or dolly.

At least two people should lift and carry the robot when needed. The battery should always be removed from the robot when lifting the robot.

#### **Stretch Can Cause Lacerations**

The robot may have sharp edges that can cause lacerations or punctures to skin or the eyes.

Operate the robot away from eyes and other sensitive body parts.<br>

#### **Stretch Can Trap, Crush, Or Pinch Body Parts**

The robot has moving joints that can trap, crush or pinch hands, fingers, or other body parts. The robot could also injure a person or animal by driving over a body part.

Keep body parts away from trap, crush, and pinch points during robot motion, including underneath the wheels.

#### **Stretch Can Entrap Loose Clothing Or Hair**

The robot's shoulder and arm have rollers that can pull in and entrap loose clothing or hair. The robot wheels are composed of rotating parts that could also trap loose hair.

Keep loose clothing and long hair away from the robot's shoulder, telescoping arm, and wheels when in motion.

#### **Stretch Has Flammable Components**

The robot has PLA and polyurethane covers that are flammable and must be kept away from potential ignition sources, such as open flames and hot surfaces.&#x20;

Keep the robot away from potential ignition sources and always have a working fire extinguisher nearby.

#### **Stretch Is An Electrical Device**

Stretch has batteries, electronics, wires, and other electrical components throughout its body. It also provides uncovered connectors that provide power. While the robot has fuses to reduce electrical risks, users must be careful.

Keep the robot dry and away from liquids, avoid electrical shocks, ensure power cables and wires are in good condition, be careful with the robot’s connectors, and generally exercise caution while working with this electrical device.

#### **Stretch Can Perform Dangerous Activities**

Stretch is a versatile robot capable of performing many actions, including actions that would be dangerous to people. For example, if a dangerous object is held by or affixed to the robot, such as a knife, a heavy object, or breakable glass, the robot can become very dangerous. Likewise, the robot is capable of physically altering the environment in ways that would be dangerous, such as turning a knob that releases gas from a gas stove.

Users must be cautious while using the robot to ensure it interacts safely with people and the surrounding environment.

#### **Stretch Is An Open Platform That Can Be Made More Dangerous**

Stretch is an open platform with user-modifiable and user-extensible hardware and software. User changes to the hardware or software can entail serious risks. For example, when shipped, the robot has conservative settings that restrict its speed and the forces it applies to reduce the risks associated with the robot. By modifying the robot, users could enable the robot to move at unsafe speeds and apply unsafe forces. As another example, improper electrical connections could result in a fire.

Researchers who choose to modify or extend the robot’s hardware or software do so at their own risk, and should be careful to understand the implications of their modifications or extensions. Changes to the robot could result in dangerous situations that cause injury or death.

### Additional Risks

The most important aspects of safety with Stretch are to use good judgment and common sense. Additional important considerations follow:

* If the robot appears to be damaged, stop the robot immediately.
* Always be ready to stop the robot.
* Do not operate the robot unless an experienced user is present and attentive.
* Be aware that the robot can move in unexpected ways.
* Do not put fingers or other objects into the channel that runs along the length of the mast. A belt moves within this channel.
* Keep an eye on cords, rugs, and any other floor hazards as the robot drives.
* Keep the robot at least 3 meters from ledges, curbs, stairs, and any other toppling hazard.
* Do not operate the robot outdoors.
* Do not attempt to ride the robot.
* Do not have the robot hold sharp objects.
* Do not attempt to service the robot without supervision by Hello Robot.

### Other Problems Will Likely Occur

“Anticipate potential problems and hazards. Always imagine what might happen if the robot malfunctions or behaves in a way different from the desired action. Be vigilant.” - [PR2 User Manual](https://www.clearpathrobotics.com/assets/downloads/pr2/pr2_manual_r321.pdf) by Willow Garage from October 5, 2012

Stretch is a complex device that includes many mechanical, electrical, and computational systems that have been designed to work together. Be prepared for something to go wrong. For example, a motor control board might fail, software might not operate as anticipated, an unexpected process might still be running on the robot, or the batteries for the Xbox-style controller or the robot itself might run out.

### Safety Features

{% hint style="warning" %}
Warning: At the time of writing, two important features are still not implemented to run on most demo's by default:&#x20;

1. Cliff detection: Stretch will not stop if driven off a threshold or a flight of stairs. Please do not navigate or teleoperate the robot near stairs or sharp drops.
2. Ramps: Stretch will classify a ramp as a tilt event, and the wheels will go into freewheel. This means that if the robot is at the top of the ramp, it will roll down freely and quickly. It will not be able to climb ramps.
   {% endhint %}

We have considered safety from the outset in the design of Stretch.

* Runstop: The illuminated runstop button on Stretch’s head can be used to pause operation of the robot joints when it is in motion.
* Lightweight design: The overall mass of Stretch is less than 100lbs, and the majority of the mass is in the base. While this reduces the risk of crushing, crushing injury can still occur and should be carefully monitored.
* Gravity friendly: The arrangement of Stretch’s design means that it doesn’t have to counteract gravity on a larger lever arm. As a result, the motors and gearboxes are much lower torque and lower weight than a traditional robot, allowing us to avoid the often dangerous strong joints.
* Low gear ratio: The primary joints of Stretch have low gear-ratios (approx 5:1), allowing for backdriving of joints when powered off. A low gear-ratio also reduces the effective inertia of each joint, limiting the impacted force during undesired contacts with people and the environment.
* Contact Sensitivity: The primary joints of Stretch have contact sensitivity. We measure motor currents to estimate contact forces. Because Stretch is a low gear-ratio robot, current sensing provides a fairly sensitive measure of contact forces.
* Firmware limits: Motor torques are limited at the lowest level of the firmware to configured bounds.
* Velocity limits: Fast motions of the base are restricted when the lift is up high. This limits the likelihood of toppling.
* Tilt detection: The robot can detect when its body is tilted beyond a safe threshold. The robot can be configured to trigger a runstop event during an over-tilt event.

#### **Runstop**

The runstop allows the user to pause the motion of the primary actuators by tapping the illuminated button on the head. An experienced operator should always keep the runstop within reach, allowing them to stop the motion of the robot if it is deemed unsafe.

NOTE: The runstop is not equivalent to an Emergency Stop found on industrial equipment and no safety guarantees are made by its function.

When the runstop is enabled, these actuators are in a ‘Safety Mode’ that inhibits the motion controller at the firmware level. Disabling the runstop allows normal operation to resume.

The runstop logic is:<br>

| Action                           | Runstop State   | Button Illumination |
| -------------------------------- | --------------- | ------------------- |
| Robot startup                    | Motion enabled  | Solid               |
| Tap runstop button               | Motion disabled | Flashing at 1Hz     |
| Hold down runstop button for >2s | Motion enabled  | Solid               |

### **Safety Hazard Details**

#### Sharp Edges

Stretch is a piece of laboratory equipment. As such, its structure has moderately sharp edges and corners that can be unsafe. These edges can get snagged during motion, or they may cause lacerations when sufficient force is applied to a person. Care should be taken when grasping or otherwise making contact with Stretch that a sharp corner or edge is not contacted.

#### **Toppling**

Stretch is a relatively lightweight robot. In some kinematic configurations a high center of gravity can make it prone to toppling. Toppling can occur when:

* The mobile base is moving at moderate or fast speed and hits a bump, threshold, or other change in floor property.
* The lift is raised up high and pushes on the environment with sufficient force.
* The robot drives over a drop-off such as a stair or curb.

#### **Pinch Points**

Pinch points around the robot's shoulder can cause discomfort and care should be taken when handling these joints as they move.

The shoulder, which travels up and down on the lift, has a series of rollers that ride along the mast. While the shoulder shells can prevent large objects from getting pinched by the rollers, small and thin objects can be pulled into and crushed.

Extra care should be taken with long hair, clothing, and small fingers around the shoulder rollers.

#### **Crush Points**

The lift degree of freedom is the strongest joint on the robot and as such can apply potentially unsafe forces to a person.

The lift, while in motion, may trap or crush objects between the ‘shoulder’ and another surface. As such, best practices for lift safety should always be used when using the lift degree of freedom.

The lift has a max theoretical strength of over 200N of linear force. In practice, this force is limited by the lift’s Guarded Move function, which places the lift in Safety Mode when the actuator forces exceed a threshold.

\
\
\ <br>


# Stretch 4 Hardware Guide

This manual provides the engineering data and user guidance for working with the Hello Robot Stretch 4 hardware.

## Disclaimer

{% hint style="warning" %}
The Hello Robot Stretch is intended for use in the research of mobile manipulation applications by users experienced in the use and programming of research robots. This product is not intended for general use in the home by consumers, and lacks the required certifications for such use. Please see the section on Regulatory Compliance for further details.
{% endhint %}

## Functional Specification

![](/files/uzOOTL74ZW2kA7Fr5Mla)

<figure><img src="/files/Xg217ueI9AjGvdry52hv" alt=""><figcaption></figcaption></figure>

Full system datasheet [available here](https://drive.google.com/file/d/1lyNPdvGHvn5w1UVGlV-HEfL53Mco89LW/view?usp=sharing).

## Body Plan

![](/files/AsLW3VksmjkcTyU5TPvP)

## Hardware Architecture

![](/files/G3i9ZfXt60XglC0B3wOa)

## Robot Subsystems

### Base

The base is a 3-omniwheel holonomic drive. It includes 6 Laser Line Sensors to allow detection of stairs, thresholds, etc. Charging region and I/O ports are located in the "trunk" on one side of the base.

![](/files/aYJ3JSgD7VjDoTRi8WjU)

|                                    |                                                                                                |
| ---------------------------------- | ---------------------------------------------------------------------------------------------- |
| Item                               | Notes                                                                                          |
| Holonomic Drive                    | 3X 8" diameter Omniwheels.                                                                     |
| Line Sensor Array                  | 6X Pixart Laser Line Sensors for cliff and hazard monitoring.                                  |
| Removable Shell for Battery Access | Please refer to the Battery Swap section.                                                      |
| Docking Region                     | Spring-loaded contacts mating with the Docking Station for wireless charging.                  |
| Charge Port                        | <p>Barrel Jack Connector. <br>!! Only use the Official Hello Robot 36V Charger!!</p>           |
| ON/SLEEP/OFF                       | <p>ON/OFF Button.<br>ON : Green solid LED.<br>SLEEP : Red blinking LED.<br>OFF : No LED.</p>   |
| ETH/USB/HDMI Ports                 | <p>ETH : 1X RJ45 Port.<br>USB : 2X USB-A 2.0 & 1X USB-A 3.0 Ports.<br>HDMI : 4K HDMI Port.</p> |

![](/files/RMcFjBKxrRS9TeUNc37w)

### Battery Swap

Stretch 4 features a removable battery that can be accessed by removing the top shell of the base.

![](/files/TbOkLI4ViIhpIUJWCWtm)

|        |                                                        |                                                                                 |
| ------ | ------------------------------------------------------ | ------------------------------------------------------------------------------- |
| Step 1 | Turn off robot.                                        | Hold the power button for \~6 seconds until the LED turns off.                  |
| Step 2 | Remove base top shell.                                 | Shell has magnets that keep it in place. No tool required.                      |
| Step 3 | Unplug the cable from the battery.                     | Press the latch and pull the cable.                                             |
| Step 4 | Unlatch the battery from the top plate.                | Flip both draw latches to release the battery from the strike plates.           |
| Step 5 | Remove / Swap battery.                                 | Be mindful when manipulating the battery: **it weighs 30lbs**!!                 |
| Step 6 | Secure the battery on the top plate with both latches. | Make sure the battery is securely held in place before moving to the next step. |
| Step 7 | Plug the cable to the battery.                         | Make sure the cable is latched to the battery before closing the robot.         |
| Step 8 | Install top shell and turn the robot on.               | That's it :)                                                                    |

{% hint style="danger" %}
DO NOT charge the battery with any other charger that is not an official Hello Robot charger.
{% endhint %}

{% hint style="warning" %}
Replacing Stretch 4's battery is NOT a "hot swap". The robot will cut power during the swap.
{% endhint %}

Battery removal tutorial:

{% embed url="<https://drive.google.com/file/d/1V_L74vf8SQa0FjFjGQAlCPKhW-SISvxx/view?usp=sharing>" %}

Battery installation tutorial:

{% embed url="<https://drive.google.com/file/d/1qX0KM0f0DktYKUE09VTDP_x_GvSsCCSB/view?usp=sharing>" %}

{% hint style="warning" %}
**When lifting the robot (with or without the shipping box), please remove the battery.** The robot weighs \~60lbs without the battery, making it more comfortable to transport.
{% endhint %}

### Head

The head includes a high definition sensor suite with 2 Hemispherical LiDARs and 3 RGB Cameras. It also has a runstop, battery level indicators and a developer interface to allow for additional user hardware. The eye rings can be used for animations.

![](/files/3rCrnxFVSSIsgn7VZd0A)

|                          |                                                                                                                                                                                                                                                                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Item                     | Notes                                                                                                                                                                                                                                                                                                                          |
| Hemispherical LiDAR (2X) | Dual Hemispherical 3D LiDAR (360° x 189°) provide a depth map of the world around the robot at all times with over 2,000,000 measurements per second.                                                                                                                                                                          |
| Fisheye Camera (2X)      | <p>Dual Global Shutter RGB Cameras (2.3MP) provide wide FOV; calibrated to LiDARs provide RGBD data all around the body.<br>Eye LEDs for animation.</p>                                                                                                                                                                        |
| Teleoperation Camera     | High-Res RGB Camera (12MP) for remote teleoperation.                                                                                                                                                                                                                                                                           |
| Runstop                  | The runstop allows the user to pause the motion of the four primary DOF (base, lift, and arm) by tapping the illuminated button on the head. When the runstop is enabled, these DOF are in a ‘Safety Mode’ that inhibits the motion controller at the firmware level. Disabling the runstop allows normal operation to resume. |
| State of Charge LEDs     | 8X Discrete LEDs that indicate the battery State of Charge.                                                                                                                                                                                                                                                                    |
| Speaker                  | Noise reduction microphone and speaker.                                                                                                                                                                                                                                                                                        |
| USB Hub                  | 1X USB-A 3.2 & 1X USB-C 3.2 Ports.                                                                                                                                                                                                                                                                                             |

#### Mounting Points

The top of the head includes 3x M4 threaded and 4x M5 threaded mounting points as shown below:

![](/files/iCKR8LaXkKSTVSiABlry)

### Lift

The lift degree of freedom provides vertical translation of the arm. It is driven by a closed loop stepper motor, providing smooth and precise motion through a low gear-ratio belt drive. The brake release button allows the motion of the arm when the robot is off.

![](/files/59qQpNhPA56qLpk9o96U)

### Telescopic Arm

![](/files/hfhCGybP4yTuVVe1F5Tn)

The arm comprises 5 telescoping aluminum links set on rollers. Its proprietary drive train is driven by a stepper motor with closed loop control and current sensing, allowing contact sensitivity during motion.

The retracted arm and wrist combined are designed to fit within the footprint of the base. The arm is designed to have:

* Reach: 0.55m
* Max payload (Retracted) : 4kg
* Max payload (Extended) : 2.5kg

<figure><img src="/files/GGwgtS5uguceNqBEiX3F" alt=""><figcaption></figcaption></figure>

### Dexterous Wrist

The Stretch 4 dexterous wrist is 3 DOF (Yaw, Pitch, Roll) fully internally cabled system with an OAK-D SR camera at the roll joint for precise grasping and servoing.&#x20;

The quick connect interface at the roll joint gives freedom to the user to use different end effector tools.&#x20;

A USB-A 2.0 Port for accessory mounting can be found at the top of the yaw joint shell.

![](/files/Ds4cvUss9gqzr8iX9em3)

Each joint is powered by a FeeTech SM80BL. The range of motion of each joint 310°.

{% embed url="<https://drive.google.com/file/d/1bCu3H3P_sHZUt5gcskf0sOxqpNozYrD2/view?usp=sharing>" %}

#### Quick Connect Interface

Pressing the pink button next to the OAK-D SR Camera allows the tool to be removed.

If you would like to use your own end-effector tool with the quick connect interface, please contact <support@hello-robot.com> for CAD and hardware support.

{% embed url="<https://drive.google.com/file/d/1j1wevuM0LtQTT_FLPJn6j1Rp5x5ICZ5Z/view?usp=sharing>" %}

### Stretch Compliant Gripper

The Stretch Compliant Gripper utilizes a FeeTech SM80BL servo to drive the spring grasper mechanism.&#x20;

It also includes Aruco tags to enable precise visual servoing, as well as mounting points to enable ‘Puppet’ based on-device data collection.

<figure><img src="/files/Ioo3tDFOJn30hnAHEMIl" alt=""><figcaption></figcaption></figure>

## Robot Accessories

### Parallel Jaw Gripper

The Parallel Jaw Gripper utilizes a FeeTech SM80BL servo to drive the linkage mechanism.&#x20;

The fingers can be changed to a design of your choice. It comes standard with the ‘Aloha’ style fingers that have shown success in Physical AI for precise, dexterous manipulation.

It also includes Aruco tags to enable precise visual servoing, as well as mounting points to enable ‘Puppet’ based on-device data collection.

<figure><img src="/files/colPQXxE6dW9yeuyEJyU" alt=""><figcaption></figcaption></figure>

### Puppet Teleop Pistol Accessory

Stretch 4 comes with an accessory that attaches to the bottom of the Stretch Compliant Gripper or the Parallel Jaw Gripper. It can be used for data collection and remote control of a second Stretch 4 robot.

<figure><img src="/files/nCOSMTCCqSHrolYyTbYO" alt=""><figcaption></figcaption></figure>

### NVIDIA Jetson Orin NX

The NVIDIA Jetson Orin NX comes installed within the Stretch base and is ready for development.&#x20;

It includes 128GB NVME and 16GB memory. It has its own WiFi as well as Ethernet networking to the main compute.

<figure><img src="/files/q62OuJL1UEE9kHkJGeYS" alt=""><figcaption></figcaption></figure>

### Docking Station

The docking station sits on a flat floor against a wall, allowing Stretch to autonomously dock itself and begin charging. It includes 3X reflective Aruco tags, enabling Stretch to detect the station in low-light or dark environment using its LiDARs.

<figure><img src="/files/mftooMkw3sQjdu5kLSyM" alt=""><figcaption></figcaption></figure>

## Robot Care

### Transporting the Robot

Stretch was designed to be transported in the back of a car, up a staircase, or around a building.&#x20;

For short trips, the robot can be simply rolled around by grabbing its mast.&#x20;

{% hint style="warning" %}
**For safety, please use two people to lift the robot.**
{% endhint %}

{% hint style="warning" %}
**When lifting the robot (with or without the shipping box), please remove the battery.** The robot weighs \~60lbs without the battery, making it more comfortable to transport.
{% endhint %}

{% hint style="warning" %}
In a car or in any other means of transportation, it is heavily recommended to transport Stretch 4 in its shipping box to not damage the robot or alter the calibration of the system.
{% endhint %}

{% hint style="info" %}
It may be picked up by its mast and carried up stairs as well. You can use the eye hook located on the top plate to attach a handle. You can also use the handle located on the bottom of the robot for a better grasp.
{% endhint %}

### Belt Tension

A neoprene timing belt drives the arm up and down the lift. It may loosen over long periods of time if it experiences sustained loading. In this case, slack will become visually apparent in the belt as the lift moves.

The belt is very straightforward to re-tension. Please contact <support@hello-robot.com> for tensioning instructions.

### Keeping the Robot Clean

The robot surfaces can be wiped down with an alcohol wipe or a moist rag from time to time in order to remove any debris or oils that accumulate on the shells or mast.

The drive wheels can accumulate dust over time and begin to lose traction. They should be periodically wiped down as well.

If the robot cameras or LiDARs requires cleaning, use appropriate lens cleaning fluid and a microfiber cloth.

Fan filters at the bottom of the robot can accumulate dust over time and cause overheating in the base. Removing the dust with a soft brush will help keep thermals inside the base normal.

### Keeping the Robot Calibrated

The robot comes pre-calibrated with a robot-specific URDF. This calibration allows the LiDARs and cameras to accurately estimate its environment and own body.

The robot may become slightly uncalibrated over time for a variety of reasons, including normal wear and tear and loosening of robot joints, or accidental collisions or falls leading to high loads on the robot joints.

The calibration accuracy can be checked using the provided ROS tools. If necessary, the user can recalibrate the robot. See the [Stretch URDF Calibration Guide](https://github.com/hello-robot/stretch_ros2/tree/humble/stretch_calibration#overview) for more information.

### System Check

It is useful to periodically run stretch\_system\_check.py. This will check that the robot's hardware devices are present and within normal operating conditions.

## Regulatory Compliance

Stretch is not certified for use as a consumer device in the U.S., EU, or elsewhere.

Unless stated otherwise, Stretch is not subjected to compliance testing nor certified to meet any requirements, such as requirements for EMI, EMC, or ESD.

Per [FCC 47 CFR, Part 15, Subpart B, section 15.103(c)](https://www.law.cornell.edu/cfr/text/47/15.103), we claim Stretch as an exempted device, since it is a digital device used exclusively as industrial, commercial, or medical test equipment, where test equipment is equipment intended primarily for purposes of performing scientific investigations.

[OET BULLETIN NO. 62](https://transition.fcc.gov/bureaus/oet/info/documents/bulletins/oet62/oet62rev.pdf), titled "UNDERSTANDING THE FCC REGULATIONS FOR COMPUTERS AND OTHER DIGITAL DEVICES" from December 1993 provides further clarification of the Section 15.103(c) exemption: “*Test equipment* includes devices used for maintenance, research, evaluation, simulation and other analytical or scientific applications in areas such as industrial plants, public utilities, hospitals, universities, laboratories, automotive service centers and electronic repair shops.”

***

All materials are Copyright 2026 by Hello Robot Inc. Hello Robot and Stretch are registered trademarks.


# README

### Overview

This repository holds ROS 2 Jazzy packages for the Stretch 4 mobile manipulator from Hello Robot Inc. On a fresh robot, these packages are built and available through the `~/ament_ws` workspace.

Stretch 4 ROS packages contain breaking changes from Stretch 3 and earlier hardware versions. ROS packages for earlier versions can be found in [stretch\_ros2](https://github.com/hello-robot/stretch_ros2).

### Packages

| Resource                                                              | Description                                                                                         |
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [stretch\_core](/stretch4-ros2-repo/stretch_core)                     | ROS 2 drivers for Stretch 4                                                                         |
| [stretch\_description](/stretch4-ros2-repo/stretch_description)       | Visualize Stretch 4's URDF                                                                          |
| [stretch\_nav2](/stretch4-ros2-repo/stretch_nav2)                     | Navigation stack built on Nav2                                                                      |
| [stretch\_simulation](/stretch4-ros2-repo/stretch_simulation)         | Simulation of Stretch 4, built on [Stretch4 Mujoco](https://github.com/hello-robot/stretch4_mujoco) |
| [stretch\_tag\_perception](/stretch4-ros2-repo/stretch_python_bridge) | Detect aruco tags with Stretch 4                                                                    |
| [stretch\_python\_bridge](/stretch4-ros2-repo/stretch_python_bridge)  | A high-level Python API that abstracts away rclpy                                                   |
| [hello\_helpers](/stretch4-ros2-repo/hello_helpers)                   | Miscellaneous helper code used across the stretch\_ros2 repository                                  |

### Licenses

For license details for this repository, see the LICENSE files found in the directories. A summary of the licenses follows:

| Directory                | License                                                                               |
| ------------------------ | ------------------------------------------------------------------------------------- |
| stretch\_core            | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)                              |
| stretch\_description     | [BSD 3-Clause Clear License](https://choosealicense.com/licenses/bsd-3-clause-clear/) |
| stretch\_nav2            | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)                              |
| stretch\_simulation      | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)                              |
| stretch\_tag\_perception | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)                              |
| stretch\_python\_bridge  | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)                              |
| hello\_helpers           | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)                              |

### Common ROS2 commands

Below is an unordered list of common or useful ROS2 commands:

* Start Stretch Driver: `ros2 launch stretch_core stretch_driver.launch.py`
  * Teleop twist keyboard: `ros2 run teleop_twist_keyboard teleop_twist_keyboard`
* Start Lidars: `ros2 launch stretch_core dual_hesai.launch.py`
* Start Cameras: `ros2 launch stretch_core luxonis.launch.py use_center:=true use_left:=true use_right:=true` for the head cameras and `ros2 launch stretch_core gripper_camera.launch.py` for the gripper camera.
* Navigation (See <https://docs.hello-robot.com/stretch4_docs/working-with-stretch/getting-started/demo-mapping-and-navigation#set-a-navigation-goal>):
  * Start Mapping: `ros2 launch stretch_nav2 offline_mapping.launch.py` and `stretch_gamepad_teleop`
  * Save a map: `ros2 run nav2_map_server map_saver_cli -f ${HELLO_FLEET_PATH}/maps/<map_name>`
  * Start nav2: `ros2 launch stretch_nav2 navigation_mppi.launch.py map:=${HELLO_FLEET_PATH}/maps/<map_name>.yaml`
* Web teleop (See <https://docs.hello-robot.com/stretch4_docs/working-with-stretch/getting-started/demo-web-teleoperation>): `~/ament_ws/src/stretch4_web_teleop/launch_interface.sh` and navigate to `https://localhost/operator`
* Record rosbags/mcap files: `ros2 bag record -a` . Note: `-a` records all the topics, if lidar and cameras are streaming, a few minutes of recording are hundreds of gigabytes large.
  * Play rosbags/mcap files: `ros2 bag play ./path/to/recording`
* View topics: `ros2 topic list` and `ros2 topic echo /joint_states`
* View kinematic links: `ros2 run tf2_tools view_frames`
  * View transform between two links: `ros2 run tf2_ros tf2_echo base_link head_link`

### Troubleshooting

#### Zenoh middleware not working

Zenoh should start as a linux user service on Stretch 4. Run `systemctl --user status zenoh.service` to see if it has failed to start. Run `systemctl --user restart zenoh.service` to restart it.

If you would like to manually start it, use `ros2 run rmw_zenoh_cpp rmw_zenohd`.

#### rviz2 is not launching with a Launch file

There is a known conflict between opencv and rviz2 in ROS2 Jazzy that is documented and a solution is proposed here: <https://github.com/hello-robot/stretch4_ros2/issues/19>

#### Topics are not appearing

There are a number of reasons for topics to not appear.

1. Check that you are running the ros2 node and the client on the same computer. If you are trying to connect from a different computer, you may need to add the following environment variables to your client session:

```
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
# Replace ROBOT_IP with your Stretch's IP address
ROBOT_IP=192.168.X.XXX
export ZENOH_CONFIG_OVERRIDE='mode="client";connect/endpoints=["tcp/$ROBOT_IP:7447"]'
```

2. Stretch 4 is configured with `export RMW_IMPLEMENTATION=rmw_zenoh_cpp` in the .bashrc file to tell ROS2 to use the Zenoh middleware. Make sure your terminal session with this env variable.
3. Use `ros2 topic list` to list topics. Also, `ros2 topic echo /joint_states` to listen to a topic
4. Check that the TF tree has all the links: `ros2 run tf2_tools view_frames`

<br>


# hello\_helpers

### Overview

*hello\_helpers* mostly consists of the hello\_helpers Python module. This module provides various Python files used across stretch\_ros2 that have not attained sufficient status to stand on their own.

### Capabilities

*fit\_plane.py* : Fits planes to 3D data.

*hello\_misc.py* : Various functions, including a helpful Python object with which to create ROS nodes.

*hello\_ros\_viz.py* : Various helper functions for vizualizations using RViz.

### Typical Usage

```python
import hello_helpers.fit_plane as fp
```

```python
import hello_helpers.hello_misc as hm
```

```python
import hello_helpers.hello_ros_viz as hr
```

## API

### Classes

#### [HelloNode](https://github.com/hello-robot/stretch4_ros2/blob/jazzy/hello_helpers/src/hello_helpers/hello_misc.py)

This class is a convenience class for creating a ROS 2 node for Stretch. The most common way to use this class is to extend it. In your extending class, the main function would call `HelloNode`'s main function. This would look like:

```python
import hello_helpers.hello_misc as hm

class MyNode(hm.HelloNode):
    def __init__(self):
        hm.HelloNode.__init__(self)

    def main(self):
        hm.HelloNode.main(self, 'my_node', 'my_node', wait_for_first_pointcloud=False)
        # my_node's main logic goes here

node = MyNode()
node.main()
```

There is also a one-liner class method for instantiating a `HelloNode` for easy prototyping. One example where this is handy in sending pose commands from iPython:

```python
# roslaunch the stretch launch file beforehand

import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
temp.move_to_pose({'lift_joint': 0.4})
```

**Attributes**

**`dryrun`**

This attribute allows you to control whether the robot actually moves when calling `move_to_pose()`, `home_the_robot()`, `stow_the_robot()`, or other motion methods in this class. When `dryrun` is set to True, these motion methods return immediately. This attribute is helpful when you want to run just the perception/planning part of your node without actually moving the robot. For example, you could replace the following verbose snippet:

```python
# launch the stretch driver launch file beforehand
import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
actually_move = False
[...]
if actually_move:
    temp.move_to_pose({'translate_mobile_base': 1.0})
```

to be more consise:

```python
# launch the stretch driver launch file beforehand
import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
[...]
temp.dryrun = True
temp.move_to_pose({'translate_mobile_base': 1.0})
```

**Methods**

**`move_to_pose(pose, blocking=False, custom_contact_thresholds=False, duration=2.0)`**

This method takes in a dictionary that describes a desired pose for the robot and communicates with [stretch\_driver](/stretch4-ros2-repo/stretch_core#stretchdrivernodesstretchdriver) to execute it. The basic format of this dictionary is string/number key/value pairs, where the keys are joint names and the values are desired position goals. For example, `{'lift_joint': 0.5}` would put the lift at 0.5m in its joint range. A full list of command-able joints is published to the `/stretch/joint_states` topic. Used within a node extending `HelloNode`, calling this method would look like:

```python
self.move_to_pose({'lift_joint': 0.5})
```

Internally, this dictionary is converted into a [JointTrajectory](https://docs.ros2.org/latest/api/trajectory_msgs/msg/JointTrajectory.html) message that is sent to a [FollowJointTrajectory action](http://docs.ros.org/en/noetic/api/control_msgs/html/action/FollowJointTrajectory.html) server in stretch\_driver. This method waits by default for the server to report that the goal has completed executing. However, you can return before the goal has completed by setting the `blocking` argument to False. This can be useful for preempting goals.

When the robot is in `position` mode, if you set `custom_contact_thresholds` to True, this method expects a different format dictionary: string/tuple key/value pairs, where the keys are still joint names, but the values are `(position_goal, effort_threshold)`. The addition of a effort threshold enables you to detect when a joint has made contact with something in the environment, which is useful for manipulation or safe movements. For example, `{'arm_joint': (0.5, 20)}` commands the telescoping arm fully out (the arm is nearly fully extended at 0.5 meters) but with a low enough effort threshold (20% of the arm motor's max effort) that the motor will stop when the end of arm has made contact with something. Again, in a node, this would look like:

```python
self.move_to_pose({'arm_joint': (0.5, 40)}, custom_contact_thresholds=True)
```

When the robot is in `trajectory` mode, if you set argument `duration` as `ts`, this method will ensure that the target joint positions are achieved over `ts` seconds. For example, the below would put the lift at 0.5m from its current position in `5.0` seconds:

```python
self.move_to_pose({'lift_joint': 0.5}, duration=5.0)
```

**`home_the_robot()`**

This is a convenience method to interact with the driver's [`/home_the_robot` service](/stretch4-ros2-repo/stretch_core#home_the_robot-std_srvstrigger).

**`stow_the_robot()`**

This is a convenience method to interact with the driver's [`/stow_the_robot` service](/stretch4-ros2-repo/stretch_core#stow_the_robot-std_srvstrigger).

**`stop_the_robot()`**

This is a convenience method to interact with the driver's [`/stop_the_robot` service](/stretch4-ros2-repo/stretch_core#stop_the_robot-std_srvstrigger).

**`get_tf(from_frame, to_frame)`**

Use this method to get the transform ([geometry\_msgs/TransformStamped](https://docs.ros2.org/latest/api/geometry_msgs/msg/TransformStamped.html)) between two frames. This method is blocking. For example, this method can do forward kinematics from the base\_link to the link between the gripper fingers, grasp\_center\_link, using:

```python
# launch the stretch driver launch file beforehand

import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
t = temp.get_tf('base_link', 'grasp_center_link')
print(t.transform.translation)
```

**`get_robot_floor_pose_xya(floor_frame='odom')`**

Returns the current estimated x, y position and angle of the robot on the floor. This is typically called with respect to the odom frame or the map frame. x and y are in meters and the angle is in radians.

```python
# launch the stretch driver launch file beforehand

import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
t = temp.get_robot_floor_pose_xya(floor_frame='odom')
print(t)
```

**`main(node_name, node_topic_namespace, wait_for_first_pointcloud=True)`**

When extending the `HelloNode` class, call this method at the very beginning of your `main()` method. This method handles setting up a few ROS components, including registering the node with the ROS server, creating a TF listener, creating a [FollowJointTrajectory](http://docs.ros.org/en/noetic/api/control_msgs/html/action/FollowJointTrajectory.html) client for the [`move_to_pose()`](#movetoposepose-returnbeforedonefalse-customcontactthresholdsfalse-customfullgoalfalse) method, subscribing to depth camera point cloud topic, and connecting to the quick-stop service.

Since it takes up to 30 seconds for the head camera to start streaming data, the `wait_for_first_pointcloud` argument will get the node to wait until it has seen camera data, which is helpful if your node is processing camera data.

**`quick_create(name, wait_for_first_pointcloud=False)`**

A class level method for quick testing. This allows you to avoid having to extend `HelloNode` to use it.

```python
# launch the stretch driver launch file beforehand

import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
temp.move_to_pose({'lift_joint': 0.4})
```

**Subscribed Topics**

**/camera/depth/color/points (**[**sensor\_msgs/PointCloud2**](https://docs.ros2.org/latest/api/sensor_msgs/msg/PointCloud2.html)**)**

Provides a point cloud as currently seen by the Realsense depth camera in Stretch's head. Accessible from the `self.point_cloud` attribute.

```python
# launch the stretch driver launch file beforehand

import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp', wait_for_first_pointcloud=True)
print(temp.point_cloud)
```

**/stretch/joint\_states (**[**sensor\_msgs/JointState**](https://docs.ros2.org/latest/api/sensor_msgs/msg/JointState.html)**)**

Provides the current state of robot joints that includes joint names, positions, velocities, efforts. Accessible from the `self.joint_state` attribute.

```python
print(temp.joint_state)
```

**/mode (**[**std\_msgs/String**](https://docs.ros2.org/latest/api/std_msgs/msg/String.html)**)**

Provides the mode the stretch driver is currently in. Possible values include `position`, `trajectory`, `navigation`, `homing`, `stowing`.

```python
print(temp.mode)
```

**/tool (**[**std\_msgs/String**](https://docs.ros2.org/latest/api/std_msgs/msg/String.html)**)**

Provides the end of arm tool attached to the robot.

```python
print(temp.tool)
```

**Subscribed Services**

**/stop\_the\_robot (**[**std\_srvs/Trigger**](https://docs.ros2.org/latest/api/std_srvs/srv/Trigger.html)**)**

Provides a service to quickly stop any motion currently executing on the robot.

```python
# launch the stretch driver launch file beforehand

from std_srvs.srv import TriggerRequest
import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
temp.stop_the_robot_service(TriggerRequest())
```

**/stow\_the\_robot (**[**std\_srvs/Trigger**](https://docs.ros2.org/latest/api/std_srvs/srv/Trigger.html)**)**

Provides a service to stow the robot arm.

```python
# launch the stretch driver launch file beforehand

from std_srvs.srv import TriggerRequest
import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
temp.stow_the_robot_service(TriggerRequest())
```

**/home\_the\_robot (**[**std\_srvs/Trigger**](https://docs.ros2.org/latest/api/std_srvs/srv/Trigger.html)**)**

Provides a service to home the robot joints.

```python
# launch the stretch driver launch file beforehand

from std_srvs.srv import TriggerRequest
import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
temp.home_the_robot_service(TriggerRequest())
```

**/switch\_to\_trajectory\_mode (**[**std\_srvs/Trigger**](https://docs.ros2.org/latest/api/std_srvs/srv/Trigger.html)**)**

Provides a service to quickly stop any motion currently executing on the robot.

```python
# launch the stretch driver launch file beforehand

from std_srvs.srv import TriggerRequest
import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
temp.switch_to_trajectory_mode_service(TriggerRequest())
```

**/switch\_to\_position\_mode (**[**std\_srvs/Trigger**](https://docs.ros2.org/latest/api/std_srvs/srv/Trigger.html)**)**

Provides a service to quickly stop any motion currently executing on the robot.

```python
# launch the stretch driver launch file beforehand

from std_srvs.srv import TriggerRequest
import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
temp.switch_to_position_mode_service(TriggerRequest())
```

**/switch\_to\_navigation\_mode (**[**std\_srvs/Trigger**](https://docs.ros2.org/latest/api/std_srvs/srv/Trigger.html)**)**

Provides a service to quickly stop any motion currently executing on the robot.

```python
# launch the stretch driver launch file beforehand

from std_srvs.srv import TriggerRequest
import hello_helpers.hello_misc as hm
temp = hm.HelloNode.quick_create('temp')
temp.switch_to_navigation_mode_service(TriggerRequest())
```

### License

For license information, please see the LICENSE files.


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with the Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2020-2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# Overview

*stretch\_core* provides the drivers and navigation-facing sensor processing for the Stretch mobile manipulator.

## Dual Hesai point cloud merger

The `dual_lidar_pointcloud_merger` node synchronizes the left and right Hesai `PointCloud2` streams, transforms both clouds into a target frame, preserves the original per-point fields such as `ring` and `timestamp`, and publishes one merged cloud.

It is intended for consumers that need a merged point cloud rather than a projected navigation scan. The node caches the static lidar transforms after they become available, supports an optional pre-transform voxel downsample with `merger_voxel_leaf_size`, and can blank points inside a base-centered cylinder with `cylinder_filter_radius`.

Example:

```bash
ros2 run stretch_core dual_lidar_pointcloud_merger --ros-args \
  -p left_topic:=/lidar_points_left \
  -p right_topic:=/lidar_points_right \
  -p output_topic:=/lidar_points \
  -p target_frame:=base_link
```

## Dual Hesai LaserScan filtering

The `dual_lidar_laserscan` node (`pointcloud_to_laserscan`) fuses the left and right Hesai point clouds, runs a configurable filter pipeline, and publishes `/scan_filtered` (`LaserScan`) in `base_footprint`. The scan is only published while synchronized pairs arrive from **both** lidars: if either lidar stops (or their stamps drift more than 0.2 s apart), `/scan_filtered` goes silent and the node reports the stale input on `/diagnostics`.

* `pub_pointcloud` - optional debug output: publish a filtered merged xyz cloud on `pointcloud_topic` (default `/lidar_pointcloud`, off by default)

### Filter presets

Processing is handled by `DualLidarPipeline`. The easiest way to choose behavior is with `filter_type`:

| `filter_type` | What it enables                                                 | Typical use                                         |
| ------------- | --------------------------------------------------------------- | --------------------------------------------------- |
| `region`      | Region crop + robot self-filter                                 | Mapping and general filtered scans                  |
| `sor`         | Region crop + robot self-filter + near-robot SOR                | Navigation                                          |
| `sor_ransac`  | Region crop + robot self-filter + near-robot SOR + floor RANSAC | Navigation when floor returns need explicit removal |
| `self`        | Robot self-filter only                                          | Debugging robot/self-hit removal                    |
| `none`        | No filtering before projection                                  | Baseline/debug comparison                           |
| `custom`      | Uses the individual `enable_*` booleans                         | Experiments and tuning                              |

### Available filtering techniques

Each technique is optional except the final LaserScan projection:

| Technique                 | What it does                                                                                                                                                                    | User-facing knobs                                                                                                             |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Region crop               | Removes points outside the configured height/range limits. This is the simple floor/ceiling/far-range crop.                                                                     | `z_min`, `z_max`, `range_max`                                                                                                 |
| Robot self-filter         | Removes returns from the robot itself: base cylinder, arm capsule, and required URDF-derived arm/wrist/gripper/tool boxes.                                                      | `base_radius`, `arm_filter_radius`, `self_filter_*_buffer`, `self_filter_box_*`                                               |
| Self-filter spatial gate  | Cheap pre-check before robot geometry. Points outside this XY/Z gate skip the expensive robot shape checks. It does not remove points by itself.                                | `self_filter_spatial_gate_enabled`, `self_filter_gate_radius_m`, `self_filter_gate_z_min_m`, `self_filter_gate_z_max_m`       |
| Internal voxel downsample | Near-robot voxel grid reduction used before expensive work. It is not a user-selectable `filter_type`; it runs automatically when robot self-filter or SOR is enabled.          | `dist_rob`, `leaf_size`                                                                                                       |
| SOR                       | PCL StatisticalOutlierRemoval for flying/noisy near-robot points. In the `sor` preset, voxel downsample runs first, then SOR runs inside `dist_rob`; far points pass through.   | `dist_rob`, `sor_mean_k`, `sor_stddev`                                                                                        |
| Floor RANSAC              | Fits the floor plane with RANSAC from the configured floor-detection Z band, then removes points close to that plane. It is only floor removal, not general obstacle detection. | `floor_detect_z_min`, `floor_detect_z_max`, `plane_fitting_threshold`, `angle`                                                |
| Speckle filter            | Post-projection cleanup on LaserScan bins. It removes weak isolated bins that do not have angular support from neighboring bins.                                                | `speckle_filter_enabled`, `speckle_min_points`, `speckle_neighbor_window`, `speckle_min_neighbors`, `speckle_range_tolerance` |

The runtime order is:

```
transform -> region crop -> internal voxel -> robot self-filter -> SOR -> floor RANSAC -> LaserScan projection -> speckle filter
```

Only enabled stages run. Projection always runs because it produces `/scan_filtered`. Use `region` for mapping, `sor` for navigation, and `self` with `pub_pointcloud:=true` to visualize robot/self-hit removal without the region crop.

### Launch

```bash
# Mapping-style filtering: region + self-filter
ros2 launch stretch_core dual_hesai.launch.py filter_type:=region tool_preset:=auto

# Navigation-style filtering: region + self-filter + near-robot SOR
ros2 launch stretch_core dual_hesai.launch.py filter_type:=sor tool_preset:=auto

# With RViz
ros2 launch stretch_core dual_hesai.launch.py filter_type:=sor tool_preset:=auto use_rviz:=true
```

`tool_preset` selects URDF-derived self-filter geometry for the mounted tool. Use `auto` to read Stretch robot params when available, or pass `sg4`, `pg4`, `tablet`, or `nil` explicitly.

Nav2 navigation launch (`stretch_nav2`) typically includes this stack with `filter_type:=sor` and starts `robot_footprint_publisher` for a dynamic costmap footprint.

### Robot self-filter and footprint geometry

Self-filter geometry is generated from URDF collision geometry at launch time. The generated boxes cover the arm/lift details, wrist chain, gripper camera, and the selected SG4/PG4/tablet tool geometry. Empty frames such as grasp frames and ArUco marker frames are not used as collision boxes.

The base cylinder and arm capsule are always active when the self-filter stage runs. The URDF-derived boxes are required; launch through `dual_hesai.launch.py` or `robot_footprint.launch.py` so `self_filter_config.py` can generate the temporary `self_filter_box_*` parameter YAML.

The dynamic footprint publisher receives the same generated geometry as the lidar self-filter. By default, footprint buffers match the self-filter buffers so Nav2 plans conservatively around the same arm/tool volume that lidar filtering treats as robot geometry.

### Debug self-filter geometry

Run the lidar filter and enable marker publication on the node:

```bash
ros2 launch stretch_core dual_hesai.launch.py filter_type:=self tool_preset:=auto pub_pointcloud:=true use_rviz:=true
ros2 param set /pointcloud_to_laserscan pub_self_filter_markers true
ros2 param set /pointcloud_to_laserscan publish_raw_urdf_self_filter_markers true
ros2 param set /pointcloud_to_laserscan publish_buffered_self_filter_markers true
```

RViz markers are published on `/self_filter_markers`. Raw URDF boxes show the collision geometry from the URDF. Buffered boxes show the effective lidar self-filter geometry.

### Configuration

| File                            | Purpose                                                                              |
| ------------------------------- | ------------------------------------------------------------------------------------ |
| `config/dual_lidar_filter.yaml` | `filter_type`, region limits, SOR, speckle, floor RANSAC                             |
| `config/robot_self_filter.yaml` | Shared self-filter policy: base cylinder, arm capsule, spatial gate, marker controls |
| launch-generated temp YAML      | Required URDF-derived `self_filter_box_*` geometry for the selected `tool_preset`    |
| `config/robot_footprint.yaml`   | Dynamic footprint publisher topics, base polygon, and joint update thresholds        |

Tuning notes for filter order, gate radius, URDF box buffers, and markers: see [config/README.md](/stretch4-ros2-repo/stretch_core/config).

### Head lidar PTP check

Read-only verification of JT128 return mode (Last + Strongest), point-cloud filter (Strong), PTP lock offset (350 µs), locked PTP status (single read), and jitter p95 ≤ 350 µs over 30 s (direct PTC TCP, no ROS topics):

```bash
ros2 run stretch_core stretch_lidar_check
```

Options: `--left`, `--right`, `--duration 30`, `--json`, `--verbose`.

## API

For comprehensive API documentation, please refer to [Coming soon](#TODO).

## Testing

Colcon is used to run the system/perf tests in the */test* folder. The command to run the entire suite of tests is:

```bash
$ cd ~/ament_ws
$ colcon test --packages-select stretch_core
```

You can run individual tests using the following command:

```bash
$ colcon test --packages-select stretch_core --pytest-args -k test_trajectory_server -s --event-handlers console_direct+

Test suites:
  - test_trajectory_server
  - test_pub_topics
  - test_sub_topics
  - test_services
  - test_parameters
```

## Head lidar PTC check

`stretch_lidar_check` verifies JT128 return mode (Last + Strongest), point-cloud filter (Strong), PTP lock offset (350 µs), locked PTP status, and jitter p95 over PTC TCP (port 9347):

## License

Please see the [LICENSE](/stretch4-ros2-repo/stretch_core/license) file.


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with the Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2020-2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# stretch\_core filter configuration

Shared YAML under this directory configures the dual-lidar pipeline, robot self-filter policy, and Nav2 footprint publisher. Arm, wrist, gripper-camera, and tool boxes are generated from URDF collision geometry at launch time by `launch/self_filter_config.py`.

## Filter presets and techniques

The node always publishes `LaserScan` on `output_topic` (default `/scan_filtered`). Set `pub_pointcloud: true` to also publish a debug xyz `PointCloud2` on `pointcloud_topic`.

`filter_type` selects a preset:

| `filter_type` | Enabled techniques                                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
| `region`      | Region crop, robot self-filter                                                                                      |
| `sor`         | Region crop, robot self-filter, SOR                                                                                 |
| `sor_ransac`  | Region crop, robot self-filter, SOR, floor RANSAC                                                                   |
| `self`        | Robot self-filter only                                                                                              |
| `none`        | No filters before LaserScan projection                                                                              |
| `custom`      | Controlled by `enable_self_robot_filter`, `enable_region_filter`, `enable_sor_filter`, `enable_floor_ransac_filter` |

Available techniques:

| Technique                 | What it does                                                                                                                                                                      | Main parameters                                                                     |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Region crop               | Removes points outside the configured height/range limits.                                                                                                                        | `z_min`, `z_max`, `range_max`                                                       |
| Robot self-filter         | Removes robot returns using the base cylinder, arm capsule, and required URDF-derived arm/wrist/gripper/tool boxes.                                                               | `base_radius`, `arm_filter_radius`, `self_filter_*_buffer`, `self_filter_box_*`     |
| Self-filter spatial gate  | Cheap XY/Z pre-check before exact robot geometry checks. It reduces computation; it is not a standalone removal filter.                                                           | `self_filter_gate_radius_m`, `self_filter_gate_z_min_m`, `self_filter_gate_z_max_m` |
| Internal voxel downsample | Near-robot voxel grid reduction before self-filter or SOR. This is an implementation detail, not a `filter_type`.                                                                 | `dist_rob`, `leaf_size`                                                             |
| SOR                       | PCL StatisticalOutlierRemoval for noisy/flying points near the robot. In the `sor` preset, voxel downsample runs first, then SOR runs inside `dist_rob`; far points pass through. | `dist_rob`, `sor_mean_k`, `sor_stddev`                                              |
| Floor RANSAC              | Fits the floor plane from the floor-detection Z band and removes points close to that plane. It is only floor removal, not general obstacle detection.                            | `floor_detect_z_min`, `floor_detect_z_max`, `plane_fitting_threshold`, `angle`      |
| Speckle filter            | Post-projection cleanup that removes weak isolated LaserScan bins.                                                                                                                | `speckle_*`                                                                         |

The implementation order in `dual_lidar_pipeline.cpp` is:

```
transform -> region crop -> internal voxel -> robot self-filter -> SOR -> floor RANSAC -> LaserScan projection -> speckle filter
```

Only enabled stages run. Projection always runs because it produces `/scan_filtered`. The internal voxel step runs automatically when robot self-filter or SOR is enabled because those are the expensive near-field stages.

## Spatial gate vs other radii

| Parameter                   | Default | Shape          | Purpose                                  |
| --------------------------- | ------- | -------------- | ---------------------------------------- |
| `self_filter_gate_radius_m` | 1.5 m   | Cylinder in XY | Near-field ROI for robot geometry checks |
| `base_radius`               | 0.25 m  | Cylinder in XY | Base robot volume removed as self-hit    |
| `dist_rob`                  | 2.5 m   | Square in XY   | SOR denoise ROI when `filter_type:=sor`  |

Tune `self_filter_gate_radius_m` to at least max arm+tool XY reach, usually about 1.0-1.2 m. Increase it if extended-arm returns leak into `/scan_filtered`.

Gate parameters in `robot_self_filter.yaml`:

* `self_filter_spatial_gate_enabled` - master switch
* `self_filter_gate_radius_m` - XY circle radius
* `self_filter_gate_z_min_m` / `self_filter_gate_z_max_m` - optional height band inside the cylinder

## URDF box generation

`self_filter_config.py` generates temporary `self_filter_box_*` parameters from URDF collision geometry. This replaces the old manual `self_filter_<tool>.yaml` files and the old `wrist_chain_*` parameter names.

`tool_preset:=auto` tries Stretch robot params first, then fleet/user YAML, and falls back to `sg4` if the mounted tool cannot be detected. You can also pass `sg4`, `pg4`, `tablet`, or `nil` explicitly.

Generated boxes include physical robot/tool collision links only:

| Group            | Typical links                                                         |
| ---------------- | --------------------------------------------------------------------- |
| `arm`            | `arm_l0_link` through `arm_l4_link`, `lift_link`                      |
| `wrist`          | `wrist_link`, `wrist_yaw_link`, `wrist_pitch_link`, `wrist_roll_link` |
| `gripper_camera` | `gripper_camera_link`                                                 |
| `tool`           | selected SG4, PG4, or tablet tool collision links                     |

Empty/reference frames such as grasp frames, ArUco marker frames, and attachment-site frames are not used as collision boxes.

The C++ self-filter requires generated URDF boxes. If a node is started without the generated `self_filter_box_*` parameters, configuration will fail instead of silently running with incomplete robot geometry.

## URDF box buffers

Runtime tuning is by group:

| Parameter                        | Applies to                         |
| -------------------------------- | ---------------------------------- |
| `self_filter_arm_buffer`         | `arm` URDF boxes                   |
| `self_filter_wrist_buffer`       | `wrist` URDF boxes                 |
| `self_filter_gripper_cam_buffer` | `gripper_camera` URDF boxes        |
| `self_filter_tool_buffer`        | selected SG4/PG4/tablet tool boxes |

Example:

```bash
ros2 param set /pointcloud_to_laserscan self_filter_wrist_buffer 0.06
ros2 param set /pointcloud_to_laserscan self_filter_tool_buffer 0.06
```

`self_filter_box_buffers` remains available as a full per-box override. Leave it empty for group tuning.

By default, the Nav2 footprint uses the same effective buffer as the self-filter so planning remains conservative around the arm and tool. `self_filter_box_footprint_buffers` is available as an expert override when the footprint must intentionally differ from lidar self-filter geometry.

## RViz self-filter markers

Markers are published by `pointcloud_to_laserscan` on `/self_filter_markers`. Start the lidar filter, then enable the marker parameters on that node:

```bash
ros2 launch stretch_core dual_hesai.launch.py filter_type:=self tool_preset:=auto pub_pointcloud:=true use_rviz:=true
ros2 param set /pointcloud_to_laserscan pub_self_filter_markers true
ros2 param set /pointcloud_to_laserscan publish_raw_urdf_self_filter_markers true
ros2 param set /pointcloud_to_laserscan publish_buffered_self_filter_markers true
```

| Namespace                                       | Meaning                                                                              |
| ----------------------------------------------- | ------------------------------------------------------------------------------------ |
| `self_filter/gate`                              | Spatial gate volume, which bounds expensive robot geometry checks                    |
| `self_filter/gate_ring`                         | Ground circle at gate radius for top-down RViz inspection                            |
| `self_filter/base`                              | Base cylinder from `base_radius`                                                     |
| `self_filter/arm`                               | Arm capsule from `arm_l0_link`/`lift_link` to `wrist_link`                           |
| `self_filter/urdf_raw/<group>/<collision>`      | Raw URDF collision bounding box, when `publish_raw_urdf_self_filter_markers` is true |
| `self_filter/urdf_buffered/<group>/<collision>` | Buffered filtering box used for lidar self-hit/artifact removal                      |

Useful live commands:

```bash
ros2 param list /pointcloud_to_laserscan
ros2 param set /pointcloud_to_laserscan self_filter_gate_radius_m 1.7
ros2 param set /pointcloud_to_laserscan self_filter_wrist_buffer 0.05
ros2 param set /pointcloud_to_laserscan self_filter_tool_buffer 0.05
```


# stretch\_deep\_perception


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with the Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2020-2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# stretch\_description

### Overview

The purpose of *stretch\_description* is simply to provide a launch file to visualize the URDF in Rviz. None of the files (`.xacro`, `.urdf`, `.stl`, etc.) live in this repo anymore; they have been moved to [stretch4\_urdf](https://github.com/hello-robot/stretch4_urdf).

You can launch the visualization using:

```bash
ros2 launch stretch_description display.launch.py
```

By default, the visualization uses your robot's URDF. To visualize another end-effector, batch, or variant of robot, you can run:

```bash
ros2 launch stretch_description display.launch.py urdf_file:=<path_to_urdf>
```

### Details

`display.launch.py` loads the URDF from [*stretch4\_urdf*](https://github.com/hello-robot/stretch4_urdf) using:

```python
from stretch4_urdf import get_robot_params, get_urdf

model_name, batch_name, tool_name = get_robot_params()
robot_description_content = get_urdf(model_name=model_name, batch_name=batch_name, tool_name=tool_name)
robot_state_publisher = Node(package='robot_state_publisher',
                             executable='robot_state_publisher',
                             parameters=[{'robot_description': robot_description_content}])
```

`model` and `tool` is defined for your robot in Stretch's parameters:

```
import stretch4_body.robot
r = stretch4_body.robot.Robot()
model = r.params['model_name']
tool = r.params['tool']
```

To access other URDFs, XACROs, or individual meshes from *stretch4\_urdf*, look through [Coming Soon](#TODO) for details on these assets are organized.

### License and Patents

Patents are pending that cover aspects of the Stretch mobile manipulator.

For license information, please see the LICENSE files.


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software and data for use with the Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

***

The Clear BSD License

Copyright (c) 2021-2026 Hello Robot Inc. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met:

```
 * Redistributions of source code must retain the above copyright notice,
 this list of conditions and the following disclaimer.

 * Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution.

 * Neither the name of the copyright holder nor the names of its
 contributors may be used to endorse or promote products derived from this
 software without specific prior written permission.
```

NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


# stretch\_nav2

### Overview

The *stretch\_nav2* package provides the standard ROS 2 navigation stack (Nav2) with its launch files. This package utilizes slam\_toolbox and Nav2 to drive Stretch around a mapped space. Running this code will require the robot to be untethered. We recommend stowing the arm while running navigation on the robot.

### Quickstart

The first step is to map the space that the robot will navigate in. The `offline_mapping.launch.py` will enable you to do this. First, run:

```bash
ros2 launch stretch_nav2 offline_mapping.launch.py
```

Rviz will show the robot and the map that is being constructed. With the terminal open, use the joystick (see instructions below for using a keyboard) to teleoperate the robot around. Avoid sharp or fast turns and revisit previously visited spots to form loop closures. In Rviz, once you see a map that has reconstructed the space well enough, open a new terminal and run the following commands to save the map to the `stretch_user/` directory.

```bash
mkdir ${HELLO_FLEET_PATH}/maps
ros2 run nav2_map_server map_saver_cli -f ${HELLO_FLEET_PATH}/maps/<map_name>
```

**NOTE**: The `<map_name>` does not include an extension. The map\_saver node will save two files as `<map_name>.pgm` and `<map_name>.yaml`.

**Tip**: For a quick sanity check, you can inspect the saved map using a pre-installed tool called Eye of Gnome (eog) by running the following command:

```bash
eog ${HELLO_FLEET_PATH}/maps/<map_name>.pgm
```

Next, with `<map_name>.yaml`, we can navigate the robot around the mapped space. Run:

```bash
ros2 launch stretch_nav2 navigation_mppi.launch.py  map:=${HELLO_FLEET_PATH}/maps/<map_name>.yaml
```

A new RViz window should pop up with a `Startup` button in a menu at the bottom left of the window. Press the `Startup` button to kick-start all navigation related lifecycle nodes. Rviz will show the robot in the previously mapped space, however, it's likely that the robot's location on the map does not match the robot's location in the real space. To correct this, from the top bar of Rviz, use `2D Pose Estimate` to lay an arrow down roughly where the robot is located in the real space. This gives an initial estimate of the robot's location to AMCL, the localization package. AMCL will better localize the robot once we pass the robot a `2D Nav Goal`.

In the top bar of Rviz, use `2D Nav Goal` to lay down an arrow where you'd like the robot to navigate. In the terminal, you'll see Nav2 go through the planning phases and then navigate the robot to the goal. If planning fails, the robot will begin a recovery behavior - spinning around 180 degrees in place or backing up.

**Tip**: If navigation fails or the robot becomes unresponsive to subsequent goals through RViz, you can still teleoperate the robot using the Xbox controller.

***

***

### Launch structure + Nav2 params overlay

#### Which launch files to use

Top-level launch files live directly under `launch/` — these are the ones you run:

| Launch file                               | Purpose                                                |
| ----------------------------------------- | ------------------------------------------------------ |
| `offline_mapping.launch.py`               | Build a map with SLAM                                  |
| `navigation_mppi.launch.py`               | Navigate on a saved map (main entry point)             |
| `navigation_mppi_nav2_filters.launch.py`  | Navigate with the keepout and/or speed costmap filters |
| `navigation_mppi_binary_filter.launch.py` | Navigate with binary-filter adaptive params            |
| `binary_filter_launch.py`                 | Start the binary costmap filter servers                |
| `global_plan_demo.launch.py`              | Standalone global planner demo                         |

Supporting launch files that are included by the above live under `launch/include/` (`nav_core`, `bringup`, `navigation_launch`, `nav2_filters`, `slam_toolbox`). You normally do not run these directly.

#### Launch ordering

The usual entry point is `navigation_mppi.launch.py`, which includes launch files in this order:

`navigation_mppi.launch.py` → `include/nav_core.launch.py` → `include/bringup_launch.py` → `include/navigation_launch.py`

What each file contains:

* **`navigation_mppi.launch.py`**: top-level “run navigation on the robot” launcher. Starts the Stretch driver, starts the dual-lidar filter that publishes `/scan_filtered`, then launches `include/nav_core.launch.py` with the merged Nav2 params.
* **`include/nav_core.launch.py`**: Stretch wrapper around bringup. Validates the map file, includes `include/bringup_launch.py`, and optionally launches RViz.
* **`include/bringup_launch.py`**: Nav2 bringup orchestrator. Loads/rewrites the `params_file`, then includes localization/SLAM (from `nav2_bringup`) and navigation (from `stretch_nav2`).
* **`include/navigation_launch.py`**: Nav2 navigation servers (controller/planner/BT/etc). Runs either as composed components in a container or as separate ROS nodes depending on `use_composition`.

#### Nav2 parameter overlay order (`MultiYaml`)

`navigation_mppi.launch.py` passes a `params_file` built with `MultiYaml([...])`. YAML files are merged **in order**; later files override earlier ones at nested keys.

Overlay order:

* `config/original_nav2_params.yaml`: upstream Nav2 baseline
* `config/nav2_params_core.yaml`: Stretch-specific changes (e.g., `/scan_filtered`, omni AMCL, costmap scan topics)
* `config/nav2_params_mppi.yaml`: MPPI controller selection + core controller/costmap changes
* `config/mppi_params.yaml`: MPPI tuning parameters

#### Debugging Nav2 components: `use_composition:=False`

By default, Nav2 may run as composable components inside a single container node, which makes per-component logs harder to follow. For debugging, set:

`use_composition:=False`

This runs each Nav2 component as its own ROS node so its logs are visible directly. Note: the `use_composition` launch argument is declared in `include/bringup_launch.py` / `include/navigation_launch.py` and must be passed through from the top-level launch file to take effect.

### Navigation Launch Options:

Different environments often require different navigation strategies. There’s no single setup that works best everywhere. Below are options you can try to adapt navigation performance to your environment.

**NOTE:** use can use the argument map:=/\<map\_name>.yaml with all the navigation launch commands

#### 1) Handling Noisy Laser Scans

If your LaserScan data contains a lot of noise, use the denoise layer:

```bash
ros2 launch stretch_nav2 navigation_mppi.launch.py \
  params_file:=/home/hello-robot/ament_ws/src/stretch4_ros2/stretch_nav2/config/nav2_params_mppi_denoise.yaml
```

#### 2) Preventing Unnecessary Replanning

In environments with many possible paths (e.g., highly connected spaces), Nav2 may constantly switch paths. You can avoid this by using a behavior tree (set via the `default_nav_to_pose_bt_xml` parameter) that only replans when the current path becomes invalid:

```bash
ros2 launch stretch_nav2 navigation_mppi.launch.py params_file:=/home/hello-robot/ament_ws/src/stretch4_ros2/stretch_nav2/config/nav2_params_mppi_bt.yaml
```

#### 3) Forcing the Robot to Always Face Forward

You can set the motion\_model parameter of the MPPI to diffdrive. ⚠️ Not recommended unless you specifically want the robot to always face forward. This disables omni-motion, which is usually helpful for obstacle avoidance (the robot can slide sideways without rotating making response faster).

#### 4) Using a Binary Filter for Adaptive Navigation

The **binary filter** detects when the robot **enters or exits specific areas** of the map.\
It publishes to the `/binary_state` topic, which outputs `true` or `false` only when the robot transitions into or out of a marked region (not continuously).

In our setup, the binary filter was used to **dynamically adjust navigation parameters** to improve doorway navigation:

* **Entering a doorway (narrow, cluttered area):** speed is reduced and costmap inflation is lowered.
* **Exiting the doorway:** normal speed and inflation are restored.

By lowering inflation in narrow areas, the robot could maneuver without being overly conservative. To maintain safety, speed was also reduced in these areas. In open spaces, higher inflation and normal speed were used to prevent collisions while enabling faster movement. This approach balances safety and efficiency by adjusting both inflation and speed according to the environment.

**Launching the Binary Filter**

1. Start the filter node:

```bash
ros2 launch stretch_nav2 binary_filter_launch.py
```

2. Launch navigation with the filter configuration:

```bash
ros2 launch stretch_nav2 navigation_mppi_binary_filter.launch.py
```

Alternative for running the navigation\_mppi\_binary\_filter:

```bash
ros2 launch stretch_nav2 navigation_mppi.launch.py \
  params_file:=/home/hello-robot/ament_ws/src/stretch4_ros2/stretch_nav2/config/nav2_params_mppi_binary_filter.yaml

ros2 run stretch_nav2 binary_filter_switch.py
```

⚠️ Remember: **binary\_filter\_launch.py** must always be started first.

You can verify that the filter is active by subscribing to the map topic defined under mask\_topic in binary\_filter\_param.yaml. Ensure the QoS settings match the publisher.

**How the Filter Works**

The binary\_filter\_switch.py node listens to /binary\_state and updates navigation parameters whenever the robot enters or leaves a marked area.

You can modify this node (located in the stretch\_nav2 subfolder) to adjust any parameter or trigger additional actions, such as disabling cameras in certain zones.

**How Set Up Your Own Binary Filter**

1. Annotate your map image with the areas where the filter should activate.
2. Make sure your global or local costmap includes the filter under the filters: parameter.

#### 5) Keepout and Speed Filters (`nav2_filters`)

Two Nav2 costmap filters, driven by mask images you paint over your map:

* **Keepout filter** — marks regions the planner must not route through. Applied to both the global and local costmaps.
* **Speed filter** — marks regions with a speed limit. Applied to the global costmap only.

Both live in one launch file and are selected with launch arguments, so you can run either one or both together.

**Launching**

Both filters (the default):

```bash
ros2 launch stretch_nav2 navigation_mppi_nav2_filters.launch.py \
  map:=/path/to/map.yaml \
  keepout_mask:=/path/to/keepout_mask.yaml \
  speed_mask:=/path/to/speed_mask.yaml
```

Keepout only — no `speed_mask` needed:

```bash
ros2 launch stretch_nav2 navigation_mppi_nav2_filters.launch.py \
  map:=/path/to/map.yaml \
  keepout_mask:=/path/to/keepout_mask.yaml \
  enable_speed:=false
```

Speed only — no `keepout_mask` needed:

```bash
ros2 launch stretch_nav2 navigation_mppi_nav2_filters.launch.py \
  map:=/path/to/map.yaml \
  speed_mask:=/path/to/speed_mask.yaml \
  enable_keepout:=false
```

Setting both `enable_keepout:=false` and `enable_speed:=false` starts no filter servers and skips the overlay entirely, leaving you with plain `navigation_mppi` behaviour.

**Arguments**

| Argument          | Default      | Purpose                                                                       |
| ----------------- | ------------ | ----------------------------------------------------------------------------- |
| `map`             | *(required)* | Occupancy map yaml                                                            |
| `enable_keepout`  | `true`       | Start the keepout mask/info servers and enable the plugin                     |
| `enable_speed`    | `true`       | Start the speed mask/info servers and enable the plugin                       |
| `keepout_mask`    | `''`         | Keepout mask yaml. **Required when `enable_keepout` is true**                 |
| `speed_mask`      | `''`         | Speed mask yaml. **Required when `enable_speed` is true**                     |
| `tool_preset`     | `auto`       | Mounted tool for the lidar self-filter: `auto`, `sg4`, `pg4`, `tablet`, `nil` |
| `use_rviz`        | `true`       | Start RViz                                                                    |
| `use_composition` | `True`       | Run Nav2 composed. Set `False` to debug individual nodes                      |

Enabling a filter without giving it a mask fails fast at launch:

```
RuntimeError: enable_keepout=true but keepout_mask is empty
```

Resources for \[Keepout and Speed Filters]

### <https://docs.nav2.org/tutorials/docs/navigation2\\_with\\_speed\\_filter.html> <https://docs.nav2.org/tutorials/docs/navigation2\\_with\\_keepout\\_filter.html>

### ArUco Tag-Based Localization and Calibration

The `stretch_nav2` package supports seeding the robot's initial pose using a pre-calibrated ArUco tag on the map. This is useful for instantly localizing the robot without manually estimating its pose in RViz using the `2D Pose Estimate` tool.

The services for calibrating the tag location (`/calibrate_tag_pose` with `std_srv Trigger`) and seeding localization when the robot can see the tag (`/seed_localization` with `std_srv Trigger`) are provided by the `aruco_tag_localization.py` node.

The workflow consists of two phases:

1. **Calibration**: Measure and save the static transform between the `map` and the ArUco tag (default ID: `999`, 150mm, 6x6x1000 dictionary).
2. **Localization Seeding**: Whenever the robot is unlocalized, look at the tag and trigger initial pose estimation.

#### 1. Calibration Setup & Launch

To run the calibration process, place the robot in a well-localized state on your map (using AMCL or RViz) facing your statically mounted localization ArUco tag.

**Step 1: Launch the Tag Calibration Stack**

Run the bringup launch file to start the driver, cameras, tag perception, Nav2, and the tag localization node:

```bash
ros2 launch stretch_nav2 tag_calibration_bringup.launch.py map:=${HELLO_FLEET_PATH}/maps/<map_name>.yaml
```

**Step 2: Launch the Interactive Calibration GUI**

In a separate terminal, start the interactive OpenCV calibration interface:

```bash
ros2 run stretch_nav2 calibrate_tag_cli.py
```

* Interactive Window Controls:
  * Adjust the robot's head or position until the target ArUco tag is highlighted with a green bounding box in the GUI.
  * Press c or SPACE in the GUI window, or press ENTER in your terminal to trigger the calibration.
  * Press ESC in the GUI or Ctrl+C in the terminal to exit.

Once triggered, the terminal will print a structured comparison table of the calibrated pose and automatically save it to `~/stretch_user/maps/tag_localization/<map_name>_tag_pose.yaml`.

#### 2. Seeding Robot Localization

Once your tag is calibrated and saved, you can use it to instantly localize the robot.

Power on the robot and start navigation or tag-calibration stacks:

```bash
ros2 launch stretch_nav2 tag_calibration_bringup.launch.py map:=${HELLO_FLEET_PATH}/maps/<map_name>.yaml
```

Position the robot so that its camera can see the calibrated tag. Call the seed localization service to calculate and publish the robot's initial pose to Nav2 (/initialpose):

```bash
ros2 service call /seed_localization std_srvs/srv/Trigger
```

If the robot can see the calibration tag, AMCL will automatically ingest this initial pose estimate and align the robot's position on the map.

***

### Things to Note and Design Decisions

* **Filtering Methods:** We use two filters for point cloud processing:

  1. **voxel\_sor filter** – Applies a voxel grid followed by a StatisticalOutlierRemoval within a configurable radius (e.g., \~2 m works well) to clean the point cloud before converting it into a laser scan. For navigation, `voxel_sor` improves localization by reducing outliers, but it performs poorly for mapping because the map builder expects a consistent, full environment.
  2. **region\_filter** – Removes points based on position (e.g., base, ground, ceiling).

  Mapping launch files use `region_filter`, while navigation uses `voxel_sor` (selected via the `filter_type` parameter). The reason for applying `voxel_sor` only within a certain distance is that farther points have a different distribution than closer ones so they tend to be more distant, and noise from closer points affects motion more significantly so one parameter doesn't fit close and far away points so we focus only on the closer points.

  Dual-lidar filter nodes live in `stretch_core` (`region_dual_lidar_laserscan`, `voxel_dual_lidar_laserscan`, `voxel_dual_lidar_laserscan_RANSAC`), launched via `stretch_core/launch/dual_hesai.launch.py`.
* **LaserScan Topic:** The filtered laser scan is published to `/scan_filtered` with **Best Effort QoS** (not reliable).
* **Debugging Tips:** If a topic appears inactive in RViz2:
  1. Check that `ros2 topic echo` shows messages.
  2. Verify that RViz is subscribed to the correct topic.
  3. Run `ros2 topic info /topic_name -v` to inspect publishers, subscribers, and QoS. Ensure subscriber and publisher QoS match.

***

### License

For license information, please see the LICENSE files.


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with the Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2020-2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# Stretch Python Bridge

The `stretch_python_bridge` package provides Python generators that allow simple, synchronous access to asynchronous ROS 2 topics like images, pointclouds, and TF transforms. This is particularly useful for quickly writing scripts or for integration into standard python loop structures without the need for explicitly setting up ROS2 subscriptions and callbacks for topics.

## Prerequisites

For the streams to work properly, ensure the primary robot nodes and Zenoh router are running in the background:

```bash
ros2 run rmw_zenoh_cpp rmw_zenohd
ros2 launch stretch_core stretch_driver.launch.py
ros2 launch stretch_core dual_hesai.launch.py
ros2 launch stretch_core luxonis.launch.py
ros2 launch stretch_core gripper_camera.launch.py
```

## Features

* **Generators for ROS 2 Topics**: Access the latest message from a topic directly without defining subscriptions or explicit callbacks.
* **Image Bridge**: Convert ROS 2 `sensor_msgs/Image` topis to NumPy arrays seamlessly. Yields a numpy array.
* **PointCloud2 Bridge**: Convert ROS 2 `sensor_msgs/PointCloud2` streams to structured NumPy arrays, making it easy to access fields like x, y, z, and intensity. Yields a numpy array.
* **Transforms Bridge**: Query TF2 transforms asynchronously and yield the 4x4 homogenous transformation matrix from a base frame to a target frame. Yields a 4x4 tracking numpy ndarray.
* **CameraInfo Bridge**: Convert `sensor_msgs/CameraInfo` streams to fetch identical timestamped intrinsic parameter arrays cleanly.
* **IMU Bridge**: Convert `sensor_msgs/Imu` streams to fetch identical timestamped IMU data arrays cleanly.

## Installation

This package is a standard `ament_python` package. Compile the ROS workspace with `colcon build`:

```bash
cd ~/ament_ws
colcon build --packages-select stretch_python_bridge
source install/setup.bash
```

## Typical usage

## Prepared Methods for Known Topics

To make scripts exceptionally concise, all primary robot cameras and lidars have prepared wrapper methods inside the module export that stream the payloads locally. They natively support `(timeout: float | None = 10.0, blocking: bool = True)`.

* `stream_camera_center()` → `/cameras_head/center/image_raw`
* `stream_camera_center_rotated()` → `/cameras_head/center/rotated_image`
* `stream_camera_center_info()` → `/cameras_head/center/camera_info`
* `stream_camera_left()` → `/cameras_head/left/image_raw`
* `stream_camera_left_rotated()` → `/cameras_head/left/rotated_image`
* `stream_camera_left_info()` → `/cameras_head/left/camera_info`
* `stream_camera_right()` → `/cameras_head/right/image_raw`
* `stream_camera_right_rotated()` → `/cameras_head/right/rotated_image`
* `stream_camera_right_info()` → `/cameras_head/right/camera_info`
* `stream_lidar_points_left()` → `/lidar_points_left`
* `stream_lidar_points_right()` → `/lidar_points_right`
* `stream_gripper_imu()` → `/cameras_gripper/imu/data`
* `stream_gripper_right_info()` → `/cameras_gripper/right/camera_info`
* `stream_gripper_right()` → `/cameras_gripper/right/image_raw`
* `stream_gripper_stereo_info()` → `/cameras_gripper/stereo/camera_info`
* `stream_gripper_stereo()` → `/cameras_gripper/stereo/image_raw`
* `stream_gripper_stereo_points()` → `/cameras_gripper/stereo_left_rgbd/points`

```
import cv2
from stretch_python_bridge import *

for rgb_frame in stream_camera_right_rotated():
    cv2.namedWindow("RGB Stream", cv2.WINDOW_NORMAL)
    if rgb_frame is not None:
        print(f"Got a frame with timestamp: {rgb_frame.timestamp}")
        cv2.imshow("RGB Stream", rgb_frame.image)
        cv2.waitKey(1)
```

```
import rerun as rr
import numpy as np
from stretch_python_bridge import *

rr.init("pointcloud_stream", spawn=True)
for pc_frame in stream_lidar_points_left():
    if pc_frame is not None:
        rr.log("pointcloud", rr.Points3D(pc_frame.points))
```

### Examples:

* [Lidar Pointcloud with intensity coloring](https://github.com/hello-robot/stretch4_ros2/blob/jazzy/stretch_python_bridge/examples/lidar_pointcloud.py)
* [Gripper Colored Pointcloud](https://github.com/hello-robot/stretch4_ros2/blob/jazzy/stretch_python_bridge/examples/gripper_pointcloud.py)
* [Right camera image with cv2](https://github.com/hello-robot/stretch4_ros2/blob/jazzy/stretch_python_bridge/examples/right_camera_image.py)

### 1. Typical usage using Stream Manager

If your script requires monitoring multiple ROS topics simultaneously (e.g., streaming images from 3 cameras and a TF listener at the same time), using the individual generator methods will spawn heavy background threads for each topic.

Instead, use `StreamManager` to monitor an unlimited number of topics using only a single internal execution thread.

```python
from stretch_python_bridge import StreamManager
from stretch_python_bridge import *

def main():
    # 1. Initialize the manager
    manager = StreamManager()
    
    # 2. Pass the manager to the stream generators to use the manager.
    center_stream = stream_camera_center(stream_manager=manager)
    lidar_stream = stream_lidar_points_left(stream_manager=manager)

    # You can also add as many subscriptions as you want explicitly:
    manager.add_tf_stream(target_frame="grasp_center_link", base_frame="base_link")
    manager.add_image_topic("/cameras_head/left/image_raw")
    
    print("Waiting for any data to arrive...")
    
    try:
        # 3. Use the stream as an iterable generator, this will still use the manager under-the-hood
        for frame in center_stream:
            if frame is not None:
                print(f"Got camera image at {frame.timestamp}")
                break
        
        # 4. you could also use `next()` on the generator
        camera_frame = next(center_stream)        

        # 5. Get a specific topic from the manager, with options for blocking and timeout:
        camera_frame = manager.get(center_stream, block=True, timeout=5.0)
        lidar_frame = manager.get(lidar_stream, block=True, timeout=5.0)

        # 6. Stream from the unified generator
        # If block is True, this will wait until AT LEAST ONE new frame arrives across the registered streams.
        for frames_dict in manager.stream(block=True):
            
            # frames_dict contains the absolute *latest* cached data for ALL topics
            # If a topic hasn't received any data yet, its value will be None

            # Access the topic using .get()
            left_camera_frame = frames_dict.get("/cameras_head/left/image_raw")
            if left_camera_frame is not None:
                print(f"Got left camera image at {left_camera_frame.timestamp}")
            
            # Use the returned generators to cleanly retrieve the frames
            camera_frame = next(center_gen)
            if camera_frame is not None:
                print(f"Got camera image at {camera_frame.timestamp}")
                
            # Access the TF transform
            tf_data = frames_dict.get("grasp_center_link")
            if tf_data is not None:
                x = tf_data.transform_4x4[0, 3]
                print(f"Got Grasp Center X: {x:.3f}")
                
    except KeyboardInterrupt:
        pass
    finally:
        # Important: shut down the internal threads cleanly
        manager.close()

if __name__ == '__main__':
    main()
```

## For custom topics, you can use your own stream generators

### Blocking vs Non-blocking Generators

The bridging functions are split into two variants for each data type:

* **Blocking Generators** (e.g. `image_stream_blocking`, `tf_stream_blocking`): Guarantee a non-`None` return. Calling `next()` or using them in a `for` loop will block indefinitely until a *new* frame or message arrives.
* **Non-blocking Generators** (e.g. `image_stream`): Can be configured to be completely non-blocking, or blocking with a specific timeout window. They yield `Frame | None`.

### 1. Image Stream Example

Use the `image_stream_blocking` to effortlessly fetch frames from an active image topic.

```python
import cv2
from stretch_python_bridge import image_stream_blocking

def main():
    rgb_topic = "/cameras_head/center/image_raw"
    rgb_gen = image_stream_blocking(rgb_topic)

    print(f"Subscribed to {rgb_topic}. Press CTRL+C to stop.")

    try:
        # Continuously fetch the newest image frame 
        for rgb_frame in rgb_gen:
            cv2.imshow("RGB Stream", rgb_frame.image)
            key = cv2.waitKey(1)
            if key == 27:
                break
    except KeyboardInterrupt:
        pass
    finally:
        cv2.destroyAllWindows()

if __name__ == '__main__':
    main()
```

### 2. Pointcloud Stream Example

Use the `pointcloud_stream_blocking` generator to get the most recent pointcloud as a structured Numpy array. Given the structured output, you can query specific fields using keys.

```python
from stretch_python_bridge import pointcloud_stream_blocking

def main():
    pc_topic = '/lidar_points_left'
    pc_gen = pointcloud_stream_blocking(pc_topic)

    print(f"Waiting for pointclouds on {pc_topic}...")
    
    # Grab just the very first available pointcloud frame
    try:
        first_cloud = next(pc_gen) 
        
        print("\nSuccessfully received 1 point cloud!")
        print(f"Type: {type(first_cloud.points)}")
        print(f"Shape: {first_cloud.points.shape}")
        
        # Access structured fields
        x_vals = first_cloud.points['x']
        y_vals = first_cloud.points['y']
        z_vals = first_cloud.points['z']
        intensity_vals = first_cloud.points['intensity']
        
        print(f"First point: x={x_vals[0]:.3f}, y={y_vals[0]:.3f}, z={z_vals[0]:.3f}, intensity={intensity_vals[0]:.3f}")
        
    except StopIteration:
        print("Generator stopped unexpectedly.")

if __name__ == '__main__':
    main()
```

### 3. Transforms Stream Example

Use the `tf_stream` to track the geometric relationship and 6DoF pose of arbitrary frames. The matrix yielded is a 4x4 affine homogenous transformation containing rotation and translation data.

```python
import numpy as np
from stretch_python_bridge import tf_stream

def main():
    target_frame = "grasp_center_link"
    base_frame = "base_link"

    # Creates a generator that yields 4x4 matrices 5 times a second
    pose_gen = tf_stream(target_frame, base_frame=base_frame)

    print(f"Tracking {target_frame} with respect to {base_frame}")

    try:
        for pose_frame in pose_gen:
            if pose_frame is None:
                print("Transform lookup failed this tick...")
                continue
            
            # Extract out the translation components 
            x = pose_frame.transform_4x4[0, 3]
            y = pose_frame.transform_4x4[1, 3]
            z = pose_frame.transform_4x4[2, 3]
            
            print(f"[{target_frame} pose in {base_frame}]: X={x:.3f}, Y={y:.3f}, Z={z:.3f}")

    except KeyboardInterrupt:
        pass

if __name__ == '__main__':
    main()
```

## Frame Dataclasses

All generator wrappers from this bridge yield native Python dataclasses, providing clean and strongly-typed interfaces to underlying ROS 2 messages.

Here is the structure and available bindings for each underlying datum:

### `ImageFrame`

Returned by `image_stream`, `stream_camera_center`, etc.

* `image: np.ndarray`: A standard NumPy array representing the image data. Its dimensions and encoding match the original topic config.
* `timestamp: float`: The absolute floating-point timestamp acquired from the ROS 2 message header (`sec + nanosec * 1e-9`).

### `PointCloudFrame`

Returned by `pointcloud_stream`, `stream_lidar_points_left`, etc.

* `points: np.ndarray`: A structured NumPy array output from `ros2_numpy`. Common accessible channels include `.points['x']`, `.points['y']`, `.points['z']`, and `.points['intensity']`.
* `timestamp: float`: The absolute floating-point timestamp acquired from the ROS 2 message header.

### `TransformsFrame`

Returned by `tf_stream`.

* `transform_4x4: np.ndarray`: A 4x4 homogenous affine transformation matrix containing the resolved translation and rotation from the queried base frame to target frame.
* `timestamp: float`: The absolute floating-point timestamp acquired from the ROS 2 TF buffer.

### `ImuFrame`

Returned by `imu_stream`, `stream_gripper_imu`.

* `orientation: np.ndarray`: 1D array natively ordered as `[x, y, z, w]`.
* `angular_velocity: np.ndarray`: 1D array ordered as `[x, y, z]`.
* `linear_acceleration: np.ndarray`: 1D array ordered as `[x, y, z]`.
* `timestamp: float`: The absolute floating-point timestamp acquired from the ROS 2 message header.

### `CameraInfoFrame`

Returned by `camera_info_stream`, `stream_camera_center_info`, etc.

* `camera_matrix: np.ndarray`: A 3x3 NumPy array of the intrinsic `K` parameters.
* `distortion_coefficients: np.ndarray`: A 1D NumPy array covering the `D` vector.
* `distortion_model: str`: The specific distortion model (e.g. `plumb_bob`) utilized by this camera logic.
* `timestamp: float`: The absolute floating-point timestamp acquired from the ROS 2 message header.


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with the Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2020-2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# Stretch Simulation in ROS2

Use this package to use ROS2 with Stretch in Mujoco.

## System Requirements

Stretch Simulation supports the following environments:

1. Native Ubuntu 24.04 workstation with an Nvidia GPU
2. Docker environment with GPU support

Minimum: 16GB RAM. Recommended: 32GB RAM.

> Note: This package is not supported on Metal (MacOS) at this time due to the lack of GPU acceleration and OpenGL 1.5+ support in Docker, and slow performance in UTM with a virtual machine.

## Launching

The main launch file is `stretch_mujoco_driver.launch.py` which you can invoke by running:

```shell
ros2 launch stretch_simulation stretch_mujoco_driver.launch.py
```

This launch file has many arguments that you can pass via the command-line.

### Driver Options

* `mode` (default=`position`, options: `position`, `navigation`, `trajectory`, `gamepad`) - command mode for the robot
* `use_cameras` (default=false) - Enabling the cameras uses a bit of CPU

### Scene Options

* By default, the Stretch spawns into a simple three-walled scene with objects on a table.
* By setting `use_robocasa` to True, you can spawn into a number of different [RoboCasa](https://robocasa.ai/) environments.
* You can also use your own custom scene by providing the absolute path to its XML to the `scene_xml` path. The Stretch 4 robot will be included at the origin if you have `<include file="stretch_4/stretch_4.xml"/>` in the scene xml.

### Visualization Options

* `use_mujoco_viewer` (default=true)
* `use_rviz` (default=true)

> Note: If using zenoh middleware, you should run `ros2 run rmw_zenoh_cpp rmw_zenohd` in its own terminal.

## Nav2

First go through the [Getting Started](#getting-started) guide to set up your environment.

### Mapping

This section is similar to <https://docs.hello-robot.com/0.3/ros2/navigation\\_stack/#mapping>, but uses the Stretch simulation environment.

To map the simulated environment, run the following:

```shell
# Terminal 1: Zenoh Router
ros2 run rmw_zenoh_cpp rmw_zenohd

# Terminal 2: Slam Toolbox
ros2 launch stretch_nav2 online_async_launch.py use_sim_time:=true
# Optional: Navigation bringup:
ros2 launch stretch_nav2 navigation_mppi.launch.py use_slam:=true use_sim_time:=true use_rviz:=true teleop_type:=none

# Terminal 3: Stretch Mujoco Driver
export MUJOCO_GL=egl # On Ubuntu, tell Mujoco to use the GPU
ros2 launch stretch_simulation stretch_mujoco_driver.launch.py use_mujoco_viewer:=true mode:=navigation use_robocasa:=false

# Terminal 4: Keyboard Teleop
ros2 service call /switch_to_navigation_mode std_srvs/srv/Trigger
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args --remap cmd_vel:=/stretch/cmd_vel
```

To save your map, run:

```sh
mkdir ${HELLO_FLEET_PATH}/maps
ros2 run nav2_map_server map_saver_cli -f ${HELLO_FLEET_PATH}/maps/<map_name>
```

### Navigation

To run navigation on a previously generated map, run:

```sh
# Terminal 1: Stretch Mujoco Driver
ros2 launch stretch_simulation stretch_mujoco_driver.launch.py use_mujoco_viewer:=true use_rviz:=false mode:=navigation

# Terminal 2: Navigation
ros2 service call /switch_to_navigation_mode std_srvs/srv/Trigger

ros2 launch stretch_nav2 navigation_mppi.launch.py map:=${HELLO_FLEET_PATH}/maps/<map_name>.yaml use_sim_time:=true use_rviz:=true teleop_type:=none
```

You may want to dynamically reduce the cost\_map inflation radius for most Robocasa environments:

```shell
ros2 param get /global_costmap/global_costmap inflation_layer.inflation_radius
ros2 param get /local_costmap/local_costmap  inflation_layer.inflation_radius

ros2 param set /global_costmap/global_costmap inflation_layer.inflation_radius 0.20
ros2 param set /local_costmap/local_costmap  inflation_layer.inflation_radius 0.20
```

#### Pre-mapped scene

There are [maps](https://github.com/hello-robot/stretch4_ros2/blob/jazzy/stretch_simulation/maps/README.md) included in this package that you can use with navigation out of the box.

Launch the pre-mapped environment using the following commands:

```shell
# Terminal 1
ros2 launch stretch_simulation stretch_mujoco_driver.launch.py use_mujoco_viewer:=true mode:=navigation robocasa_layout:='G-shaped' robocasa_style:=Modern_1

# Terminal 2
ros2 launch stretch_nav2 navigation_mppi.launch.py map:=~/ament_ws/src/stretch4_ros2/stretch_simulation/maps/gshaped_modern1_robocasa.yaml use_sim_time:=true use_rviz:=true teleop_type:=none

# Terminal 3
ros2 service call /stow_the_robot std_srvs/srv/Trigger
ros2 param set /global_costmap/global_costmap inflation_layer.inflation_radius 0.20
ros2 param set /local_costmap/local_costmap  inflation_layer.inflation_radius 0.20
```

## Web Teleop

You can use Stretch Web Teleop with the Stretch Simulation environment!

Before you start, install the dependencies for Stretch Web Teleop by following these [instructions](#setting-up-stretch-web-teleop).

Use the following commands to start Stretch Mujoco with Web Teleop:

```shell
parallel_terminal="gnome-terminal --tab -- /bin/bash -c " # or "xterm -e"

# Terminal 1
$parallel_terminal "MUJOCO_GL=egl ros2 launch stretch_simulation stretch_mujoco_driver.launch.py use_mujoco_viewer:=false mode:=position robocasa_layout:='G-shaped' robocasa_style:=Modern_1 use_rviz:=false use_cameras:=true map:=~/ament_ws/src/stretch4_ros2/stretch_simulation/maps/gshaped_modern1_robocasa.yaml" &

# Terminal 2
$parallel_terminal "ros2 launch stretch_simulation stretch_simulation_web_interface.launch.py" &

# Terminal 3
$parallel_terminal "cd ~/ament_ws/src/stretch4_web_teleop; npm run localstorage" &

# Terminal 4
$parallel_terminal "cd ~/ament_ws/src/stretch4_web_teleop; sudo node ./server.js" &

# Terminal 4
$parallel_terminal "cd ~/ament_ws/src/stretch4_web_teleop; node start_robot_browser.js" &
```

## Cameras and PointClouds

Please use the `use_cameras:=true` argument to enable cameras and pointclouds. e.g. `ros2 launch stretch_simulation stretch_mujoco_driver.launch.py use_mujoco_viewer:=true mode:=navigation use_cameras:=true`

There are five camera topics being published:

* RGB and Depth for the D405 camera in the gripper.
* RGB and Depth or the D435i camera in the head.
* RGB for the wide-lens camera in the head.

The RGB and Depth frames are used to create two PointCloud2 topics as well.

<img src="/files/y8otLBJmIGzQ4E0i6Fd9" alt="" width="600">

## Stretch Drivers

Simulation Drivers interface with the simulator to read and write data.

Simulation Drivers mimic the StretchDriver in `stretch_core`, which talks to the real robot.

You could display all the launch options available to the Stretch Mujoco Driver using: `ros2 launch stretch_simulation stretch_mujoco_driver.launch.py --show-args`:

```
    'broadcast_odom_tf':
        Whether to broadcast the odom TF. Valid choices are: ['True', 'False']
        (default: 'True')

    'fail_out_of_range_goal':
        Whether the motion action servers fail on out-of-range commands. Valid choices are: ['True', 'False']
        (default: 'False')

    'mode':
        The mode in which the ROS driver commands the robot. Valid choices are: ['position', 'navigation', 'trajectory', 'gamepad']
        (default: 'position')

    'use_rviz':
        One of: ['true', 'false']
        (default: 'true')

    'use_mujoco_viewer':
        One of: ['true', 'false']
        (default: 'true')

    'use_cameras':
        One of: ['true', 'false']
        (default: 'false')

    'use_robocasa':
        One of: ['true', 'false']
        (default: 'true')

    'robocasa_task':
        no description given
        (default: 'PnPCounterToCab')

    'robocasa_layout':
        One of: ['Random', 'One wall', 'One wall w/ island', 'L-shaped', 'L-shaped w/ island', 'Galley', 'U-shaped', 'U-shaped w/ island', 'G-shaped', 'G-shaped (large)', 'Wraparound']
        (default: 'Random')

    'robocasa_style':
        One of: ['Random', 'Industrial', 'Scandanavian', 'Coastal', 'Modern_1', 'Modern_2', 'Traditional_1', 'Traditional_2', 'Farmhouse', 'Rustic', 'Mediterranean', 'Transitional_1', 'Transitional_2']
        (default: 'Random')

```

You can also set the node's argument `arguments=["--ros-args", "--log-level", "debug"]` in the launch file to display Sim-to-Real time and other useful debug information.

## Getting Started

You should go through all the sections in Getting Started to run this package correctly.

> Note: If you are running on a Stretch robot, you may not need to run Stretch Simulation, unless you are trying to test out the simulation environment.

Estimated install time: `~1-2hrs`.

### Docker Install (Recommended)

If you are on Linux or Windows, you can use the [Docker setup](/stretch4-ros2-repo/stretch_simulation/readme_docker) to get started using Docker with hardware acceleration. This is not supported on MacOS due to the lack of OpenGL 1.5+ support in Docker.

### Native Install

If you would like to install Stretch Simulation directly on your host machine, please follow the [README\_SETUP](/stretch4-ros2-repo/stretch_simulation/readme_setup) instructions.


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with the Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2020-2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# Docker Setup for Stretch Simulation with Nvidia GPU Support

This guide provides instructions for setting up and running the Stretch Simulation environment in Docker with Nvidia GPU acceleration.

## Prerequisites

* Ubuntu host system (22.04 or newer recommended)
* Nvidia GPU with CUDA >=12.6 drivers installed
* Docker
* Minimum 16GB RAM (32GB recommended)

## Quick Setup

The following commands will install the Nvidia Container Toolkit, test GPU access, build the Docker image, and run the container:

1. Follow [Install Docker](#1-install-docker) and [Install Nvidia Container Toolkit](#2-install-nvidia-container-toolkit) to set up your system.
2. Run the following commands:

**Note:** The build process may take 1-2 hours depending on your internet connection and system performance.

```bash
# 1. Install Nvidia Container Toolkit
make install-nvidia-toolkit

# 2. Test GPU access
make test-gpu

# 3. Build the Docker image (takes 1-2 hours)
make build

# 4. Run the container
make run
```

After you have the container running, you can follow the [main README](/stretch4-ros2-repo/stretch_simulation) for commands to run the simulation.

If you would like to manually go through the setup steps, please follow the instructions starting at [Build the Docker Image](#3-build-the-docker-image).

## 1. Install Docker

If you don't have Docker installed, run:

```bash
# Update package index
sudo apt-get update

# Install dependencies
sudo apt-get install -y \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

# Add Docker's official GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Set up the repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Add your user to the docker group (optional, to run docker without sudo)
sudo usermod -aG docker $USER
newgrp docker
```

## 2. Install Nvidia Container Toolkit

The Nvidia Container Toolkit allows Docker containers to access your GPU for hardware acceleration.

```bash
# Configure the production repository
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

# Update package list
sudo apt-get update

# Install the Nvidia Container Toolkit
sudo apt-get install -y nvidia-container-toolkit

# Configure Docker to use Nvidia runtime
sudo nvidia-ctk runtime configure --runtime=docker

# Restart Docker daemon
sudo systemctl restart docker
```

### Verify Nvidia Container Toolkit Installation

Test that GPU access works in Docker:

```bash
docker run --rm --gpus all nvidia/cuda:12.6.1-base-ubuntu24.04 nvidia-smi
```

You should see your GPU information displayed.

## 3. Build the Docker Image

Navigate to the stretch\_simulation directory and build the image:

```bash
cd /path/to/stretch4_ros2/stretch_simulation
docker build -t stretch-simulation:latest .
```

**Note:** The build process may take 1-2 hours depending on your internet connection and system performance.

**Technical Note:** The Dockerfile sets the `DOCKER_BUILD=1` environment variable when running the workspace setup script. This enables Docker mode which:

* Bypasses sudo checks (running as root is expected in Docker)
* Skips interactive prompts for automated builds
* Uses the local script instead of downloading from the internet

The same `stretch_create_ament_workspace.sh` script works in both Docker and manual modes based on the `DOCKER_BUILD` environment variable.

## 4. Run the Docker Container

### Basic Usage

Run the container with GPU support and X11 forwarding for GUI applications:

```bash
# Allow X11 connections from Docker
xhost +local:docker

# Run the container
docker run -it --rm \
    --gpus all \
    --env="DISPLAY=$DISPLAY" \
    --env="QT_X11_NO_MITSHM=1" \
    --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
    --volume="$HOME/.Xauthority:/root/.Xauthority:rw" \
    --network=host \
    --privileged \
    stretch-simulation:latest
```

### Run with Persistent Storage

To persist maps and other data:

```bash
docker run -it --rm \
    --gpus all \
    --env="DISPLAY=$DISPLAY" \
    --env="QT_X11_NO_MITSHM=1" \
    --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
    --volume="$HOME/.Xauthority:/root/.Xauthority:rw" \
    --volume="$HOME/stretch_data:/root/stretch_user:rw" \
    --network=host \
    --privileged \
    stretch-simulation:latest
```

### Run Specific Launch Commands

Launch the Mujoco driver directly:

```bash
docker run -it --rm \
    --gpus all \
    --env="DISPLAY=$DISPLAY" \
    --env="QT_X11_NO_MITSHM=1" \
    --env="MUJOCO_GL=egl" \
    --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
    --volume="$HOME/.Xauthority:/root/.Xauthority:rw" \
    --network=host \
    --privileged \
    stretch-simulation:latest \
    ros2 launch stretch_simulation stretch_mujoco_driver.launch.py mode:=navigation use_mujoco_viewer:=true
```

## 5. Using Docker Compose (Optional)

Create a `docker-compose.yml` file for easier management:

```yaml
version: '3.8'

services:
  stretch-simulation:
    image: stretch-simulation:latest
    container_name: stretch-sim
    privileged: true
    network_mode: host
    environment:
      - DISPLAY=${DISPLAY}
      - QT_X11_NO_MITSHM=1
      - MUJOCO_GL=egl
    volumes:
      - /tmp/.X11-unix:/tmp/.X11-unix:rw
      - ${HOME}/.Xauthority:/root/.Xauthority:rw
      - ${HOME}/stretch_data:/root/stretch_user:rw
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    stdin_open: true
    tty: true
```

Run with:

```bash
xhost +local:docker
docker compose up
```

## 6. Common Usage Examples

### Navigation with Pre-mapped Scene

```bash
# Terminal 1: Launch Mujoco driver
ros2 launch stretch_simulation stretch_mujoco_driver.launch.py \
    use_mujoco_viewer:=true \
    mode:=navigation \
    robocasa_layout:='G-shaped' \
    robocasa_style:=Modern_1

# Terminal 2: Launch navigation
ros2 launch stretch_nav2 navigation_mppi.launch.py \
    map:=/root/ament_ws/src/stretch4_ros2/stretch_simulation/maps/gshaped_modern1_robocasa.yaml \
    use_sim_time:=true \
    use_rviz:=true \
    teleop_type:=none

# Terminal 3: Configure and stow
ros2 service call /stow_the_robot std_srvs/srv/Trigger
ros2 param set /global_costmap/global_costmap inflation_layer.inflation_radius 0.20
ros2 param set /local_costmap/local_costmap inflation_layer.inflation_radius 0.20
```

### Enable Cameras and PointClouds

```bash
ros2 launch stretch_simulation stretch_mujoco_driver.launch.py \
    use_mujoco_viewer:=true \
    mode:=navigation \
    use_cameras:=true
```

## 7. Troubleshooting

### GPU Not Detected

If the container can't access the GPU:

```bash
# Check Nvidia driver installation
nvidia-smi

# Verify Docker can see the GPU
docker run --rm --gpus all nvidia/cuda:12.6.1-base-ubuntu24.04 nvidia-smi

# Check Nvidia Container Toolkit configuration
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
```

### X11 Display Issues

If GUI applications don't appear:

```bash
# Allow X11 connections
xhost +local:docker

# Check DISPLAY variable
echo $DISPLAY

# Try running with --net=host
docker run -it --rm --gpus all --net=host -e DISPLAY=$DISPLAY ...
```

### OpenGL/EGL Issues

If you encounter OpenGL errors:

```bash
# Make sure MUJOCO_GL is set to egl
docker run -it --rm --gpus all -e MUJOCO_GL=egl ...
```

## 8. Performance Tips

* Use `--shm-size=2g` or higher if you encounter shared memory issues
* For better performance, use `--ipc=host`
* Consider using `--cpuset-cpus` to dedicate specific CPU cores
* Monitor GPU usage with `nvidia-smi` while running simulations

## 9. Cleaning Up

Remove containers and images:

```bash
# Remove stopped containers
docker container prune

# Remove unused images
docker image prune

# Remove the stretch-simulation image
docker rmi stretch-simulation:latest
```

## Additional Resources

* [Stretch ROS2 Documentation](https://docs.hello-robot.com/0.3/ros2/)
* [Nvidia Container Toolkit Documentation](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html)
* [Docker Documentation](https://docs.docker.com/)


# Setting up Stretch Simulation

You should go through all the sections in this setup guide to run this package correctly.

> NOTE: If you are running on a Stretch robot, you can skip to [Setting up Mujoco](#setting-up-mujoco-15-minutes)

> NOTE: If you are on Linux or Windows, you can use the [Docker setup](/stretch4-ros2-repo/stretch_simulation/readme_docker) to get started using Docker with hardware acceleration.

Estimated install time: `~1-2hrs`.

## Prerequisites

* Ubuntu 24.04 host system
* Nvidia GPU with CUDA >=12.6 drivers installed
* Minimum 16GB RAM (32GB recommended)

## Install ROS2 Jazzy (10 minutes)

> NOTE: Please do not run this step if you are running on a Stretch robot.

The commands below are taken from this guide: <https://docs.ros.org/en/jazzy/index.html>

```shell
sudo apt install software-properties-common
sudo add-apt-repository universe

sudo apt update && sudo apt install curl -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

sudo apt update

sudo apt install ros-jazzy-desktop ros-jazzy-rmw-zenoh-cpp ros-dev-tools python3-pip

source /opt/ros/jazzy/setup.bash
```

## Setting up `ament_ws` (1 hour)

> NOTE: Please do not run this step if you are running on a Stretch robot.

If you are not running this package on a robot NUC (which is *not* [recommended](#system-requirements)), you will need to set up a ROS2 environment similar to the environment that ships with Stretch.

Please run these commands to install the environment. This will delete the existing `~/ament_ws` directory, so please proceed with caution.

First you should install `NodeJS>=21.x` and `npm` if you don't already have them:

```shell
curl -sL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
```

```sh

curl -sL https://raw.githubusercontent.com/hello-robot/stretch4_ros2/refs/heads/jazzy/stretch_simulation/stretch_ros2_jazzy.repos > /tmp/stretch_ros2_jazzy.repos
curl -sL https://raw.githubusercontent.com/hello-robot/stretch4_ros2/refs/heads/jazzy/stretch_simulation/stretch_create_ament_workspace.sh > /tmp/stretch_create_ament_workspace.sh
bash /tmp/stretch_create_ament_workspace.sh


# Optional: add source install/setup.bash to .bashrc:
echo 'source ~/ament_ws/install/setup.bash' >> ~/.bashrc
```

> Note: If you run into a colcon build error: "fatal error: numpy/ndarrayobject.h: No such file or directory", run `sudo ln -s ~/.local/lib/python3.10/site-packages/numpy/core/ /usr/include/numpy` to resolve it.

A successful `ament_ws` setup will look like this:

```
$ ls ~/ament_ws/src
airy_lidar_filter_cpp  depthai-core  depthai-ros  ros2_numpy   stretch4_ros2  stretch4_web_teleop
```

## Setting up URDF (15 minutes)

Run the commands below or follow the instruction in the [`stretch_description #updating-the-urdf`](/stretch4-ros2-repo/stretch_description#updating-the-urdf) README file to set up the URDF meshes.

```shell
source ~/ament_ws/install/setup.bash

git clone https://github.com/hello-robot/stretch4_urdf.git --depth 1 /tmp/stretch4_urdf

cd /tmp/stretch4_urdf
pip install -e .
```

A successful URDF update will look like this:

```
$ ls ~/ament_ws/src/stretch4_ros2/stretch_description/urdf/
d405                             stretch_aruco.xacro     stretch_description_SE3_eoa_wrist_dw3_tool_nil.xacro          stretch_head_nav_cam.xacro        stretch_tool_sg3.xacro
d435i                            stretch_base_imu.xacro  stretch_description_SE3_eoa_wrist_dw3_tool_sg3.xacro          stretch_laser_range_finder.xacro  stretch_tool_tablet_12in.xacro
export_urdf_license_template.md  stretch_d405_sg3.xacro  stretch_description_SE3_eoa_wrist_dw3_tool_tablet_12in.xacro  stretch_main.xacro                stretch_uncalibrated.urdf
export_urdf.sh                   stretch_d435i.xacro     stretch_description.xacro                                     stretch_respeaker.xacro           stretch_wrist_dw3.xacro
```

## Setting up Mujoco (15 minutes)

This ROS 2 package includes nodes and launch files that use the [`stretch4_mujoco`](https://github.com/hello-robot/stretch4_mujoco) repo to interface with Mujoco.

Run the following, after having done the previous ament\_ws setup steps, to start interacting with Stretch in Mujoco using ROS 2:

```shell
pip3 install --upgrade pip #This is important after a fresh install of Ubuntu, for edittable installation of dependencies

source ~/ament_ws/install/setup.bash

# Set-up script to install mujoco and dependencies:
# Note: if you get an externally managed environment error, first run:export PIP_BREAK_SYSTEM_PACKAGES=1
bash ~/ament_ws/src/stretch4_ros2/stretch_simulation/stretch_mujoco_driver/setup.bash

pip install pyquaternion PyOpenGL==3.1.4 # Fixes AttributeError: module 'OpenGL.EGL' has no attribute 'EGLDeviceEXT'

cd ~/ament_ws
source ./install/setup.bash
colcon build
ros2 launch stretch_simulation stretch_mujoco_driver.launch.py mode:=navigation
```

## Setting up Stretch Web Teleop

Make sure you've already completed everything under [Setting up `ament_ws`](#setting-up-ament_ws) above.

Run the following commands to get IK for the gripper working:

```shell
cd stretch_description/urdf
cp ./stretch_uncalibrated.urdf stretch.urdf

sudo apt install rpl
./export_urdf.sh # It's okay if it fails on calibrated params

mkdir -p $HELLO_FLEET_PATH/$HELLO_FLEET_ID/exported_urdf
cp -r ./exported_urdf/* $HELLO_FLEET_PATH/$HELLO_FLEET_ID/exported_urdf
```


# Introduction

The `stretch4_body` repository contains the core Python software stack that allows developers to interact with the hardware of Stretch 4 robots. The repository for Stretch 3 and below can be found in the [stretch\_body](https://github.com/hello-robot/stretch_body) repo. This repo provides a robust, soft real-time capable framework for managing low-level motor communication, subsystem coordination, autonomous behaviors, and a high-level API for user applications. This repository is intended to be imported by other code that needs access to these features.

This package can be installed by:

```
python3 -m pip install -U hello-robot-stretch4-body
```

## Architecture Block Diagram

At its heart, the architecture is built around a Client-Server model. A dedicated `RobotServer` runs as a background daemon managing the physical hardware at 100Hz, executing safety monitoring, self-collision detection, and hardware command multiplexing. Developers build their applications using the `RobotClient`, which asynchronously communicates with the server over ZeroMQ. This decouples user scripts from strict hardware timing constraints and allows for safe, concurrent control of the robot.

```mermaid
graph TD
    ClientCode["User Application RobotClient"]
    Server["Robot Server 100Hz Loop"]

    Subsystems["Hardware Subsystems"]
    Arm
    Lift
    Omnibase
    PowerPeriph
    EndOfArm

    Behaviors["Behaviors"]
    Sentries["Sentries Safety Monitors"]
    SafeMotions["Safe Motions Collision Avoidance"]
    Routines["Routines Autonomous Actions"]

    Workers["Background Workers"]
    LineSensorLoop["Line Sensor Loop"]
    CollisionLoop["Self Collision Loop"]
    EOALoop["End Of Arm Loop"]

    ClientCode -->|ZeroMQ Commands and Status| Server

    Server --> Behaviors
    Behaviors --> Sentries
    Behaviors --> SafeMotions
    Behaviors --> Routines

    Server --> Subsystems
    Subsystems --> Arm
    Subsystems --> Lift
    Subsystems --> Omnibase
    Subsystems --> PowerPeriph
    Subsystems --> EndOfArm

    Server --> Workers
    Workers --> LineSensorLoop
    Workers --> CollisionLoop
    Workers --> EOALoop
```

## Technical Primers

For an in-depth understanding of how specific parts of the system are designed, refer to the following technical primers:

| Primer                                                                               | Description                                                                                                 |
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| [Core Architecture](/stretch4-body-repo/core-framework/primer_core)                  | Maps out the foundational classes, IPC communication, and file organization of the core library.            |
| [Robot Parameters](/stretch4-body-repo/core-framework/primer_robot_params)           | Explains the multi-layered parameter system (default vs user) and dynamic runtime generation.               |
| [Robot Client API](/stretch4-body-repo/core-framework/primer_robot_client)           | A guide to using the RobotClient API for reading status and commanding motion asynchronously.               |
| [Hardware Subsystems](/stretch4-body-repo/subsystems-and-hardware/primer_subsystems) | Overview of the primary hardware abstractions (Arm, Lift, Base) and how they are instantiated.              |
| [End-Of-Arm EOA](/stretch4-body-repo/subsystems-and-hardware/primer_end_of_arm)      | Details the dynamically instantiated, multi-process architecture for interchangeable tool attachments.      |
| [Line Sensors](/stretch4-body-repo/subsystems-and-hardware/primer_line_sensor)       | Details the operation and background processing for the downward-facing Pixart line sensors.                |
| [Server Behaviors](/stretch4-body-repo/behaviors-and-safety/primer_behaviors)        | Explains the plugin architecture for Sentries, Safe Motions, and Routines within the 100Hz server loop.     |
| [Self-Collision](/stretch4-body-repo/behaviors-and-safety/primer_self_collision)     | Details the MuJoCo-based collision checking system, its background loop, and configuration parameters.      |
| [Gamepad Teleop](/stretch4-body-repo/teleoperation/primer_gamepad_teleop)            | Explains how different control schemes can be mapped onto a standard gamepad controller + how to extend it. |
| [Cameras](/stretch4-body-repo/subsystems-and-hardware/primer_cameras)                | A guide to the cameras on Stretch 4's head and wrist, with an overview of the CLIs and API.                 |

## Installation

1. `pip3 install -e .`
2. `stretch_body_server --launch`

*Note: The C++ shared libraries for `transport` and `SCSerial` will compile automatically via Meson during the `pip install`.*

If you want to install the object detection dependencies:

```bash
pip3 install -e .[object_detection]
```

### Troubleshooting Editable Installs

If you make a C++ syntax error or typo in the source files and attempt to run a command while in editable mode (e.g., launching `stretch_body_server`), you may encounter an obscure Python exception instead of the actual C++ compiler error message:

```
subprocess.CalledProcessError: Command '['ninja']' returned non-zero exit status 1.
```

Because `meson-python` editable builds run quietly in the background on import, it drops the standard output of the C++ compiler natively, hiding your C++ syntax error. To see the actual compiler output and locate the line where C++ failed, prepend your command with the verbose flag:

```bash
MESONPY_EDITABLE_VERBOSE=1 stretch_body_server --launch
```

## Custom User End-of-Arm Tools

Stretch 4 supports dynamic user-defined custom end-of-arm tools. Users can define, process, register, and switch to their own tools without modifying the core software stack.

### 1. Directory Structure

Custom tools should be placed in your fleet's `user_tools` directory:

* If environment variables `HELLO_FLEET_PATH` and `HELLO_FLEET_ID` are set: `<HELLO_FLEET_PATH>/<HELLO_FLEET_ID>/user_tools/`
* Otherwise (fallback): `~/stretch_user/user_tools/`

Create a subdirectory named after your tool (e.g., `user_eoa_mytool`):

```yaml
> user_eoa_mytool
    > meshes
        my_tool_mesh.stl      # Visual/Collision mesh files
    user_eoa_mytool.urdf      # Tool URDF file describing joints & links
    user_eoa_mytool.py        # Optional custom Python driver class
```

If your tool has custom driver code, the main Python file must match the tool directory name (e.g., `user_eoa_mytool.py`) or be named `tool.py`, and contain a class matching the tool name in PascalCase (e.g., `class UserEoaMytool`).

### 2. Mesh Preprocessing and Registration

Once your files are in place, process the tool using the automatic registration utility. This script simplifies visual meshes, generates collision meshes, and appends the default baseline configuration (including serial devices, joint exclusion, and collision management) to `stretch_user_params.yaml`:

```bash
stretch_configure_tool --add_user_tool
```

The tool will prompt you to select your custom tool subdirectory, process its URDF/meshes, and generate the parameters.

### 3. Switching to Your Tool

To switch your robot to use the custom tool:

```bash
stretch_configure_tool --quick --tool user_eoa_mytool
```

This updates `stretch_user_params.yaml` to make `user_eoa_mytool` the active tool. The `RobotClient`, `stretch_status`, and `stretch_system_check` utilities will automatically recognize, load, and poll your custom tool.


# Core Architecture

The `stretch4_body/core` directory contains the foundational base classes, utilities, and communication protocols that power the `stretch4_body` software stack.

These classes are typically **not used directly by end-user developers**. Instead, they provide the internal scaffolding that the higher-level robot subsystems (like `OmniBase`, `Arm`, `Lift`, etc.) build upon to interact with the hardware and network.

## Organizational Structure

The contents of the `core` directory can be grouped into six main functional areas:

1. Device and Hardware Abstractions
2. Teleoperation and Gamepad Control
3. Client/Server and IPC (Inter-Process Communication)
4. Diagnostics, Visualization, and Tracing
5. General Utilities and Configuration
6. Subdirectories (`factory`, `feetech`, `transport`)

***

### 1. Device and Hardware Abstractions

These files define the base Python representations of the physical robot hardware.

* **`device.py`**: Defines the foundational `Device` base class inherited by all hardware subsystems. It manages basic lifecycle execution (startup, stop), parses configuration dictionaries, and handles logging initialization.
* **`prismatic_joint.py`**: An abstraction layer for linear joints, inherited specifically by the `Arm` and `Lift` subsystems. It handles the mathematical conversions from motor rotation to linear translation, manages soft-limits, and coordinates homing procedures.
* **`stepper.py`**: The API interface for the custom Hello Robot stepper motor controllers (used by the mobile base wheels, arm, and lift). It manages the serial command protocols for position/velocity trajectory control, guarded motion (collision detection), and motor telemetry reporting.
* **`worker_loop.py`**: A utility wrapper for instantiating high-frequency, non-blocking background multiprocessing workers (used extensively to isolate I/O tasks in the `end_of_arm` and `line_sensor` subsystems).

### 2. Teleoperation and Gamepad Control

This module handles interpreting user input from USB/Bluetooth gamepads (like Xbox controllers) into smooth robotic motion.

* **`gamepad_teleop.py` & `gamepad_controller.py`**: The primary drivers and classes for reading asynchronous events from the controller and smoothing them into velocity or position commands for the robot's joints.
* **`gamepad_control_mappings.py` & `gamepad_enums.py` & `gamepad_joints.py`**: Define the specific button mappings, state machines, and joint-specific kinematics required to drive the robot intuitively.

### 3. Client/Server and IPC

These files manage the multi-process architecture of Stretch Body, allowing multiple user scripts to communicate with the robot hardware simultaneously safely.

* **`client_server.py`**: Contains the `StretchBodyServer` and `StretchBodyClient` classes. The server uses ZeroMQ (ZMQ) to publish high-frequency robot status, subscribe to incoming commands, and multiplex access by managing client priorities and leases (preventing command collisions).
* **`subsystem_client.py`**: Provides the base ZMQ client used by isolated worker processes to push commands and pull status data to/from the main server loop.

### 4. Diagnostics, Visualization, and Tracing

Tools for ensuring the robot operates safely and providing developers with visual debugging aids.

* **`robot_monitor.py`**: A background thread that continually checks for hardware faults—such as low battery state-of-charge, runstop events, or over-tilting—and can autonomously trigger safety behaviors.
* **`robot_trace.py` & `scope.py`**: Utilities for capturing, logging, and visualizing high-frequency telemetry. `scope.py` uses Matplotlib to generate oscilloscope-like plots for tuning motor control loops.
* **`rerun_plot.py` & `rerun_dynamic_plotter.py`**: Integrations with the Rerun SDK for advanced, real-time 3D visual telemetry and state debugging.
* **`mujoco_urdf.py`**: Uses the MuJoCo physics engine to load the robot's URDF, facilitating dynamic self-collision detection and collision visualization.

### 5. General Utilities and Configuration

* **`robot_params.py`**: The central configuration manager. It loads, merges, and resolves the robot's layered YAML configuration files (`stretch_factory_params`, `stretch_configuration_params`, and `stretch_user_yaml`), providing the unified dictionary used by every subsystem.
* **`hello_utils.py`**: A collection of miscellaneous, widely-used helpers for thread management, POSIX file locking, timestamping, and parsing configuration paths.

***

### 6. Subdirectories

The `core` folder also contains three deeply integrated subdirectories handling low-level communication and manufacturing tooling:

* **`factory/`**: A suite of low-level utility classes and scripts strictly intended for manufacturing, hardware testing, and firmware flashing. Developers should rarely need to interact with these unless debugging deep hardware issues.
* **`feetech/`**: Communication wrappers and SDK implementations for interfacing with the Feetech smart serial servos. These are used exclusively to actuate the `end_of_arm` tools (like the dexterous wrist and gripper).
* **`transport/`**: Contains the critical Python and C++ extensions that manage the raw, high-speed serial packet communication to the custom motor controllers and microcontrollers distributed across the robot.


# Robot Client API

The `stretch4_body/robot/robot_client.py` file defines the `RobotClient`, which serves as the **user-facing Python API** for interacting with the Stretch 4 robot. Whether you are writing a simple script to move an arm or developing a complex  autonomous behavior, `RobotClient` is the entry point.

The `RobotClient`, in your python script, talks to the `RobotServer` (started with the `stretch_body_server --daemon` command in a terminal on the robot) using [ZMQ](https://zeromq.org/). This is a **many-to-one relationship** with leases and priorities; you can have multiple RobotClient instances running at the same time, but the first one to connect to the server will hold the lease until it stops sending commands.

> Note: `stretch_gamepad_teleop` also uses `RobotClient` and has a higher priority than the default priorty setting for client instances. Commands from gamepad teleop will override commands from any other script.

## 1. Typical Usage of the `RobotClient`

The typical usage involves instantiating the `RobotClient`, calling `startup()` to establish connections to the robot server, executing your behavior, and finally calling `stop()` to cleanly close the connections:

```python
from stretch4_body.robot.robot_client import RobotClient

robot = RobotClient()

if not robot.startup():
    raise Exception("Could not start the robot client.")

if not robot.is_homed():
    if input('Home Stretch? Note: Joints will move! [y/n]') == 'y':
        robot.home() # Warning! This will move the robot joints!
        
print("Stretch 4 is ready!")

robot.stop()
```

Alternatively, you can use it as a context manager to handle the cleanup automatically:

```python
import time
from stretch4_body.robot.robot_client import RobotClient

# Using a context manager ensures stop() is called automatically
with RobotClient() as robot:
    # Check if the robot needs to be homed
    if not robot.is_homed():
    if input('Home Stretch? Note: Joints will move! [y/n]') == 'y':
        robot.home() # Warning! This will move the robot joints!
        
    print("Stretch 4 is ready!")
```

## 2. Accessing Subsystems

The `RobotClient` aggregates all of the robot's hardware components into individual **subsystem clients**.&#x20;

The primary subsystems relate directly to the `RobotClient` as attributes. For example, `robot.arm`, `robot.lift`, `robot.omnibase`, and `robot.end_of_arm`.

### Example: Commanding Subsystems

```python
with RobotClient() as robot:
    # Command the arm to extend to 0.3 meters
    robot.arm.move_to(0.3)
    
    # Command the lift to move up by 0.1 meters
    robot.lift.move_by(0.1)
```

> Note: If you command a joint to move, but the joint is not homed, **it will not move.** The subsystem command will return `False` and print a warning that the joint is not homed. To home the robot, run `robot.home()` in your code, or use the cli command `stretch_robot_home` in a terminal.

## 3. The User Control Loop Design Pattern

Stretch is designed around Client and Server control loops. The Client relies on two primary functions: `pull_status()` and `push_command()`to communicate with the Server.

1. **`pull_status()`**: Fetches the latest sensor data, joint positions, and state from the robot server and updates the `robot.status` dictionary.&#x20;
2. **`push_command()`**: Takes all commands queued up in the various subsystems and flushes them to the robot hardware simultaneously.

### Deep Copy

Note that the dictionary returned by `pull_status()` is **deep-copied**. A reference to a key in an older `pull_status()` will retain the value of the old dictionary and will not be updated by a subsequent `pull_status()`.

Therefore, it it important to pull\_status() before acting on the status dictionary:

```python
with RobotClient() as robot:
    while True:
        # 1. Always pull_status inside the loop before accessing it
        robot.pull_status()
        # 2. Access the status dictionary
        print(f"{robot.arm.status['pos']=}")
        
        time.sleep(1/15) # 15Hz loop
```

### Rate Expectations

A typical user control loop runs between **10 Hz and 50 Hz**. Running faster than the Server's control loop (100 Hz) is unnecessary.

### Example: Control Loop

```python
with RobotClient() as robot:
    rate_hz = 20.0
    dt = 1.0 / rate_hz
    
    while True:
        # 1. Update the status dictionary with the latest hardware state
        robot.pull_status()
        
        # 2. Read the current position of the lift
        current_lift_pos = robot.status['lift']['pos']
        
        # 3. Calculate a new position based on some logic (e.g., following a target)
        target_lift_pos = current_lift_pos + 0.01 
        
        # 4. Queue the command (does not move the robot yet)
        robot.lift.move_to(target_lift_pos)
        
        # 5. Flush all queued commands to the hardware simultaneously
        robot.push_command()
        
        # 6. Sleep to maintain the loop rate
        time.sleep(dt)
```

## 4. The Status Dictionary Structure

When you call `robot.pull_status()`, the `RobotClient` populates a master dictionary accessible via `robot.status`. This dictionary acts as a snapshot of the robot's state at that exact moment.

The `robot.status` dictionary is organized by subsystem. Each subsystem provides its own set of keys reflecting its physical state.

### Common Subsystem Status Keys

For prismatic or revolute joints like the `arm` or `lift`, the status dictionary generally includes:

* **`pos`**: The current position (meters or radians).
* **`vel`**: The current velocity (m/s or rad/s).
* **`effort`**: The measured effort/torque.

For the `omnibase`, you will typically find odometry information such as `x`, `y`, and `theta`. For the `power_periph` (power system and IMU), you'll find system health data like `voltage` and `current`.

### Example: Working with Status Dictionaries

```python
with RobotClient() as robot:
    # Always pull the latest status before reading!
    robot.pull_status()
    
    # --- Reading Arm Status ---
    arm_pos = robot.status['arm']['pos']
    arm_effort = robot.status['arm']['effort']
    print(f"Arm Position: {arm_pos:.3f} m, Effort: {arm_effort:.2f}")
    
    # --- Reading End-Of-Arm (EOA) Status ---
    # The EOA structure depends on the tool configured (e.g., a gripper)
    if 'stretch_gripper' in robot.status['end_of_arm']:
        gripper_pos = robot.status['end_of_arm']['stretch_gripper']['pos']
        print(f"Gripper Position: {gripper_pos:.2f} rad")
        
    # --- Reading Power Status ---
    battery_v = robot.status['power_periph']['voltage']
    print(f"Battery Voltage: {battery_v:.2f} V")
```

### Direct Subsystem Status Access

You can also access a subsystem's status directly via the subsystem object, which points to the exact same dictionary:

```python
# These two lines return the identical value
pos_1 = robot.status['lift']['pos']
pos_2 = robot.lift.status['pos']
```

## 5. Blocking vs. Non-Blocking Calls

A crucial concept in Stretch's API is understanding when a command blocks the execution of your Python script and when it does not.

### Non-Blocking Calls (Asynchronous)

By default, subsystem commands like `move_to()`, `move_by()`, and `set_velocity()` simply queue the intent. When you call `robot.push_command()`, the command is sent to the server, and your script continues executing immediately. **The robot will move in the background.** This is essential for control loops (like the one shown above) where you need to continuously read sensors while the robot is moving.

### Blocking Calls (Synchronous)

Sometimes you want your script to wait until a motion is physically finished before executing the next line of code. You can achieve this by explicitly waiting for the motion to finish.

High-level routines, like `robot.home()` or `robot.stow()`, are typically blocking by default.

### Example: Waiting for Motion

```python
with RobotClient() as robot:
    # Command the arm to move (Non-blocking)
    robot.arm.move_to(0.5)
    robot.push_command()
    
    # Wait until the arm has finished its trajectory (Blocking)
    robot.wait_on_motion_finish(['arm'])
    
    print("Arm has reached its destination. Moving the lift.")
    
    # Now command the lift
    robot.lift.move_to(0.8)
    robot.push_command()
    robot.wait_on_motion_finish(['lift'])
```

## 6. Remote Connection Clients

The `RobotClient` sits on top of a network communication layer (ZMQ), and doesn't have to be running on the robot's physical computer. You can run your Python scripts from your laptop to control the robot remotely over WiFi or a tunnel.

To do this, simply instantiate the `RobotClient` with the robot's IP address:

```python
# Connect to a Stretch robot over the local network
REMOTE_IP = "192.168.1.105"

with RobotClient(ip_address=REMOTE_IP) as robot:
    robot.pull_status()
    print("Successfully connected to the remote robot!")
    print(f"Current battery voltage: {robot.status['power_periph']['voltage']}")
```

### Multiple User Access

If multiple Ubuntu User Accounts are logged in to the robot at the same time (e.g. using [RDP or remote access](https://docs.hello-robot.com/stretch4_docs/working-with-stretch/general_use/connecting-to-stretch?q=user#untethered_setup)), there might only be **one instance** of the Server (`stretch_body_server --daemon`) running at a time.

By default, inter-user access to the server is disabled to avoid versioning conflicts. However, if you would still like to connect to another user's Server, you can pass the `allow_different_user_connection=True` parameter during startup:

```python
robot = RobotClient()
robot.startup(allow_different_user_connection=True)
robot.pull_status()
```

Note that if you try to connect to another user's server without the `allow_different_user_connection` parameter, you will get this error:

```
StretchBodyClient: A server is already running, but it was started by a different user (Username).
StretchBodyClient: You can run `stretch_body_server --kill` to forcefully end the other user's session.
```

## 7. Architecture Visualization

The following diagram illustrates the hierarchy and data flow from your user application down to the physical hardware.

```mermaid
graph TD
    USER_CODE["User Application Code<br/>(Your Script)"]:::user
    
    RC["RobotClient<br/>(Aggregator)"]:::client
    
    subgraph Subsystems
        ARM["ArmClient"]:::sub
        LIFT["LiftClient"]:::sub
        BASE["OmniBaseClient"]:::sub
        EOA["EndOfArmClient"]:::sub
        ROUTINES["RoutinesClient"]:::sub
    end
    
    COMM_LAYER["Communication Layer<br/>(SubsystemClient / RPC)"]:::comm
    ROBOT_SERVER["Robot Server<br/>(Runs on Robot Hardware)"]:::server
    
    %% API Interactions
    USER_CODE -->|"Calls pull_status() / push_command()"| RC
    USER_CODE -->|"Calls move_to(), etc."| ARM
    USER_CODE -->|Calls routines| ROUTINES
    
    %% Aggregation
    RC --> ARM
    RC --> LIFT
    RC --> BASE
    RC --> EOA
    RC --> ROUTINES
    
    %% Communication
    ARM --> COMM_LAYER
    LIFT --> COMM_LAYER
    BASE --> COMM_LAYER
    EOA --> COMM_LAYER
    ROUTINES --> COMM_LAYER
    RC --> COMM_LAYER
    
    %% Network Boundary
    COMM_LAYER <-->|ZeroMQ / TCP / IPC| ROBOT_SERVER
    
    classDef user fill:#e88d3e,stroke:#333,stroke-width:2px,color:#fff;
    classDef client fill:#57a661,stroke:#333,stroke-width:2px,color:#fff;
    classDef sub fill:#4f81c7,stroke:#333,stroke-width:2px,color:#fff;
    classDef comm fill:#a859b3,stroke:#333,stroke-width:2px,color:#fff;
    classDef server fill:#d9534f,stroke:#333,stroke-width:2px,color:#fff;
```

## RobotClient Best Practices

When writing code to control the Stretch robot via `RobotClient`:

1. **Always Pull Before Reading:** The status dictionaries are not magically updated in the background. You MUST call `robot.pull_status()` at the start of your loop before reading `robot.status` or `robot.subsystem.status`.
2. **Commands are Queued:** Calling `robot.arm.move_to()` simply queues the command locally. It does nothing until you call `robot.push_command()`.
3. **Execution is Asynchronous:** `robot.push_command()` returns immediately. If you need to wait for a motion to finish before executing the next step (e.g. a simple sequence script), you must use `robot.wait_on_motion_finish(['subsystem_name'])`.
4. **End of Arm (EOA) Dynamism:** Be aware that `robot.end_of_arm` and `robot.status['end_of_arm']` are dynamic based on the tool attached. Do not hardcode a specific gripper key without checking if it exists (e.g. check for `'stretch_gripper'`).
5. **Always Cleanup:** Use `with RobotClient() as robot:` or explicitly call `robot.stop()` to ensure the connection to the server is terminated cleanly.

## Troubleshooting

### robot.startup() is returning False

If you are not able to call RobotClient's `startup()` method or are getting a message, such as the snippet below, it means that the client is unable to connect to the Robot Server.

```
===============================================
                  
StretchBodyClient: Not able to connect to Stretch Body Server. Check that server is running
StretchBodyClient: Try running the server with stretch_body_server --launch
                  
===============================================
```

You can resolve this by running `stretch_body_server --print` in a terminal to see the server logs and check for any errors. If you do not see any errors, you can run `stretch_body_server --restart` to restart it and tail the logs. Check the logs for any startup errors and resolve them.

#### Common Causes of Server Start Failure

**Detached or wrong gripper or end-of-arm tool**

A common cause of the server failing to start is a detached gripper or end-of-arm tool. You can run `stretch_configure_tool` or (`stretch_configure_tool -d` if the server is offline) to select the right end-of-arm tool or no tool.

**E-fuse reset**

In very rare cases, your robot's joints may have triggered an e-fuse, which is like a circuit breaker to protect your robot's electrical components. To reset an e-fuse, turn off the robot and unplug the battery for 10 seconds, then plug it in again. You could also run `REx_actuator_control --all` to power cycle all the robot's joints while the robot is turned on. You will need to call `stretch_robot_home` to recalibrate the joints after doing this.&#x20;

### move\_by or move\_to is not working

If your joint's move\_by or move\_to is not responding, check that:

1. You have called `robot.push_command()` to tell the robot serverto execute queue'd commands.
2. Your joint is homed. You can home by running `stretch_robot_home` from a terminal. Warning: the robot's joints will move when you run this command!


# Robot Parameters

This document provides a comprehensive overview of the parameter system in the Stretch robot codebase, including its organizational structure, runtime plug-in mechanisms, and the tools available for managing parameters. It is designed to be easily accessible to both users and AI agents.

## 1. High-Level Organizational Structure

The parameter system in Stretch uses a multi-layered dictionary approach, prioritized from base defaults to user-specific overrides. Parameters are resolved dynamically at runtime by `stretch4_body.core.robot_params.RobotParams`.

The parameter dictionaries are loaded and overwritten in the following order (ascending priority):

1. **`robot_params_<MODEL>.py` (Python):** Model-specific nominal parameters (e.g., `robot_params_SE4.py`). Defines the baseline configuration for a specific robot model.
2. **`stretch_configuration_params.yaml` (YAML):** Robot-specific data (e.g., serial numbers, hardware offsets, and factory calibration data). Typically updated by factory or calibration tools.
3. **`stretch_user_params.yaml` (YAML):** User-specific overrides (e.g., custom velocity limits, contact thresholds, controller tunings). **This is the highest priority.**

> \[!WARNING] **Common Trap:** A frequent issue is that a robot may have `user_params` overriding the factory params (perhaps set by another user previously), generating undesired or unexpected behavior. Always check `stretch_user_params.yaml` for active user overrides if the robot behaves unexpectedly.

> \[!NOTE] **YAML vs. Python:** Python files serve as the rigid, heavily-structured "default" settings shipped by the manufacturer. YAML files act as local configuration files used to store specific calibrations (`configuration_params.yaml`) or explicit user preferences (`user_params.yaml`) without modifying the tracked source code.

### Plug-in System and Runtime Generation

Stretch utilizes a highly modular plug-in architecture to dynamically generate configurations at runtime based on the physical hardware attached to the robot.

* **Plug-in Managers (`SentryManager`, `SafeMotionManager`, `RoutineManager`):** The system uses various managers to dynamically load plug-ins.
  * `SentryManager` handles safety sentries or monitors (e.g., `self_collision_loop`) that protect the robot or monitor state.
  * `SafeMotionManager` manages plug-ins that restrict motions of the motors to help avoid hazards (e.g., limiting velocity/acceleration, triggering safe stops).
  * `RoutineManager` manages plug-ins that execute complex routines or behaviors. These plug-ins are defined in the parameters under `['controllers']`. At runtime, the respective manager reads the `py_module_name` and `py_class_name` for each enabled plug-in and instantiates them dynamically.
* **End-Of-Arm (EOA) Tool Generation:** The End-Of-Arm tool configuration is entirely generated at runtime. When `robot.tool` is specified in a YAML file (e.g., `'eoa_wrist_dw4_tool_sg4'`), the `RobotParams` class looks up the corresponding tool template in `nominal_params`. It expands the parameter dictionary by pulling in the relevant joint properties (like `SE4_wrist_yaw_DW4` or `SE4_stretch_gripper_DW4`) into the active `devices` list.

## 2. Managing Parameters with CLI Tools

The codebase provides two primary Python scripts to inspect and modify parameters:

### `stretch_params.py`

This tool recursively traverses the resolved parameter tree and prints it to the console, importantly noting the **Origin** of each parameter (e.g., whether it came from nominal python defaults, or was overridden by `stretch_user_params.yaml`).

**Using with `grep`:** Since the parameter list is extensive, `stretch_params.py` is best used alongside `grep` to quickly check the active value of a specific parameter and ensure user overrides are being respected.

```bash
# Check the active value and origin of the arm's velocity
stretch_params.py | grep arm | grep vel_m
```

### `stretch_change_param.py`

An interactive command-line utility used to safely modify the robot parameters. It dynamically explores the `RobotParams` tree. When a user modifies a value, the script automatically writes the override to `stretch_user_params.yaml` without destroying existing overrides in the file.

```bash
# Launch the interactive parameter modification tool
stretch_change_param.py
```

## 3. Detailed Parameter Sections in `robot_params_SE4.py`

The `robot_params_SE4.py` file contains the baseline configuration for the Stretch SE4 model. It is organized into several distinct logical blocks:

### 3.1. EOA Joint Templates

Variables like `SE4_wrist_yaw_DW4` and `SE4_stretch_gripper_DW4` define the physical attributes of individual dynamixel/feetech servos.

* **Details:** Contains `eeprom_cfg` (limits, PIDs, protections) and `motion` profiles (`default`, `fast`, `max`, `slow`).
* **Relation to Structure:** These serve as building blocks. They are not directly loaded into the root parameter dictionary until an EOA tool explicitly requests them.

### 3.2. EndOfArm Defn (Tool Configurations)

Dictionaries like `SE4_eoa_wrist_dw4_tool_sg4` group multiple EOA joints together into a cohesive tool.

* **Details:** Defines the `wrist`, the `tool`, `stow` positions, collision management offsets, and points to the `py_class_name` and `py_module_name` for the software driver. It also enumerates the required joint templates under the `devices` key.
* **Relation to Structure:** This represents the plug-in schema. When a tool is selected, `RobotParams` iterates through the `devices` key and merges the associated EOA Joint Templates (from 3.1) into the active parameter tree.

### 3.3. `nominal_params` (Root Dictionary)

This is the master dictionary that gets injected into `RobotParams`. It contains base configurations for major subsystems and managers:

* **Hardware Subsystems:** Configurations for hardware like `omnibase` (kinematics, wheel diameter), `arm` (gearing, homing thresholds), and generic motor templates (e.g., `hello-motor-omni-2` or stepper motors).
* **Loops and Rates:** Settings for control loops like `line_sensor_loop` and `end_of_arm_loop` (e.g., `loop_rate_Hz`).
* **Plug-in Configurations:** Defines the parameter profiles for the various plug-ins managed by `SentryManager`, `SafeMotionManager`, and `RoutineManager` (e.g., settings for `routine_docking` or `sentry_self_collision`).
* **Relation to Structure:** It acts as the baseline priority hierarchy. It holds the `supported_eoa` lists and templates that the runtime generation system uses to build the final dictionary.

## 4. Organizational Structure Visualization

The following diagram illustrates how parameters flow into the resolved parameter tree and how plug-ins are generated.

```mermaid
graph TD
    %% Parameter Sources
    RP_PY["robot_params_SE4.py<br/>(Nominal Defaults)"]:::python
    CONF_YAML["stretch_configuration_params.yaml<br/>(Factory Calibrations)"]:::yaml
    USER_YAML["stretch_user_params.yaml<br/>(User Overrides)"]:::yaml

    %% Core System
    R_PARAMS{"RobotParams<br/>(Runtime Dictionary)"}:::core
    
    %% Injection
    RP_PY -->|1. Base| R_PARAMS
    CONF_YAML -->|2. Overrides| R_PARAMS
    USER_YAML -->|3. Highest Priority Overrides| R_PARAMS
    
    %% Dynamic Generation
    TOOL_YAML["robot.tool specified<br/>(e.g., 'eoa_wrist_dw4_tool_sg4')"] -.->|Triggers| EOA_GEN
    
    subgraph "Dynamic Runtime Generation"
        EOA_GEN["EOA Tool Generation"]:::gen
        PLUG_GEN["Plug-in Generation<br/>(Sentry, SafeMotion, Routine)"]:::gen
    end
    
    R_PARAMS --> EOA_GEN
    R_PARAMS --> PLUG_GEN
    
    %% Class/System usage
    EOA_GEN -->|Injects Tool Devices| R_PARAMS
    PLUG_GEN --> |Loads py_module_name| MANAGERS["Plug-in Managers"]
    
    classDef python fill:#4f81c7,stroke:#333,stroke-width:2px,color:#fff;
    classDef yaml fill:#e88d3e,stroke:#333,stroke-width:2px,color:#fff;
    classDef core fill:#57a661,stroke:#333,stroke-width:2px,color:#fff;
    classDef gen fill:#a859b3,stroke:#333,stroke-width:2px,color:#fff;
```

## Summary for AI Agents

When interacting with the Stretch parameter system:

1. **Never edit `robot_params_SE4.py` to change a local behavior.** Always instruct the user to use `stretch_change_param.py` or modify `stretch_user_params.yaml`.
2. **Dynamic Resolution:** Be aware that the dictionary structure changes based on the configured tool. If a joint isn't physically attached (defined by `robot.tool`), its parameters will not exist in the resolved tree.
3. **Debugging:** Use `stretch_params.py` to trace where a parameter is coming from if you suspect a YAML file isn't applying correctly.


# Server Behaviors

The `stretch4_body/behavior` directory contains the logic for advanced autonomous functionality and safety monitoring within the `RobotServer`. It is structured into three primary types of behaviors: **Sentries**, **Routines**, and **Safe Motions**.

These behaviors operate within a dynamic plug-in architecture, allowing new safety checks or autonomous sequences to be added without modifying the core server loop.

***

## 1. High-Level Roles

### Sentries

**Role:** Continuous background monitoring and limit enforcement. Sentries constantly watch the robot's state (telemetry, odometry, current draw, CPU temperature) and enforce dynamic limits on the robot's capabilities. For example, the `sentry_limit_vel_on_pose` reduces the maximum allowed base velocity if the arm is extended far out, preventing tipping. The `sentry_self_collision` monitors for self-collision and updates safety limits. Sentries generally do not create motion commands; they enforce the bounds within which motion commands must operate.

### Safe Motions

**Role:** Final-check command modification and hazard avoidance. Safe motions act as a final layer of defense. They intercept the pending commands immediately before they are sent to the hardware. If a command would cause a hazard (e.g., `safe_motion_overtilt_avoid`), the safe motion plug-in can actively overwrite the setpoint (e.g., zero out the velocity) or trigger a system-wide safe stop.

### Routines

**Role:** Predefined, autonomous macro sequences. Routines handle complex, multi-step actions like homing (`routine_homing`), stowing the arm (`routine_stow`), or docking to the charger (`routine_blind_dock`). When a routine is active, it takes over control of the robot, rejecting motion commands from external clients until the routine finishes or is explicitly canceled.

***

## 2. Plug-in Architecture and Management

The `RobotServer` does not hardcode which behaviors run. Instead, it relies on three manager classes (`SentryManager`, `SafeMotionManager`, and `RoutineManager`).

During startup, each manager reads the `controllers` list from the active parameter configuration (`robot_params`). It then uses `importlib` to dynamically import the corresponding `py_module_name` and instantiate the `py_class_name`.

```yaml
# Example from YAML configuration

sentry_cpu_temp:
  py_module_name: stretch4_body.behavior.sentries.sentry_cpu_temp
  py_class_name: SentryCPUTemp
  enabled: 1
```

If a behavior is marked with `enabled: 1`, it is instantiated and passed a reference to the `Robot` instance. This allows developers to easily create and inject custom sentries or routines simply by updating the YAML configuration.

***

## 3. Flow of Control within the Server Loop

Understanding the differences between these three behaviors requires looking at exactly *when* they execute inside the `RobotServer`'s 100Hz control loop:

1. **Pull Status:** The server asynchronously pulls the latest state from the hardware.
2. **Step Sentries (`sentry_manager.step()`):** Sentries run immediately after new state data arrives. They analyze the state and update internal limits (like `max_vel` or `max_accel`). They *do not* see incoming commands.
3. **Ingest Client Commands:** The server receives new commands from the ZMQ network (e.g., user scripts, ROS).
4. **Step Routines (`routine_manager.step()`):** If a Routine is active, it overrides step 3. The routine takes over generating the motion commands for this cycle and external client commands are rejected.
5. **Step Safe Motions (`safe_motion_manager.step()`):** Right before the commands are sent to the motors, Safe Motions analyze the pending trajectory. If the command violates a safety condition, the Safe Motion will rewrite the command (e.g., override the target velocity to 0) or trigger a safe stop.
6. **Push Command:** The server pushes the finalized commands to the hardware via serial/ZMQ.
7. **Publish Status:** The server broadcasts the updated state and results to listening clients.

### Summary of Differences

* **Sentries** run *early* in the loop. They analyze state and set bounds.
* **Routines** run in the *middle* of the loop. They *generate* commands and block the client.
* **Safe Motions** run at the *end* of the loop. They analyze and *overwrite/veto* commands just before execution.


# Self-Collision

The self-collision system is a critical safety behavior implemented as a Sentry (`SentrySelfCollision`). It continuously monitors the robot's joint states to prevent the arm, lift, and end-of-arm tools from colliding with the robot's own body (e.g., the mast, base, or head).

## 1. High-Level Architecture

The self-collision system is organized into several distinct layers:

* **MuJoCo Physics Engine:** At the core, the system uses the MuJoCo physics engine to perform fast forward kinematics and collision checking. It dynamically loads the robot's exact URDF and collision meshes using the `stretch4_urdf` package, matching the specific robot model, batch, and attached end-of-arm tool.
* **Multiprocessing (`SelfCollisionLoop`):** Collision checking is computationally intensive. To prevent it from blocking the high-frequency (100Hz) main control loop, the `SelfCollisionLoop` spawns a dedicated background worker process. The main server loop sends the current joint configuration into an asynchronous command queue, and the background worker continuously processes these states, pushing the resulting collision status back into a status queue.
* **Motion Clipping Response:** When a collision is imminent, MuJoCo calculates the "collision direction"—the gradient indicating which way the joint must move to escape the collision. The `SentrySelfCollision` uses this to set `pos` or `neg` collision flags for the affected joints. These flags are fed into each subsystem's `step_collision_avoidance` method (e.g., in the `Arm` or `Lift` classes), which actively zeroes out any commanded velocity that would push the joint further into the collision, stopping the joint while allowing the user to back away safely.

## 2. Configuration Parameters

The system is highly configurable via the `self_collision_mujoco` dictionary in the robot's parameter files (e.g., `stretch4_body/robot/robot_params_SE4.py`). These parameters dictate what is checked and how sensitive the system is:

* **`k_brake_distance`:** The system doesn't just check the robot's exact current position; it checks a "virtual" position slightly ahead of the robot based on its current velocity and braking capabilities. The `k_brake_distance` parameter (e.g., `{'lift': 1.1, 'arm': 1.1}`) acts as a multiplier. A value of 1.1 means the system pads the joint's position by 110% of its required braking distance, ensuring the robot stops *before* making contact.
* **`ignore_links`:** A list of URDF link names (e.g., `wheel_0_link`, `camera_center_link`) that will be completely ignored by the collision engine. Their collision properties are disabled internally to save computation time and ignore benign hardware.
* **`exclusions`:** A list of link pairs (e.g., `["head_link", "lift_link"]`). These specific pairs are allowed to intersect or touch without triggering a collision event. This is necessary because many adjacent links in a URDF naturally overlap at their joints during normal operation.

## 3. Flow of Control

The following diagram illustrates how data flows through the system, from the main control loop down to the physics engine, and back up to stop the joint motion.

```mermaid
graph TD
    ServerControlLoop --> SentryManager
    SentryManager --> SentrySelfCollision
    
    SentrySelfCollision --> SendJointConfigurationQueue
    SendJointConfigurationQueue --> SelfCollisionLoopWorker
    
    SelfCollisionLoopWorker --> MujocoPhysicsEngine
    MujocoPhysicsEngine --> ComputeCollisionsAsync
    ComputeCollisionsAsync --> ReturnCollisionStatus
    ReturnCollisionStatus --> SelfCollisionLoopWorker
    
    SelfCollisionLoopWorker --> ReadLatestStatusQueue
    ReadLatestStatusQueue --> SentrySelfCollision
    
    SentrySelfCollision --> IdentifyResolveDirection
    IdentifyResolveDirection --> SetCollisionStopFlags
    SetCollisionStopFlags --> Subsystems
    Subsystems --> ApplyBrakeAndStopMotion
```


# Hardware Subsystems

This document provides an overview of the core hardware subsystems that make up the Stretch robot. It details how these subsystems are instantiated, managed, and configured, along with key parameters, status fields, and API calls for each.

## Subsystem Instantiation and Management

Subsystems are managed collectively by the `RobotCore` base class (and its descendants, like `Robot` or the `RobotServer`). During startup, `RobotCore` reads the `subsystems` list from the active parameter configuration (`robot_params`). It then instantiates the corresponding Python objects and stores them in its `self.subsystems` dictionary.

* `RobotCore` handles the base instantiation of `arm`, `lift`, `omnibase`, and `power_periph`.
* The `end_of_arm` and `line_sensor` subsystems are handled specifically by `Robot` or the server, as they rely on additional configuration (such as `robot.tool` to determine the active class) or separate multiprocess workers.

During the control loop, `RobotCore` aggregates data by calling `pull_status` and `push_command` on every active subsystem in the list.

### Disabling a Subsystem

If a hardware subsystem is physically removed or malfunctioning, you can prevent the software from attempting to instantiate and communicate with it by overriding the `subsystems` list in your `stretch_user_yaml`.

For example, to run the robot without the `end_of_arm` subsystem, you would add the following to your user YAML:

```yaml
robot:
  subsystems:
    - arm
    - lift
    - omnibase
    - power_periph
```

*(By omitting `end_of_arm`, the software will bypass its initialization entirely.)*

> **Note on Cameras:** The `cameras` directory is included within the `subsystems` folder structure, but cameras are not actively managed by the `RobotServer` or `RobotCore` control loop. The directory exists primarily to manage camera configuration parameters (like framerates, exposure limits, and intrinsic calibrations) within the unified parameter system.

***

## Subsystems Overview

### 1. Arm (`stretch4_body.subsystem.arm.Arm`)

Controls the horizontal prismatic joint (in/out extension) of the robot. It translates motor rotation into linear motion using a chain and sprocket mechanism.

### 2. Lift (`stretch4_body.subsystem.lift.Lift`)

Controls the vertical prismatic joint (up/down) of the robot, driven by a belt. It supports payload-based gravity feedforward compensation to safely lift and hold various end-of-arm tools.

### 3. OmniBase (`stretch4_body.subsystem.omnibase.OmniBase`)

Controls the differential/omnidirectional base using three stepper motor wheels. It provides real-time odometry and handles blended translational and rotational commands, as well as guarded collision detection.

### 4. PowerPeriph (`stretch4_body.subsystem.power_periph.PowerPeriphBase`)

Serves as the central hub for the robot's power state, IMU data, battery monitoring, and peripheral GPIO (LEDs, buzzers, fans, and runstop management).

### 5. EndOfArm (`stretch4_body.subsystem.end_of_arm.EndOfArm`)

A modular tool system dynamically instantiated based on the active `robot.tool` configuration (e.g., dexterous wrists, parallel grippers). It typically runs its hardware communication inside a separate high-frequency background worker process to decouple from the main server loop.

### 6. LineSensor (`stretch4_body.subsystem.line_sensor.LineSensorLoop`)

Interfaces with the downward-facing Pixart hardware sensors used for line following or drop-off detection. Similar to the EndOfArm, it runs in a dedicated background worker process for high-speed, non-blocking serial acquisition.

***

## Class Architecture

The following diagram illustrates how `RobotCore` manages the subsystems, and how specific hardware subsystems inherit from shared base classes (like `PrismaticJoint`).

```mermaid
classDiagram
    class RobotCore {
      +dict subsystems
    }
    class Robot
    
    class Arm
    class Lift
    class OmniBase
    class PowerPeriph
    class EndOfArm
    class LineSensor
    class PrismaticJoint

    RobotCore <|-- Robot
    RobotCore *-- Arm
    RobotCore *-- Lift
    RobotCore *-- OmniBase
    RobotCore *-- PowerPeriph
    RobotCore *-- EndOfArm
    RobotCore *-- LineSensor
    PrismaticJoint <|-- Arm
    PrismaticJoint <|-- Lift
```


# End-Of-Arm (EOA)

The `end_of_arm` subsystem is responsible for controlling the various tools and joints attached to the distal end of the robot's arm, such as the wrist (yaw, pitch, roll) and the gripper. It is designed to be highly modular, supporting a wide range of custom and factory tool attachments.

## Modular Architecture and Tool Determination

The modularity of the end-of-arm system is heavily reliant on the `RobotParams` system. At runtime, the `EndOfArmLoop` reads the user's YAML configuration to determine exactly which tool is installed on the robot.

It does this by looking up the `eoa_name` property and then instantiating the class dynamically via Python's `importlib`:

```python
rp = RobotParams._robot_params
eoa_name = RobotParams.eoa_name
module_name = rp[eoa_name]['py_module_name']
class_name = rp[eoa_name]['py_class_name']
eoa = getattr(importlib.import_module(module_name), class_name)()
```

This architecture allows users to define custom end-of-arm tools by simply creating a Python class that inherits from `EndOfArm` and specifying its module/class name in their `stretch_user_params.yaml`. Note that changing the active tool is done by setting

```
robot:
  tool: eoa_name
```

in the user yaml.

### Actuation Component: Feetech Servos

All actuation at the end-of-arm is driven by **Feetech** smart serial servos. The base `EndOfArm` class inherits from `FeetechSMChain`, meaning that the end-of-arm is treated as a daisy-chained serial bus of Feetech motors. Each individual joint (like `wrist_pitch`, `wrist_roll`, or `stretch_gripper`) corresponds to a `FeetechSMHello` instance that sits on this chain.

## Class Hierarchy

The following diagram illustrates the class hierarchy and ownership from the low-level Feetech chain up to the client application:

```mermaid
classDiagram
    FeetechSMChain <|-- EndOfArm
    EndOfArm <|-- EOA_Wrist_DW4_Tool_NIL
    EndOfArm <|-- EOA_Wrist_DW4_Tool_SG4
    EndOfArm <|-- EOA_Wrist_DW4_Tool_PG4
    
    EndOfArmLoop *-- EndOfArm : Instantiated in Worker Process
    RobotServer *-- EndOfArmLoop : Manages Lifecycle
    RobotClient ..> RobotServer : Communicates over ZMQ
```

## Multiprocessing and Control Rate

Communicating with multiple serial servos on a shared bus can cause I/O latency. To maintain a solid **50Hz** control and status update rate, the `end_of_arm` subsystem relies on Python multiprocessing.

The `EndOfArmLoop` spins up a separate background `Process` that runs the `end_of_arm_loop_worker`. Inside this worker, a high-frequency loop constantly calls `eoa.pull_status()` to read from the servos over the serial port.

Because this happens in an isolated process, any serial port blocking or I/O delays do not interrupt the main `RobotServer` application loop. The server manages this by providing two `CircularMultiprocessingQueue` objects:

* `q_status`: The worker drops fresh state dictionaries here; the main process reads them.
* `q_cmd`: The main process drops RPC commands (like `move_to`) here; the worker executes them against the `EndOfArm` instance.

## Control Flow State Diagram

The flow of commands and data moves bidirectionally through the multiprocess queues and ZMQ transport.

```mermaid
flowchart TD
    A[Feetech Hardware Servos] <-->|UART and Serial| B[FeetechSMHello]
    B <--> C[EndOfArm Instance]
    
    subgraph Background Worker Process
        C -->|status polling| D[end_of_arm_loop_worker]
        D -->|method execution| C
    end
    
    D -->|status dict| E[q_status CircularMultiprocessingQueue]
    I[q_cmd CircularMultiprocessingQueue] -->|command tuple| D
    
    subgraph Main Process
        E -->|pull_status| F[EndOfArmLoop]
        F -->|push_command| I
        F -->|Aggregated Status| G[RobotServer]
        G -->|RPC and Commands| F
    end

    G -->|ZMQ Pub and Sub| H[RobotClient]
    H -->|ZMQ Req and Rep| G
```

## Available Tools

There are several command-line tools available for interacting with the `end_of_arm` subsystem, testing joints, and calibrating the system, including:

* `stretch_dex_wrist_home.py`: Homes the full 3-DOF dexterous wrist.
* `stretch_dex_wrist_jog.py`: Interactively jog the yaw, pitch, and roll axes of the wrist.
* `stretch_gripper_home.py`: Homes the end-of-arm gripper.
* `stretch_gripper_jog.py`: Interactively open and close the gripper.
* `stretch_wrist_yaw_home.py`, `stretch_wrist_pitch_home.py`, `stretch_wrist_roll_home.py`: Individually home specific wrist joints.
* `REx_feetech_backlash_measure.py`: Factory tool designed to measure mechanical backlash in the Feetech servos.
* `REx_feetech_id_change.py`: Factory tool to change the ID of a Feetech servo.
* `REx_feetech_id_scan.py`: Factory tool to scan the serial bus for active Feetech IDs.
* `REx_feetech_jog.py`: Factory tool to interactively jog individual Feetech servos.
* `REx_feetech_reboot.py`: Factory tool to soft-reboot a Feetech servo.
* `REx_feetech_set_baud.py`: Factory tool to change the baud rate of a Feetech servo.


# Cameras

## Preview from the Cameras from the command line

You can use the following tool to display the camera feeds in opencv, rerun and store them locally anywhere from the command line: [stretch\_camera\_show](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/tools/stretch_camera_show.py).

Run `stretch_camera_show --help` for all the available options.

Examples:

```
stretch_camera_show # Display the left, right and center head cameras in rerun (Default)

stretch_camera_show --left # Display the left head camera only in rerun

stretch_camera_show --left --opencv # Display the left head camera only in opencv

stretch_camera_show --left --no-rotate # Display the left head camera without rotation in rerun

stretch_camera_show --gripper # Display the gripper camera feeds and the point cloud from the stereo depth in rerun

stretch_camera_show --left --recording_directory ./recordings # Store the left head camera to disk as a video (and display in rerun)

stretch_camera_show --left --recording_directory ./recordings --record_format .png # Store one .png per frame instead

stretch_camera_show --recording_directory ./recordings --recording_chunk_seconds 60 # Split the videos into one-minute files
```

Recording turns the display off, because drawing the imagery costs frames: rerun cannot keep up with the head cameras and backpressures the pipeline that feeds it, which drops the synced left/right pair from 30 fps to around 11 a few minutes into a recording, and has been seen to stall capture altogether. Pass `--rerun` or `--opencv` alongside `--recording_directory` to display it anyway and accept the dropped frames.

Recordings are written to `RECORDING_DIRECTORY/<camera>/<timestamp>/`. `--record_format` takes `.mp4` (the default), which writes video per camera at the camera's configured frame rate, or `.png` and `.jpg`, which write one file per frame, named after the frame's timestamp.

### Video recordings

Video is encoded as HEVC (H.265), on the GPU when the machine has one that can encode it and on the CPU otherwise. This needs `ffmpeg` on the `PATH`; without it recording falls back to OpenCV's mpeg4 encoder, which produces files roughly ten times larger.

A video recording is split into chunks of `--recording_chunk_seconds` (300 by default), named `video_0000.mp4`, `video_0001.mp4` and so on. Chunking matters because an mp4 that never gets closed has no index and will not play back at all, so without it an interrupted recording is lost entirely rather than just its last chunk. Pass `--recording_chunk_seconds 0` to write a single `video.mp4` instead.

Chunks rotate on elapsed real time, not on the length of the recorded video. The two differ whenever a camera delivers below its configured frame rate: the video's own timeline then advances slower than the clock, so a chunk measured in recorded seconds would take proportionally longer to fill. The gap is small when nothing is competing for the frames, but it is wide enough to matter with a display attached, where a "300 second" chunk took over half an hour to close. Note that this also means the video plays back faster than real time when frames were dropped, because it is written at the camera's configured rate.

Long recordings are practical at these settings. Measured on the 12MP center camera, HEVC writes about 10 Mbps where the mpeg4 encoder wrote 112 Mbps, so recording the left, right and center cameras together costs roughly 4.5 GB an hour instead of 63 GB. A static scene compresses much further - the three head cameras together measured 4.1 Mbps, about 1.8 GB an hour - so budget by how much the scene moves.

With the display off, the head cameras hold their configured rates for the whole recording: the left and right cameras measured 30.0, 29.9 and 29.9 fps over successive chunks, and the center camera 96% of its configured 10 fps.

To play the chunks of a recording as one video:

```bash
printf "file '%s'\n" video_*.mp4 > chunks.txt
ffmpeg -f concat -safe 0 -i chunks.txt -c copy whole_recording.mp4
```

## Using the Cameras with Python

### Stream-API

The camera subsystem exposes Python [generators](https://book.pythontips.com/en/latest/generators.html) for streaming frames from the cameras. These generators yield [ImageFrame](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/models/image_frame.py) or [SyncedImageFrame](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/models/image_frame.py) objects.

[ImageFrame](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/models/image_frame.py) is a container for a single camera's image and metadata. Access the image using the `image` attribute, timestamp with `timestamp`, and [AI model results](#custom-ai-models-eg-rtmo-pose-estimation) with `ai_model_results`.

[SyncedImageFrame](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/models/image_frame.py) is a container for multiple cameras' images and metadata. You can access the individual camera frames using the `left`, `right`, and `center` attributes of the `SyncedImageFrame` object, which are `ImageFrame` objects.

Using one camera yields an ImageFrame.

```python
from stretch4_body.subsystem.cameras import *

for image_frame in stream_left_camera():
    if image_frame is None: 
        print("No frame returned")
        continue
    print(f"Got image: {image_frame.image.shape=}, {image_frame.timestamp=}")
```

Using multiple cameras from the head cameras simultaneously yields a `SyncedImageFrame`.

```python
from stretch4_body.subsystem.cameras import *

for image_frame in stream_left_right_camera():
    if image_frame is None: 
        print("No frame returned")
        continue
    print(f"Got left image: {image_frame.left.image.shape=}, {image_frame.left.timestamp=}")
    print(f"Got right image: {image_frame.right.image.shape=}, {image_frame.right.timestamp=}")
```

Stream from the gripper RGBD camera. This yields a SyncedImageFrame and populates the pointcloud field.

```python
from stretch4_body.subsystem.cameras import *

for image_frame in stream_gripper_camera():
    if image_frame is None: 
        print("No frame returned")
        continue
    print(f"Got left image: {image_frame.left.image.shape=}, {image_frame.left.timestamp=}")
    if image_frame.pointcloud is not None:
        print(f"Got pointcloud image: {image_frame.pointcloud.shape=}")
```

### Compressed streams

Every stream function has a `*_compressed()` twin (`stream_left_camera_compressed()`, `stream_left_right_camera_compressed()`, `stream_gripper_camera_compressed()`, and so on). They open the same cameras, but the camera MJPEG-encodes frames on-chip, so a head frame costs a couple hundred kilobytes instead of \~7 MB. Use them whenever frames have to leave the process; the head cameras are 1920x1200 and the center camera is 12MP, so raw pixels saturate a transport long before the sensors do.

The default `is_run_pipeline=True` decodes frames for you, so `image` is ordinary BGR and the compressed variants are a drop-in replacement:

```python
from stretch4_body.subsystem.cameras import *

for image_frame in stream_left_right_camera_compressed():
    print(f"Got left image: {image_frame.left.image.shape=}")
```

Pass `is_run_pipeline=False` to keep the JPEG bitstream instead. `ImageFrame.is_compressed()` then returns True, `image` is a 1-D buffer, and `uncompress()` decodes it when you actually need pixels. This is what the ROS 2 camera node does: it forwards the bitstream to the compressed topics without ever decoding it.

Camera configuration (resolution, frame rate, JPEG quality, rotation, exposure) lives in `CAMERA_CONFIGS` in [rgb\_camera\_config.py](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/models/rgb_camera_config.py) and is read through `RGBCameras.<camera>.config`. A compressed variant reuses its base camera's entry with compression forced on, and `RGBCameras.<camera>.base` gets you back to the camera it derives from.

## Preview RGBD Cameras from the command line

Run `stretch_rgbd_show --help` for all the available options.

Examples:

```
stretch_rgbd_show --left_right # Display the left, right head cameras only

stretch_rgbd_show --left_right_center # Display the left, right and center head cameras

stretch_rgbd_show --left # Display the left head camera only

stretch_rgbd_show --right # Display the right head camera only

stretch_rgbd_show --center # Display the center head camera only
```

If you have ROS2 launch files running for cameras or lidars, you can use the `--use_ros_for_cameras` or `--use_ros_for_lidar` flags to use the ROS2 data instead of direct python-api access to those sensors.

```
stretch_rgbd_show --left --use_ros_for_cameras --use_ros_for_lidar
```

## Using Emulated RGBD with Python

You can capture RGBD data from the head cameras using the following scripts:

```python
from stretch4_body.subsystem.cameras import *

for frame in stream_left_rgbd():
    if frame is None: 
        print("No frame returned")
        continue
    print(f"""Got a point cloud using the left camera and both lidar.
Number of points: {frame.pointcloud.shape[0]}
Depth size: {frame.depth_image.shape}""")

for frame in stream_left_right_center_rgbd():
    if frame is None: 
        print("No frame returned")
        continue
    print(f"""Got a point cloud using the left, right and center cameras and both lidar.
Left:
    Number of points: {frame.left.pointcloud.shape[0]}
    Depth size: {frame.left.depth_image.shape}
Right:
    Number of points: {frame.right.pointcloud.shape[0]}
    Depth size: {frame.right.depth_image.shape}
Center:
    Number of points: {frame.center.pointcloud.shape[0] if frame.center is not None else 'N/A'}
    Depth size: {frame.center.depth_image.shape if frame.center is not None else 'N/A'}""")
```

## Custom AI Models (e.g. RTMO Pose Estimation)

You can pass custom AI models to the camera pipeline by wrapping them in an `AIModelWrapper` instance. The pipeline will automatically route imagery to your model. The results are packed in the `image_frame.ai_model_results` field.

Here is an example wrapping an RTMO model:

First install

```
git clone https://github.com/hello-robot/stretch4_human_pose_estimation.git
pip install ./stretch4_human_pose_estimation
human_pose_estimation_install_dependencies
human_pose_estimation_setup_models
human_pose_estimation_setup_models --size m
```

Then run this example:

```python
import cv2
import numpy as np
from stretch4_body.subsystem.cameras import stream_left_camera
from stretch4_body.subsystem.cameras.detectors.detector_ai_models import AIModelWrapper
from stretch4_human_pose_estimation.rtmo import RTMOPipeline

class RTMOWrapper(AIModelWrapper):

    def __init__(self):
        self.model = self.init_model()
        
    def name(self) -> str:
        return "RTMO Pose"

    def init_model(self):
        return RTMOPipeline(size="m", device="AUTO")

    def run_model(self, img: np.ndarray, conf_threshold: float = 0.5):
        return self.model.predict(img, conf_threshold=conf_threshold)

    def visualize_results(self, img: np.ndarray, result_from_run_model) -> np.ndarray:
        return self.model.visualize(img, result_from_run_model, kpt_thr=0.3, style="cvpr")

    @staticmethod
    def get_joint(person_res: dict, joint: BodyJoint) -> tuple[float, float]:
        """Returns the (x, y) coordinates of the specified joint for a single person."""
        kpts = person_res.get("keypoints", [])
        if joint < len(kpts):
            return float(kpts[joint][0]), float(kpts[joint][1])
        return 0.0, 0.0

    @staticmethod
    def get_joint_score(person_res: dict, joint: BodyJoint) -> float:
        """Returns the confidence score of the specified joint for a single person."""
        kpts = person_res.get("keypoints", [])
        if joint < len(kpts):
            return float(kpts[joint][2])
        return 0.0

# Instantiate the custom model wrapper
rtmo = RTMOWrapper()

# Start the left camera stream passing the AI model to the pipeline
for image_frame in stream_left_camera(ai_models_to_use=[rtmo]):
    if image_frame is None: 
        continue

    results = image_frame.ai_model_results[0]    

    annotated_image = rtmo.visualize_results(image_frame.image.copy(), results)
        
    cv2.namedWindow(rtmo.name(), cv2.WINDOW_NORMAL)
    cv2.imshow(rtmo.name(), annotated_image)
    if cv2.waitKey(1) == ord('q'):
        break
```

Here is another example wrapping a YOLOX object detection model (from rtmlib) configured to identify general COCO objects like cups, tables, desks, and chairs.

First install rtmlib:

```
pip install rtmlib
pip install openvino # To utilize the NPU on Stretch's NUC
pip install onnxruntime-openvino
```

The model weights will be automatically downloaded by rtmlib on the first run.

```python
import cv2
import numpy as np
from stretch4_body.subsystem.cameras import *
from stretch4_body.subsystem.cameras.detectors.detector_ai_models import AIModelWrapper

# RTMLIB imports
from rtmlib import YOLOX
from rtmlib.tools.base import RTMLIB_SETTINGS

# Configure rtmlib to utilize the Intel GPU or NPU via OpenVINO Execution Provider
RTMLIB_SETTINGS['onnxruntime']['npu'] = ('OpenVINOExecutionProvider', {'device_type': 'NPU'})
RTMLIB_SETTINGS['onnxruntime']['gpu'] = ('OpenVINOExecutionProvider', {'device_type': 'GPU'})


class YOLOXWrapper(AIModelWrapper):

    def __init__(self):
        self.coco_classes = [
            "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", 
            "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", 
            "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", 
            "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", 
            "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", 
            "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", 
            "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", 
            "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", 
            "scissors", "teddy bear", "hair drier", "toothbrush", "monitor", "can", "bottle", "tennis ball", "ball", "tape", "mug", "remote control", "desk"
        ]
        self.model = self.init_model()
        
    def name(self) -> str:
        return "YOLOX Object Detection"

    def init_model(self):
        device = 'gpu'  # npu, cpu, gpu
        backend = 'onnxruntime'  # We use onnxruntime to route to the OpenVINO NPU Provider
        # Provide a URL to a COCO yolox model. rtmlib will cache it automatically.
        url = 'https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_tiny.onnx'
        return YOLOX(url, mode='multiclass',model_input_size=(416, 416), backend=backend, device=device)

    def run_model(self, img: np.ndarray):
        return self.model(img)

    def visualize_results(self, img: np.ndarray, result_from_run_model) -> np.ndarray:
        bboxes, cls_inds = result_from_run_model
        h, w = img.shape[:2]
        img_area = h * w
        
        for bbox, cls_id in zip(bboxes, cls_inds):
            x1, y1, x2, y2 = map(int, bbox[:4])
            area = (x2 - x1) * (y2 - y1)
            
            # Color based on relative size (Green for small, Red for large)
            ratio = min(1.0, max(0.0, np.sqrt(area / img_area)))
            hue = int((1.0 - ratio) * 120) 
            
            hsv = np.uint8([[[hue, 255, 255]]])
            color = tuple(map(int, cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)[0][0]))

            cv2.rectangle(img, (x1, y1), (x2, y2), color, 3)
            
            label = self.coco_classes[int(cls_id)] if int(cls_id) < len(self.coco_classes) else str(cls_id)
            label = f"{label.capitalize()}"
            
            # Render a solid background for the text
            font_scale = 0.8
            thickness = 2
            (t_w, t_h), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness)
            
            y_label_bg = max(y1, t_h + 10)
            cv2.rectangle(img, (x1, y_label_bg - t_h - 10), (x1 + t_w + 10, y_label_bg), color, -1)
            
            # Draw white text over the solid background (anti-aliased)
            cv2.putText(img, label, (x1 + 5, y_label_bg - 5), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), thickness, cv2.LINE_AA)
            
        return img

# Instantiate the custom model wrapper
yolox_model = YOLOXWrapper()

# Start the left camera stream passing the AI model to the pipeline
# for image_frame in stream_center_camera(ai_models_to_use=[yolox_model]):
#     if image_frame is None: 
#         continue

#     results = image_frame.ai_model_results[0]    

#     annotated_image = yolox_model.visualize_results(image_frame.image.copy(), results)
        
#     cv2.namedWindow(yolox_model.name(), cv2.WINDOW_NORMAL)
#     cv2.imshow(yolox_model.name(), annotated_image)
#     if cv2.waitKey(1) == ord('q'):
#         break


for synced_frame in stream_gripper_camera(ai_models_to_use=[yolox_model]):
    if synced_frame is None: 
        continue
    image_frame =synced_frame.left

    results = image_frame.ai_model_results[0]    

    annotated_image = yolox_model.visualize_results(image_frame.image.copy(), results)
        
    cv2.namedWindow(yolox_model.name(), cv2.WINDOW_NORMAL)
    cv2.imshow(yolox_model.name(), annotated_image)
    if cv2.waitKey(1) == ord('q'):
        break
    
```

## Calibrating your cameras

Cameras are calibrated in the factory and the calibration files are stored in the robot's home directory, under `$HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_cameras/`.

If you need to re-calibrate your cameras, you can use the following tools.

First focus your camera lens using [REx\_camera\_focus](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/tools/factory/REx_camera_focus.py).

Then calibrate the camera intrinsics and extrinsics using [REx\_camera\_calibrate](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/tools/factory/REx_camera_calibrate.py).

The calibration process does the following:

1. Calibrates the camera intrinsics using [calibrate\_camera\_intrinsics](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/calibrate_intrinsics.py). This saves the calibration yaml file that contains `head_center`, `head_left`, and `head_right` keys with the K and D matrices, along with other information, at `$HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_cameras/calibration_rgb_head_camera.yaml` and a few other yaml files for ROS2 to work correctly.
2. Verifies the camera intrinsics using [camera\_intrinsics\_validate\_l2\_distance](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/camera_intrinsics_validate_l2_distance.py). This uses pre-tape-measured values to verify the camera intrinsics are correct. The values are expected to vary a little across robots, but it's a good "sanity check".
3. Calibrates the camera-camera extrinsics using [calibrate\_extrinsics\_cameras](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/calibrate_extrinsics_cameras.py). This saves the calibration transforms as a yaml file containing `left_to_center` and `right_to_center` keys at `$HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_cameras/camera_extrinsics.yaml` and the urdf calibration file at `$HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_values.yaml`.
4. Calibrates the camera-lidar extrinsics using [calibrate\_extrinsics\_lidars](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/calibrate_extrinsics_lidars.py). This appends the camera-lidar extrinsics `transform_right_lidar_to_head_center` key to the `$HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_cameras/camera_extrinsics.yaml` file and the urdf calibration file at `$HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_values.yaml`.

These values are used to estimate the distance to ArUco markers of known size using [detector\_aruco.py](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/detectors/detector_aruco.py) that uses `cv2.solve_pnp` or `cv2.fisheye.solve_pnp` depending on the lens type.

These values are also used by [emulated\_rgbd.py](https://github.com/hello-robot/stretch4_body/blob/main/stretch4_body/subsystem/cameras/emulated_rgbd.py) to create an colored point clouds and depth images using the left and right lidars, and each head camera using `cv2.projectPoints` or `cv2.fisheye.projectPoints` depending on the lens type. This also requires the lidar extrinsics to be calibrated using <https://github.com/hello-robot/stretch\\_dual\\_lidar\\_calibration>.

### Calibration Controls & Offline Replay

During execution of the calibration tools (`REx_camera_calibrate` for camera-lidar or camera intrinsics), progression and commands can be input using either the gamepad controller or terminal keyboard inputs:

* **Gamepad Control**: Tap `X` to capture a frame / unpause automatic movement, and hold `X` (for 3-4 seconds) to save the calibration to disk.
* **Keyboard Control**: Type `x` + Enter to capture/unpause, type `s` + Enter to save/exit, and type `q` + Enter to quit without saving. This allows running calibration without a gamepad connected.

For camera-lidar calibration, you can also replay a previous session offline:

```bash
REx_camera_calibrate --extrinsics_lidar --replay_last
```

This offline replay automatically bypasses the gamepad pauses and runs through all poses automatically without requiring user input.


# Line Sensors

Six PixArt J3 sensors ring the base, each pointing down and forward, each reporting 320 range bins at \~30 Hz. Together they see the floor immediately around the robot: obstacles, and cliffs.

The firmware streams newline-delimited JSON over a USB CDC port (`/dev/hello-pixart-j3`). A background process decodes it; the robot server publishes it; clients consume it over ZMQ.

## The status codes come first

A bin does not always carry a distance. The chip has two status codes:

| raw      | metres | meaning                                                                                                     |
| -------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| `0xFF80` | 5.11   | **No detection at all.** No reflection came back. Ambiguous: a dark floor, a lighting condition, or a void. |
| `0xFE80` | 5.09   | **Detected, but beyond the range limit.** Something returned from further than the sensor can measure.      |

Because the sensors point *down*, 5.09 is the stronger cliff evidence: the beam travelled past where the floor should have been and found something far away.

These are classified **once**, at decode, in `line_sensor/protocol.py`, before any conversion or correction:

* `ranges[bin]` is set to `NaN` wherever the bin is not a distance;
* `codes[bin]` carries the identity (`CODE_VALID`, `CODE_BEYOND_LIMIT`, `CODE_NO_RETURN`, `CODE_OTHER_INVALID`).

## Status schema

### `health` — subsystem-wide

| field                                                                             | meaning                                                          |
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `streaming`                                                                       | decoding is running. **False means no cliff detection.**         |
| `port_open`                                                                       | the serial port is open. False during a dropout.                 |
| `disabled_sensors`                                                                | names switched off at runtime, by choice                         |
| `sensors_dead`                                                                    | names not reporting, by fault. Empty during normal operation.    |
| `rate_hz`                                                                         | whole-frame rate, \~30 Hz                                        |
| `reader_restarts`                                                                 | serial recoveries so far. A climbing number means a flaky cable. |
| `decode_errors`, `frame_advance_err`, `frame_not_full_err`, `not_six_sensors_err` | counters; all should stay flat                                   |

`disabled` and `dead` are deliberately different: one is a choice, the other is a fault. A disabled sensor never appears in `sensors_dead`.

### `sensor_0` … `sensor_5` — per sensor

| field                                 | meaning                                                        |
| ------------------------------------- | -------------------------------------------------------------- |
| `ranges`                              | float64\[320], metres, `NaN` at every non-measurement bin      |
| `codes`                               | uint8\[320], `protocol.CODE_*` — the authority on 5.09 vs 5.11 |
| `n_no_return`, `n_beyond_limit`       | counts of each code this report                                |
| `missed_frames`                       | consecutive frames not reported; 0 while healthy               |
| `enabled`                             | False if switched off at runtime                               |
| `frame_id`, `ts_last_read`, `rate_hz` |                                                                |

### `calibration` — served by the body

The tare is loaded, fingerprint-checked and published **by the robot**, so no consumer opens a YAML file or repeats the validation

| field      | meaning                                             |
| ---------- | --------------------------------------------------- |
| `loaded`   | names with an accepted tare                         |
| `rejected` | `{name: why}` for every refusal                     |
| `id`       | changes when the calibration changes; cache on this |
| `sensor_N` | packed tare                                         |

## Parameters

Under `line_sensor_loop` in the robot params:

* `loop_rate_Hz` — 250. The reader polls far faster than the sensors report.
* `sensor_names` — `sensor_0` … `sensor_5`, clockwise from robot forward.
* `bus_sensor_map` — **`[[1, 0], [3, 2], [5, 4]]`**. Maps `distances<bus><dev>` in the JSON to a logical sensor index. Change it if the cables are plugged in differently.
* `flip_range_ordering` — whether to reverse each 320-bin array.
* `line_sensor_geometry` — FOV, mounting angles, emitter height and pitch diameter, `pixart_report_num`.

`bus_sensor_map` and `flip_range_ordering` **raise if missing** rather than defaulting. A wrong bin order mirrors every reading left-to-right and nothing downstream can detect it; a wrong bus map rotates the whole ring. Six identical 60-degree wedges tile the circle, so *any* mapping error produces a complete, plausible-looking result. Only a physical observation can catch it — occlude one wedge and confirm the expected sensor index responds.

## Two lossy hops, and what that forces

```mermaid
flowchart TD
    A[PixArt hardware] -->|SPI / I2C| B[hello_pixart_j3 firmware in stretch_firmware_ii]
    B -->|newline-delimited JSON over USB CDC| C[PixartJ3Reader, background process at 250 Hz]
    C -->|complete status| D[q_status: CircularMultiprocessingQueue depth 3]
    D -->|pull_status| E[LineSensorLoop in the robot server]
    E -->|whole robot status at 100 Hz| F[ZMQ PUB, CONFLATE=1]
    F --> G[RobotClient / LineSensorLoopClient]
```

Both transports **drop**:

* `q_status.put()` discards the *oldest* message when full. The reader puts at 250 Hz; the control loop drains at 48–100 Hz; the queue is 3 deep.
* The ZMQ status socket sets `CONFLATE=1` on both ends, so a subscriber only ever holds the newest message.

That single fact drives two rules:

1. **Every status message carries all six sensors.** a dropped delta loses that sensor's frame permanently, and the symptom — one sensor frozen while five stream — is indistinguishable from a hardware fault.
2. **The calibration rides every message.** Publishing it once would be conflated away for any client that connected a moment later, leaving it silently uncalibrated.

`q_cmd` is the exception: it is 100 deep, because dropping the oldest *command* means a `set_streaming(False)` quietly does not happen.

## Calibration

Flat-floor tare: park on a clean, light, flat floor with nothing within \~0.5 m, record, and subtract the difference between what each bin measures and the ideal floor depth.

```
REx_line_sensor_calibrate --all
REx_line_sensor_calibrate -s sensor_1 --print-per-bin
REx_line_sensor_calibrate --all --dry-run
REx_line_sensor_calibrate --recompute <session_id>     # no robot needed
```

What it will and will not do:

* Status-code samples never enter the arithmetic. A bin that mostly returns 5.09/5.11 is rejected, not averaged.
* A run is **refused** if too many bins fail — a partial tare is worse than none, because nothing downstream can tell a bad correction from a good one. A dark or glossy floor produces exactly this.
* Each raw session is one `session.npz`, so a tare can be recomputed later without the robot.
* A stored tare records a **configuration fingerprint** (bus map, flip, report count, geometry, code mapping). On mismatch it is *refused*, never downgraded to a warning and never quietly replaced with an older file.

## Runtime control

```python
from stretch4_body.robot.robot_client import RobotClient
r = RobotClient(); r.startup()
ls = r.line_sensor_loop

ls.set_streaming(False)                  # pause all six
ls.set_sensor_enabled('sensor_3', False) # or just one
r.push_command()                         # nothing happens without this

ls.is_streaming(); ls.disabled_sensors(); ls.dead_sensors()
```

Neither setting persists. A restart always comes up streaming with all six enabled.

While paused the port is still read and the bytes discarded, so the kernel buffer cannot fill and hand back a corrupt part-report on resume. A disabled sensor is skipped *before* `json.loads`, which is where the reader's time actually goes, so it costs less rather than merely publishing less.

## Recovery

A serial error closes the port, marks every enabled sensor dead, and retries the open on a 0.5 → 5 s backoff, incrementing `reader_restarts` on success. Verified on hardware: deauthorizing the USB node drops the subsystem and it returns by itself within \~3 s.

Closing the port matters. It is opened `exclusive=True`, so a descriptor left open blocks every later reopen with "device busy" — which is how a single transient fault used to kill the subsystem until the next reboot, silently, while the process stayed alive.

## Tools

* `REx_line_sensor_calibrate` — flat-floor tare.
* `stretch_line_sensor_ranges` — live per-bin plot of what each bin reports. Status codes get their own coloured rows so 5.09 and 5.11 are distinguishable at a glance. `--calib` overlays the tare the body serves.
* `stretch_line_sensor_viz_3d` — interactive 3D view of the projected points, with clustering and cost-map overlays.

Both viewers run on the robot and need a display.

## Consumer checklist

* Read `codes`, never a float comparison, to ask what a bin was.
* Check `health['streaming']` and `health['disabled_sensors']` before trusting a clear floor. A disabled sensor reports nothing, which is not the same as reporting that nothing is there.
* Check `sensors_dead`.
* Use `is_sensor_updated(name)` to skip repeats: status publishes at 100 Hz while sensors report at \~30 Hz, so about 70% of polls carry no new frame.
* Apply the tare with `LineSensorLoopClient.apply_tare()` rather than subtracting offsets yourself — it leaves status-code bins untouched.


# Gamepad Teleop

## Gamepad Teleop for Stretch

Stretch 4 ships with a gamepad controller for teleoperation.

You can start the gamepad teleop script by running `stretch_gamepad_teleop` in the command line, or from the Stretch Tray icon in the system tray.

> Note: `stretch_gamepad_teleop` uses a RobotClient. Make sure the Stretch Body Server is running using `stretch_body_server --status`.

### Homing from Gamepad Teleop

When you boot Stretch 4, the robot needs to be homed (auto-calibrated). You may notice that the controller vibrates and does not allow you to move the robot. If this happens, make sure the robot's surroundings are clear, and press the `Start` button on the controller to Home (calibrate) the joints.

### Running in the background

You can keep the `stretch_gamepad_teleop` application script running in the background while you run other RobotClient applications. The `stretch_gamepad_teleop` script will timeout and stop sending commands to the server after a few milliseconds of not receiving inputs.&#x20;

> Note that `stretch_gamepad_teleop` has a higher priority on the RobotClient command queue, so commands from it will override other scripts by default.

### Controller Mappings

By default, `stretch_gamepad_teleop` has two control mapping modes that you can switch between using `Y`.&#x20;

The first is Joint Control, which allows you to control each joint individually.&#x20;

The second is Flying Gripper Control, which allows you to use the `Right Stick` to move the wrist, and move towards where the gripper is pointing using the `Left Stick`. This mode is useful for manipulation and grasping objects.

{% tabs %}
{% tab title="Joint Control Mode" %}

<figure><img src="/files/ja1Awc3VQuLu0zV7h7uS" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="Flying Gripper Mode" %}

<figure><img src="/files/HXGoieJ20MLht0Ppc7xI" alt=""><figcaption></figcaption></figure>
{% endtab %}
{% endtabs %}

### Changing Speed and Strength

Tap or hold the `Right Trigger + A` to change or cycle the speed of Stretch 4. By default it is Medium speed. This cycles between Low, Medium and High speed presets.&#x20;

> Be careful when navigating with Medium or High speed. It is a good idea to keep the arm retracted or Stow the robot (Hold `Select`) while the robot is moving in the environment.

Tap or hold the `Right Trigger + B` to change or cycle the strength of Stretch 4. By default it is Medium strength. This cycles between Low, Medium and High strength presets. Use Low strength when manipulating the robot near people, and High strength when trying to manipulate objects that require more force, like opening a door.

> Strength affects the contact sensitivity thresholds. Low strength will stop joint motion when encountering a small motor effort.

### Special Functions

* **Homing**: If the robot is not homed, press the `Start` button to home the robot.
* **Gripper Handedness**: If the robot is homed, hold the `Start` button for 3 seconds to switch gripper handedness (Left/Right).
* **Stowing**: Hold the `Select` button for 3 seconds to stow the robot.

## Customization

You can customize the behavior by:

* Modifying `gamepad_control_mappings.py` to add or edit mappings.
* Setting `params['function_cmd']` in user parameters to execute a shell command via the `X` button.


# Puppet Teleop

The `stretch_puppet_teleop.py` tool allows a user to control a remote Stretch robot (the "Puppet") by physically moving the joints of a local Stretch robot (the "Controller").

## 1. Overview and Architecture

When the script is launched, it connects to the local Controller robot and places its joints (lift, arm, wrist, base) into a backdrivable "freewheel" or "safety" mode. This allows a human operator to physically push and pull the robot's links. The script continuously reads the joint positions of the Controller at a high frequency (80 Hz) and streams them as motion commands to the remote Puppet robot over the network.

Additionally, the Controller tool supports a custom "Pistol Grip" hardware attachment (`/dev/hello-gripper-pistol`) with a slider potentiometer. This slider can be used to control the Puppet's gripper aperture, independent of the Controller's physical gripper state.

### Architecture Block Diagram

```mermaid
graph TD
    User([Human Operator]) -->|Physically moves links| Controller[Local Controller Robot]
    User -->|Slides potentiometer| Pistol[Gripper Pistol Slider]
    
    subgraph Controller Side
    Controller
    Pistol
    Script[stretch_puppet_teleop.py]
    end
    
    Pistol -->|Serial USB| Script
    Controller -->|Local ZMQ / Status Pull| Script
    
    subgraph Network
    ZMQ[TCP / ZMQ Connection]
    end
    
    Script -->|Motion Commands| ZMQ
    
    subgraph Puppet Side
    PuppetServer[Stretch Body Server]
    PuppetRobot[Remote Puppet Robot]
    end
    
    ZMQ --> PuppetServer
    PuppetServer -->|Actuates motors| PuppetRobot
    PuppetRobot -->|Interacts with| Environment([Physical Environment])
```

## 2. Controller Robot Setup

* 2 Stretch
* 1 controller Stretch with pistol gripper, and gripper mapped to /dev/hello-gripper-pistol
* 2nd Stretch with SG4 or PG4 tool

Add the following to `/etc/udev/rules.d/95-hello-arduino.rules` `KERNEL=="ttyACM*", ATTRS{idVendor}=="239a", ATTRS{idProduct}=="8101",MODE:="0666", SYMLINK+="hello-gripper-pistol", ENV{ID_MM_DEVICE_IGNORE}="1"`

Then run `sudo udevadm control --reload` and confirm that /dev/hello-gripper-pistol is there

## 2. Networking Setup

To successfully teleoperate the Puppet robot, both robots must be connected to the same network (e.g., the same Wi-Fi router or a direct Ethernet connection), and you must know the IP address of the Puppet robot.

**Step 1: Find the Puppet's IP Address** On the remote Puppet robot, open a terminal and run:

```bash
hostname -I
```

Alternatively, you can use `ip a` or `ifconfig`. Note the IPv4 address (e.g., `192.168.1.15`).

**Step 2: Start the Robot Servers** Both robots require the Stretch Body Server to be running.

* **On the Puppet robot:** Start the server in a terminal:

  ```bash
  stretch_body_server
  ```
* **On the Controller robot:** Start the server in a terminal:

  ```bash
  stretch_body_server
  ```

**Step 3: Run the Teleop Script** On the Controller robot, run the teleop script and provide the Puppet's IP address (see Usage Examples below).

## 3. Command Line Arguments

The `stretch_puppet_teleop.py` script provides several arguments to customize the teleoperation experience:

| Argument       | Description                                                                                                                                                 |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--puppet_ip`  | IP address of the remote Puppet robot running Stretch Body Server (e.g., `192.168.1.10`). Required unless `--no_puppet` is set.                             |
| `--joints`     | List of joints to mimic. Default is: `omnibase lift arm wrist gripper`. Example: `--joints lift arm`                                                        |
| `--no_puppet`  | Runs the script in a local-only testing mode without connecting to a puppet robot. Useful for verifying the controller's backdrivability and joint sensors. |
| `--no_pistol`  | Runs the script without attempting to connect to the pistol grip slider hardware.                                                                           |
| `--pg4`        | Indicates that the Puppet robot is equipped with a Parallel Gripper 4. Automatically maps standard gripper slider inputs to PG4 translation commands.       |
| `--pg4c`       | Indicates that the Controller robot is equipped with a Parallel Gripper 4.                                                                                  |
| `--print_only` | Connects to both robots and prints their joint positions to the terminal, but does *not* command any motion on the Puppet. Good for safe network testing.   |

> \[!IMPORTANT] The wrist\_pitch, wrist\_yaw, and wrist\_roll joints on the Controller robot must have `enable_torque_after_runstop: 0` configured in their `stretch_user_params.yaml`. This ensures the wrist remains backdrivable and doesn't snap to a position after a runstop event.

## 4. Usage Examples

Here are common ways to launch the teleoperation script. For these examples, we assume the Puppet robot's IP address is `192.168.1.15`.

**Standard Teleoperation** Connects to the puppet and starts mimicking all default joints, assuming the pistol grip slider is attached.

```bash
stretch_puppet_teleop.py --puppet_ip 192.168.1.15
```

**Teleoperation Without Pistol Grip** If you are using a standard Controller robot without the custom pistol grip hardware installed.

```bash
stretch_puppet_teleop.py --puppet_ip 192.168.1.15 --no_pistol
```

**Teleoperation with a Parallel Gripper (PG4)** If the remote Puppet robot has the Parallel Gripper 4 attached instead of the standard Stretch Gripper.

```bash
stretch_puppet_teleop.py --puppet_ip 192.168.1.15 --pg4
```

**Partial Teleoperation (Arm and Lift Only)** Only mirror the lift and arm extensions. The base, wrist, and gripper will remain stationary.

```bash
stretch_puppet_teleop.py --puppet_ip 192.168.1.15 --joints lift arm --no_pistol
```

**Safe Dry-Run** Connects to the Puppet over the network and displays the real-time joint positions of both robots in a terminal table, without actually moving the Puppet.

```bash
stretch_puppet_teleop.py --puppet_ip 192.168.1.15 --print_only
```

**Controller Hardware Test** Places the local Controller robot into backdrivable mode and prints joint values to the terminal. No network connection or Puppet robot is required.

```bash
stretch_puppet_teleop.py --no_puppet
```

**Base Rotation Only** Filters out translation commands from the Controller's base. Pushing the Controller base will only generate pure rotation commands on the Puppet.

```bash
stretch_puppet_teleop.py --puppet_ip 192.168.1.15 --base_rotate_only
```


# README

## Overview

This interface enables a user to remotely teleoperate a Stretch robot through a web browser. This website can be set up to teleoperate the robot remotely from anywhere in the world with an internet connection, or simply eyes-off teleop from the next room on a local network. The codebase is built on ROS2, WebRTC, Nav2, and TypeScript.

## Setup & Installation

The interface is compatible with the Stretch 4. It currently only supports Ubuntu 24.04 and ROS2 Humble. Upgrade your operating system if necessary ([instructions](https://docs.hello-robot.com/0.3/installation/robot_install/)) and create/update the Stretch ROS2 Humble workspace ([instructions](https://docs.hello-robot.com/0.3/installation/ros_workspace/)). This will install all package dependencies and install the web teleop interface.

## Launching the Interface

First, navigate to the folder containing the codebase using:

```
colcon_cd stretch4_web_teleop
```

Next, launch the interface:

```
./launch_interface.sh
```

In the terminal, you will see output similar to:

```
Visit the URL(s) below to see the web interface:
https://localhost/operator
https://192.168.1.14/operator
```

Look for a URL like `https://<ip_address>/operator`. Visit this URL in a web browser on your personal laptop or desktop to see the web interface. Ensure your personal computer is connected to the same network as Stretch. You might see a warning that says "Your connection is not private". If you do, click `Advanced` and `Proceed`.

Once you're done with the interface, close the browser and run:

```
./stop_interface.sh
```

**Note:** Only one browser can be connected to the interface at a time.

## Using the Interface Remotely

**WARNING: This is prototype code and there are security issues. Deploy this code at your own risk.**

We recommend setting up the interface for remote use using [ngrok](https://ngrok.com/docs/what-is-ngrok/). First, create an account with `ngrok` and follow the Linux installation instructions in the `Setup & Installation` tab in your ngrok account dashboard.

Navigate to the `Domains` tab and click `Create Domain`. ngrok will automatically generate a domain name for your free account. You will see a domain similar to `deciding-hornet-purely.ngrok-free.app`. Follow the interface launch instructions and then start the ngrok tunnel by running the following command (replace `<NGROK_DOMAIN>` with your account's domain and `user:password` with a secure username and password):

```
ngrok http --basic-auth="user:password" --domain=<NGROK_DOMAIN> 443
```

In your browser, open `https://<NGROK_DOMAIN>/operator` to see the interface. You will then be prompted to enter the appropriate username and password. Note, anyone in the world with internet access can open this link.

### Storing Ngrok Tunnel Configuration

To store this configuration, open the ngrok config file:

```
ngrok config edit
```

Add the following configuration to the file. Make sure to update `<NGROK_AUTH_TOKEN>`, `<NGROK_DOMAIN>`, and `admin:password` with the appropriate values.

```
authtoken: <NGROK_AUTH_TOKEN>
version: 2
tunnels:
    stretch-web-teleop:
        proto: http
        domain: <NGROK_DOMAIN>
        addr: 443
        basic_auth:
          - "admin:password"
        host_header: rewrite
        inspect: true
```

Now run `ngrok start stretch-web-teleop` to start the tunnel and navigate to `https://<NGROK_DOMAIN>/operator`. You will then be prompted to enter the appropriate username and password.

## Developer Docs

The following primers provide a high-level introduction to the codebase for developers.

| Primer                                                                          | Description                                                                                                                                                                                                                                                                       |
| ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Software Architecture](/stretch-4-web-teleop/src/primer_software_architecture) | Overview of the system architecture — how the robot browser, operator browser, ROS2, and WebRTC fit together. Includes a render logic flow diagram showing how `MobileOperator` is structured into scenes, function providers, and the global footer.                             |
| [Operator Page](/stretch-4-web-teleop/src/primer_operator)                      | Deep dive into the operator page codebase — directory layout, the shared layer (`commands`, `RemoteRobot`, `util`, `webrtcconnections`), the entry point (`index.tsx`), function providers, component folders, storage handler, and a step-by-step guide for adding new features. |
| [Robot Page](/stretch-4-web-teleop/src/primer_robot)                            | Deep dive into the robot page codebase — the `Robot` class, ROS2 subscriptions/services/actions, robot modes, joint state processing, video streams, and the full bidirectional data flow between ROS2 and the operator browser.                                                  |

## Contributing

* This repository uses pre-commit hooks to enforce consistent formatting and style.
  * Install pre-commit: `python3 -m pip install pre-commit`
  * Install the hooks locally: `cd` to the top-level of this repository and run `pre-commit install`.
  * Moving forward, pre-commit hooks will run before you create any commit.

## Troubleshooting

### Collecting logs

First, ensure that your robot has the latest version of Web Teleop by [updating your ROS workspace](https://docs.hello-robot.com/0.3/installation/ros_workspace/).

Then, launch the program normally, and if you see "FAILURE. COULD NOT LAUNCH WEB TELEOP.", then locate the zipped-up logs file and send them to Hello Robot Support (<support@hello-robot.com>).

To locate the logs, open a file explorer, go into "Home", go into "stretch\_user", go into "log", go into "web\_teleop", locate the folder with the latest timestamp, and send "stretch4\_web\_teleop\_logs.zip" to the support team.

## Licenses

The following license applies to the contents of this directory written by Vinitha Ranganeni, Noah Ponto, authors associated with the University of Washington, and authors associated with Hello Robot Inc. (the "Contents"). This software is intended for use with Stretch ® mobile manipulators produced and sold by Hello Robot ®.

Copyright 2023 Vinitha Ranganeni, Noah Ponto, the University of Washington, and Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

\============================================================

Some of the contents of this directory derive from the following repositories:

<https://github.com/hello-robot/stretch\\_web\\_teleop>

<https://github.com/hello-robot/stretch\\_web\\_interface>

<https://github.com/hcrlab/stretch\\_web\\_interface>

<https://github.com/hcrlab/stretch\\_teleop\\_interface>

Text from relevant license files found in these repositories.


# LICENSE

From <https://github.com/hello-robot/stretch\\_web\\_interface/blob/master/LICENSE.md>

The following license applies to the contents of this directory created by Hello Robot Inc. (the "Contents"), but does not cover materials from other sources. This software is intended for use with the Stretch RE1 mobile manipulator, which is a robot produced and sold by Hello Robot Inc.

Copyright 2020 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# WEBRTC\_PROJECT\_LICENSE

From <https://github.com/hello-robot/stretch\\_web\\_interface/blob/master/WEBRTC\\_PROJECT\\_LICENSE.md>

The following license covers the original code from which some of the web interface code was derived (e.g., operator\_acquire\_av.js, robot\_acquire\_av.js). The original code was released in the following repository, which contains WebRTC example code.

<https://github.com/webrtc/samples>

\======================================

Copyright (c) 2014, The WebRTC project authors. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


# src


# Primer: Operator Page

The **operator page** is the browser-based UI through which a user teleoperates the Stretch 4 robot. It connects to the robot browser over WebRTC, receives sensor/camera data, and sends movement commands back.

> For a worked example of adding a brand-new feature end-to-end (the "Home the Robot" button), see [`documentation/development.md`](https://github.com/hello-robot/stretch4_web_teleop/blob/main/documentation/development.md).

***

## Directory Layout

```
src/pages/operator/
├── html/                        # Entry HTML page
├── css/                         # Page and component stylesheets
├── icons/                       # SVG icon assets
└── tsx/                         # All TypeScript/React source
    ├── index.tsx                # Module entry point — bootstraps WebRTC and renders root
    ├── MobileOperator.tsx       # Mobile operator root component
    ├── function_providers/      # Logic layer connecting UI to the robot
    ├── layout_components/       # Composite, stateful UI components
    ├── basic_components/        # Small, reusable UI primitives
    ├── static_components/       # Mostly-fixed UI panels (header, sidebar, etc.)
    ├── storage_handler/         # Layout persistence (local or Firebase)
    ├── utils/                   # Shared types, enums, and small utilities
    └── default_layouts/         # JSON default layout configs
```

***

## Shared Layer (`src/shared/`)

Before describing the operator page itself, it helps to understand the shared layer it builds on. These four files define the common language between the two browser peers.

### `commands.tsx`

This file defines the common language between the operator and robot browsers. It defines every **command** the interface can transmit — the full vocabulary of robot actions. For example, you can ask Nav2 to navigate the robot using the MoveBaseCommand, or you can ask the driver for the battery's current voltage using GetBatteryVoltageCommand. Every action taken in the web interface is converted into a command and transmitted through WebRTC's data channel to the robot browser, where it is interpreted and further transmitted to Stretch's ROS2 packages.

Each command is a TypeScript interface with a `type` discriminant:

```ts
export interface HomeTheRobotCommand {
    type: "homeTheRobot";
}
```

All commands are collected into the `cmd` union type, which is what WebRTC's data channel actually carries:

```ts
export type cmd = DriveCommand | IncrementalMove | HomeTheRobotCommand | ...;
```

**When adding a new capability**, define its command here first.

### `remoterobot.tsx`

This file wraps the logic for receiving and transmitting commands, sensor streams, etc. over the WebRTC channels in a nice API for the operator browser. You can think of `RemoteRobot` as the bridge between the operator browser and the robot. The interface creates a single instance of `RemoteRobot` and uses it for the duration of the session that it is connected to the robot. On the other end, the robot browser is listening for commands from / transmitting data to `RemoteRobot`. Each method packages a command and sends it through the WebRTC data channel:

```ts
homeTheRobot() {
    let cmd: HomeTheRobotCommand = { type: "homeTheRobot" };
    this.robotChannel(cmd);
}
```

**When adding a new capability**, add a method to `RemoteRobot` that dispatches the new command.

### `util.tsx`

This file contains shared custom message types, variables, and enums For example, this file defines the WebRTC message types:

```js
export type WebRTCMessage =
    ...
    | cmd;
```

`cmd` is imported from `commands.tsx`. This means all commands defined in `commands.tsx` can be transmitted over WebRTC. You do not need to edit this file for the homing functionality, you just need to define your command as described [in this section](#commandstsx).

You generally do not need to edit this file unless adding a new WebRTC message type.

### `webrtcconnections.tsx`

*Note: You should never touch this file!*

This file contains the code used for establishing a peer connection between the operator and robot browser and setting up data channels such that they can communicate.

***

## Entry Point: `tsx/index.tsx`

This file is the module entry. It:

1. **Creates** all `FunctionProvider` singletons (see below).
2. **Establishes** the WebRTC connection to the robot browser.
3. **Handles two WebRTC callbacks**:
   * `handleRemoteTrackAdded()` — subscribes to video/audio media channels so they feed the camera views.
   * `handleWebRTCMessage()` — a switch statement that routes incoming robot data to the right function provider or state variable:

     ```ts
     case "isRunStopped":
         remoteRobot.sensors.setRunStopState(message.enabled);
     case "moveBaseState":
         underMapFunctionProvider.setMoveBaseState(message.message);
     case "occupancyGrid":
         // chunked and assembled into occupancyGrid module-level var
     ```
4. **Calls** `initializeOperator()` once the data channel is open, which:
   * Creates the `RemoteRobot` instance and sets up its sensor callbacks.
   * Creates the `StorageHandler` (loads the saved layout).
   * Instantiates providers that require storage (e.g. `MovementRecorderFunctionProvider`, `UnderMapFunctionProvider`, etc.).
   * Renders `MobileOperator` (Note, we currently only have the mobile version of the operator page. On desktop, the mobile operator page is scaled up to fit the screen).

Key module-level exports that components import directly (no prop-drilling):

| Export                             | Type                               | Description                 |
| ---------------------------------- | ---------------------------------- | --------------------------- |
| `buttonFunctionProvider`           | `ButtonFunctionProvider`           | Main movement logic         |
| `runStopFunctionProvider`          | `RunStopFunctionProvider`          | Run-stop state              |
| `batteryVoltageFunctionProvider`   | `BatteryVoltageFunctionProvider`   | Battery display             |
| `movementRecorderFunctionProvider` | `MovementRecorderFunctionProvider` | Playback/recording          |
| `underMapFunctionProvider`         | `UnderMapFunctionProvider`         | Autonomous navigation       |
| `homeTheRobotFunctionProvider`     | `HomeTheRobotFunctionProvider`     | Homing sequence             |
| `cameraSwitcherFunctionProvider`   | `CameraSwitcherFunctionProvider`   | Camera perspective          |
| `occupancyGrid`                    | `ROSOccupancyGrid \| undefined`    | Current map data            |
| `stretchTool`                      | `StretchTool`                      | Currently mounted tool      |
| `storageHandler`                   | `StorageHandler`                   | Persisted layout/recordings |

***

## Root Components

### `MobileOperator.tsx`

The active root component rendered on the user's device. Renders two the **Pilot Mode** scene by default (live camera + robot controls) and the **Auto Nav** scene can but accessed from the scene menu (map-based autonomous navigation).

***

## Function Providers (`tsx/function_providers/`)

The **function provider** pattern is the core architectural idea: React components are kept logic-free. Each component is given a function provider object at render time, and calls methods on it in response to user interactions. Components define an enum naming the actions they need, and the provider returns an anonymous function for each:

```ts
// In the component:
export enum HomeTheRobotFunction { Home }

// In the provider:
public provideFunctions(fn: HomeTheRobotFunction) {
    switch (fn) {
        case HomeTheRobotFunction.Home:
            return () => { FunctionProvider.remoteRobot?.homeTheRobot(); };
    }
}

// In the component's render:
const Home = homeTheRobotFunctionProvider.provideFunctions(HomeTheRobotFunction.Home);
<button onClick={Home}>Home</button>
```

### `FunctionProvider` (base class)

All providers extend this. It holds:

* `static remoteRobot` — the single shared `RemoteRobot` instance (set via `addRemoteRobot()`)
* `static velocityScale`, `actionMode`, `pilotControlsCurrent` — global control settings shared across all providers
* `setBaseVelocity()` / `incrementalJointMovement()` / `continuousJointMovement()` — shared motion helpers that manage the velocity heartbeat interval (sent every 25 ms)
* `stopCurrentAction()` — cancels any active velocity send loop or timeout

### `ButtonFunctionProvider`

The heaviest provider. Maps every `ButtonPadButton` enum value to robot actions. Handles:

* Different **action modes**: `StepActions` (tap once, move discrete amount), `PressRelease` (hold for continuous), `ClickClick` (tap to start, tap to stop)
* **Joint state callbacks** from the robot → updates which buttons are at their limits/in collision → updates `ButtonStateMap` for the UI
* Drive, arm, lift, wrist, gripper, head, and camera-perspective commands

### `MovementRecorderFunctionProvider`

Manages recording and playing back sequences of joint poses. Interfaces with `StorageHandler` to persist recordings. Streams playback state back via operator callback.

### `UnderMapFunctionProvider`

Handles autonomous move-base navigation. Sends navigation goals via `RemoteRobot`, receives action state feedback, and exposes an operator callback for alert rendering.

### `HomeTheRobotFunctionProvider`

Tracks homing and robot mode state. Exposes `updateModeState()` and `updateIsHomedState()` for the WebRTC message handler to call.

### `RunStopFunctionProvider`

Tracks the software run-stop state. Exposes `updateRunStopState()`.

### `BatteryVoltageFunctionProvider`

Tracks battery voltage. Exposes `updateVoltage()`.

### `MapFunctionProvider`

Handles click-on-map → navigation goal conversion.

### `CameraSwitcherFunctionProvider`

Manages which camera perspective is active.

***

## Component Folders

There are three component folders, each with a different intended scope:

* **`layout_components/`** — customizable components that form the interactive body of the interface (camera views, button pads, map, movement recorder). In the desktop `Operator`, these can be added, removed, or rearranged by toggling "Customize".
* **`static_components/`** — fixed components found in the header, footer, or sidebar (speed controls, action mode selector, run-stop, occupancy grid canvas, etc.). They cannot be moved by the user.
* **`basic_components/`** — generic, robot-agnostic UI primitives (dropdowns, modals, carousels, tab groups) used to build the above two.

### Notable Layout Components

| Component          | Purpose                                                                              |
| ------------------ | ------------------------------------------------------------------------------------ |
| `PilotMode`        | Top-level pilot scene: camera view + controls tab group + movement recorder + footer |
| `SimpleCameraView` | Renders a single camera video stream                                                 |
| `GripperCamPIP`    | Gripper camera picture-in-picture overlay                                            |
| `ButtonPad`        | Grid of robot control buttons; receives functions from `ButtonFunctionProvider`      |
| `MovementRecorder` | Feature for recording, naming, editing, and replaying pose sequences                 |
| `AutoNav`          | Map view + click-to-navigate goal setting                                            |
| `FooterPilotMode`  | Bottom toolbar: camera switcher, action speed, action mode, scene navigation         |
| `FooterAutoNav`    | Bottom toolbar in auto-nav scene: goal input, move-base controls                     |
| `FooterGlobal`     | Persistent bottom bar switching between pilot/auto-nav scenes                        |
| `HomeTheRobot`     | Prominent banner shown when robot is un-homed, with a "Home" button                  |

### Notable Static Components

| Component             | Purpose                                                                                |
| --------------------- | -------------------------------------------------------------------------------------- |
| `ActionMode`          | Dropdown: Step Actions / Press-Release / Click-Click                                   |
| `ActionSpeed`         | Speed selector (Slowest → Fastest)                                                     |
| `PilotControlsToggle` | Switches between button pad layouts (Omni Drive, Arm, etc.)                            |
| `RunStop`             | Software run-stop indicator/button                                                     |
| `OccupancyGrid`       | Canvas-based 2D map renderer (CreateJS); robot marker, goal marker, saved pose markers |
| `CameraSwitcher`      | Buttons to switch camera perspective                                                   |

### Notable Basic Components

| Component           | Purpose                                         |
| ------------------- | ----------------------------------------------- |
| `SceneCarousel`     | Horizontally scrollable scene switcher carousel |
| `PlaybackStatusbar` | Progress bar for movement playback              |
| `Flex`              | Lightweight flexbox layout wrapper              |

***

## Storage Handler (`tsx/storage_handler/`)

Persists the operator's layout and recordings across sessions. Two backends:

* **`LocalStorageHandler`** — uses browser `localStorage`; default for on-robot use.
* **`FirebaseStorageHandler`** — syncs to Firestore; used when `process.env.storage === "firebase"`.

Both implement the common `StorageHandler` interface.

***

## Utils (`tsx/utils/`)

| File                        | Contents                                                                                                                                  |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `component_definitions.tsx` | Enums and types for the layout system: `ComponentType`, `CameraViewId`, `ActionModeType`, `LayoutDefinition`, `ComponentDefinition`, etc. |
| `genUUID.tsx`               | Thin wrapper around the uuid library                                                                                                      |
| `hex-to-rgb-array.tsx`      | `#rrggbb` → `[r, g, b]` color conversion                                                                                                  |
| `svg.tsx`                   | Inline SVG strings for all button pad icons (arm directions, gripper, head, etc.)                                                         |

***

## Full Data Flow

```
WebRTC media channels
        │
        ▼
  handleRemoteTrackAdded()      ← feeds camera <video> elements

WebRTC data channel (robot → operator)
        │
        ▼
  handleWebRTCMessage()         ← index.tsx switch statement
        │
  ┌─────┴──────────────────────────────────────────────┐
  │ joint states   → buttonFunctionProvider            │
  │ battery        → batteryVoltageFunctionProvider    │
  │ mode/homed     → homeTheRobotFunctionProvider      │
  │ run-stop       → runStopFunctionProvider           │
  │ occupancy grid → occupancyGrid (module-level var)  │
  │ move-base      → underMapFunctionProvider          │
  │ playback       → movementRecorderFunctionProvider  │
  └────────────────────────────────────────────────────┘
        │
        ▼
  React components re-render (via provider callbacks)
        │
  User interaction (button press, map click, etc.)
        │
        ▼
  FunctionProvider.provideFunctions(SomeEnum.Action)()
        │
        ▼
  RemoteRobot.someAction()   →   cmd object
        │
        ▼
  WebRTC data channel (operator → robot)
        │
        ▼
  robot/index.tsx handleMessage() switch
        │
        ▼
  Robot.executeX() → ROSLib → rosbridge → ROS2
```

***

## How to Add a New Feature (Summary)

1. **`shared/commands.tsx`** — define a new `interface MyCommand { type: "myCommand"; ... }` and add it to the `cmd` union.
2. **`shared/remoterobot.tsx`** — add a method `myAction() { this.robotChannel({ type: "myCommand" }); }`.
3. **`robot/tsx/index.tsx`** — add a `case "myCommand": robot.myAction(); break;` in `handleMessage()`.
4. **`robot/tsx/robot.tsx`** — implement `myAction()` using ROSLib (service, topic, or action client).
5. **`operator/tsx/function_providers/MyFunctionProvider.tsx`** — extend `FunctionProvider`, implement `provideFunctions()`.
6. **`operator/tsx/index.tsx`** — instantiate the new provider and wire up any WebRTC message callbacks.
7. **Component** — define the `enum MyFunction` and render a component that calls `myFunctionProvider.provideFunctions(...)`.

***

## Key Design Decisions

**Function providers are singletons, not React context.** Components import them directly from `index.tsx`. This avoids prop-drilling but means the providers are module-global — they exist for the lifetime of the page.

**Action modes are global, not per-component.** `FunctionProvider.actionMode` is a static field. All button pads share the same mode.

**The operator does not talk to ROS directly.** All ROS communication goes through the robot browser (`robot/tsx/robot.tsx` ↔ ROSLib ↔ rosbridge). The operator only speaks to the robot browser through `remoterobot.tsx` and WebRTC.


# Primer: Robot Page

The **robot page** is the headless browser that runs onboard the Stretch robot. It has no visible UI — its job is to bridge ROS2 (running on the robot) and the operator browser (running on a remote device) over WebRTC. When the interface is launched, this page starts first, connects to `rosbridge`, and waits for an operator to join.

> For a high-level view of how the robot and operator browsers fit together, see [`primer_software_architecture.md`](/stretch-4-web-teleop/src/primer_software_architecture).

***

## Directory Layout

```
src/pages/robot/
├── html/                  # Entry HTML page (headless, no visible UI)
├── css/                   # Minimal stylesheet
└── tsx/
    ├── index.tsx          # Module entry point — bootstraps Robot, WebRTC, and media streams
    ├── robot.tsx          # Robot class — all ROS2 interaction lives here
    ├── videostreams.tsx   # VideoStream class — decodes compressed images and produces a MediaStream
    └── audiostreams.tsx   # AudioStream class — captures microphone audio for WebRTC
```

***

## Entry Point: `tsx/index.tsx`

This file is the module entry. It wires together the three main subsystems — `Robot`, `WebRTCConnection`, and media streams — and manages the lifecycle of a session.

### Startup Sequence

1. **Instantiates `Robot`** with a set of forwarding callbacks (one per ROS2 data stream). Each callback calls `connection.sendData()` to forward the data to the operator browser over WebRTC.
2. **Instantiates `WebRTCConnection`** as the robot peer.
3. **Instantiates media streams**: `navigationStream`, `gripperStream` (both `VideoStream`), and `audioStream` (`AudioStream`).
4. **Sets `onRosConnectCallback`** — once ROS is ready, this callback:
   * Subscribes to the camera topics and starts the video streams.
   * Fetches the occupancy grid map (if there is one).
   * Logs into the signaling server and joins the robot room, then waits for an operator.
5. **Calls `robot.connect()`** to initiate the rosbridge WebSocket connection.

### Session Lifecycle (`handleSessionStart`)

Called by `WebRTCConnection` when an operator joins. It:

1. Removes any existing media tracks from the peer connection.
2. Adds the head cameras and gripper cameras to the peer connection.
3. Opens the WebRTC data channels for bidirectional command/data flow.

### Incoming Message Handler (`handleMessage`)

A switch statement that routes every incoming `WebRTCMessage` from the operator browser to the appropriate `Robot` method:

| Message type           | Robot method called                                          |
| ---------------------- | ------------------------------------------------------------ |
| `driveBase`            | `executeBaseVelocity(modifier)`                              |
| `setJointVelocity`     | `setJointVelocity(jointName, velocity)`                      |
| `incrementalMove`      | `executeIncrementalMove(jointName, increment)`               |
| `stopTrajectory`       | `stopTrajectoryClient()`                                     |
| `stopMoveBase`         | `stopMoveBaseClient()`                                       |
| `setRobotMode`         | `switchToNavigationMode()` / `switchToPositionMode()`        |
| `setCameraPerspective` | `useLeftCamera()` / `useRightCamera()` / `useCenterCamera()` |
| `setRobotPose`         | `executePoseGoal(pose)`                                      |
| `playbackPoses`        | `executePoseGoals(poses, 0)`                                 |
| `moveBase`             | `executeMoveBaseGoal(pose)`                                  |
| `setExpandedGripper`   | `setExpandedGripper(toggle)`                                 |
| `setRunStop`           | `setRunStop(toggle)`                                         |
| `getOccupancyGrid`     | `getOccupancyGrid()`                                         |
| `getStretchTool`       | `getStretchTool()`                                           |
| `homeTheRobot`         | `homeTheRobot()`                                             |

### Forwarding Callbacks

Each of these functions is registered with `Robot` at startup. When the robot publishes new data, `Robot` calls the callback, which packages the data and sends it to the operator browser via `connection.sendData()`:

| Callback               | WebRTC message type sent                          |
| ---------------------- | ------------------------------------------------- |
| `forwardJointStates`   | `validJointState` (robotPose, limits, collision)  |
| `forwardBatteryState`  | `batteryVoltage`                                  |
| `forwardOccupancyGrid` | `occupancyGrid` (chunked into 50k-element slices) |
| `forwardActionState`   | `moveBaseState` or `playbackPosesState`           |
| `forwardAMCLPose`      | `amclPose`                                        |
| `forwardMode`          | `mode`                                            |
| `forwardIsHomed`       | `isHomed`                                         |
| `forwardIsRunStopped`  | `isRunStopped`                                    |
| `forwardStretchTool`   | `stretchTool`                                     |

> **Note on occupancy grid chunking:** Map data can be very large. `forwardOccupancyGrid` splits the `data` array into 50,000-element slices and sends each as a separate `occupancyGrid` message. The operator browser reassembles them by concatenating the `data` arrays.

***

## `robot.tsx` — The `Robot` Class

`Robot` is the single source of truth for all ROS2 communication. It owns every ROSLib client (topics, services, action clients) and exposes a clean set of methods that `index.tsx` calls in response to operator commands.

### ROS Connection

```
robot.connect()
  └── new Ros({ url: "wss://localhost:9090" })
        ├── on "connection" → checkROSConnection() → onConnect()
        ├── on "error"      → reconnect()
        └── on "close"      → reconnect()
```

`checkROSConnection()` verifies that the two required camera topics have publishers before proceeding. This guards against timing issues where rosbridge connects before all required ROS nodes have started. If the check fails, it reconnects after 1 second.

`onConnect()` sets up all ROSLib clients:

* **Subscriptions**: joint states, joint limits, battery state, mode, is-homed, is-run-stopped, action results
* **Action clients**: `follow_joint_trajectory`, `navigate_to_pose`
* **Topics**: `/cmd_vel` (base velocity), `/joint_vel` (joint velocity)
* **Services**: camera switcher services, expanded gripper, run-stop, home the robot
* **Params**: `stretch_tool`, `mode` (read/write via rosbridge parameter API)

### Robot Modes

Stretch operates in one of three modes, controlled by writing to the `/stretch_driver:mode` ROS parameter:

| Mode         | Description                                                                       |
| ------------ | --------------------------------------------------------------------------------- |
| `position`   | Default. Position commands to arm; position commands to base.                     |
| `navigation` | Needed for trajectory goals. Position commands to arm; velocity commands to base. |
| `velocity`   | Velocity commands to both arm and base. Used for continuous jogging.              |

Methods `switchToNavigationMode()`, `switchToPositionMode()`, `switchToVelocityMode()` guard against redundant mode changes using the module-level `robotMode` variable.

### Movement Methods

| Method                               | ROS interface                                        | Notes                             |
| ------------------------------------ | ---------------------------------------------------- | --------------------------------- |
| `executeBaseVelocity(props)`         | Publishes `Twist` to `/cmd_vel`                      | Switches to velocity mode first   |
| `setJointVelocity(joint, vel)`       | Publishes `JointJog` to `/joint_vel`                 | Switches to velocity mode first   |
| `executeIncrementalMove(joint, inc)` | Sends goal to `/follow_joint_trajectory`             | Adds `inc` to current joint value |
| `executePoseGoal(pose)`              | Sends goal to `/follow_joint_trajectory`             | Switches to navigation mode       |
| `executePoseGoals(poses, idx)`       | Sends multi-point goal to `/follow_joint_trajectory` | Used for movement playback        |
| `executeMoveBaseGoal(pose)`          | Sends goal to `/navigate_to_pose`                    | Nav2 autonomous navigation        |
| `stopTrajectoryClient()`             | Cancels active trajectory goal                       | Fires `Cancel` callback           |
| `stopMoveBaseClient()`               | Cancels active move-base goal                        | —                                 |
| `homeTheRobot()`                     | Calls `/home_the_robot` service                      | `std_srvs/Trigger`                |

## Full Data Flow (Robot Page)

```
ROS2 (rosbridge wss://localhost:9090)
        │
        ▼
  Robot subscriptions / service calls
        │
  ┌─────┴──────────────────────────────────────────────┐
  │ /joint_states     → forwardJointStates             │
  │ /battery          → forwardBatteryState            │
  │ /mode             → forwardMode                    │
  │ /is_homed         → forwardIsHomed                 │
  │ is_runstopped     → forwardIsRunStopped            │
  │ /navigate_to_pose → forwardActionState             │
  │ AMCL TF           → forwardAMCLPose               │
  │ map_server/map    → forwardOccupancyGrid (chunked) │
  └────────────────────────────────────────────────────┘
        │
        ▼
  connection.sendData()  →  WebRTC data channel  →  Operator browser

  Operator browser  →  WebRTC data channel  →  handleMessage()
        │
        ▼
  robot.executeX() / robot.setX() / robot.getX()
        │
        ▼
  ROSLib → rosbridge → ROS2 (topics, services, actions)
        │
        ▼
  Camera topics → VideoStream.updateImage() → MediaStream
        │
        ▼
  WebRTC media channel → Operator browser camera views
```

***

## How to Add a New Robot Capability (Summary)

1. **`shared/commands.tsx`** — define `interface MyCommand { type: "myCommand"; ... }` and add it to the `cmd` union.
2. **`shared/remoterobot.tsx`** — add `myAction() { this.robotChannel({ type: "myCommand" }); }`.
3. **`robot/tsx/robot.tsx`** — implement the ROS2 interaction (subscribe to a topic, call a service, send an action goal). If the robot needs to push data back, accept a callback in the constructor and call it from the subscription.
4. **`robot/tsx/index.tsx`**:
   * Add a forwarding callback that calls `connection.sendData()` if data flows robot → operator.
   * Add a `case "myCommand": robot.myAction(); break;` in `handleMessage()` for operator → robot commands.
   * Pass the forwarding callback into the `Robot` constructor.
5. **Operator side** — update `operator/tsx/index.tsx` to handle the new WebRTC message type and wire it to the appropriate function provider.


# Software Architecture

![Software Architecture](/files/qR7bJnKewGLI7Rdit43f)

Stretch Web Teleop utilizes `ROS2`, `WebRTC` (web real-time communication), `NodeJS`, and `TypeScript`. The system runs in a headless browser onboard the robot. The robot browser has access to the robot via `ROS2`, however, the operator can only send commands or receive information indirectly through the robot browser. We utilize WebRTC to establish a peer connection between the operator browser, which loads the interface, and the robot browser. The robot browser uses `rosbridge` to connect to the robot via `ROS2`. `rosbridge` translates `JSON` messages from the robot browser to `ROS2` messages and vice versa.

When the interface is launched on the robot, the `ROS2` drivers and the robot browser are launched. The robot browser creates and joins a `WebSocket` room and waits for an operator to join. In a browser, the user can either go to an IP address when on the same network as the robot or a URL when accessing it remotely. We recommend using `Ngrok` to establish a secure tunnel over the internet for remote use (see instructions here). When the user navigates to the IP address or URL, the operator browser joins the `WebSocket` room created by the robot browser and a peer connection is established.

> ***NOTE:*** Only one peer connection between the operator and robot browser can be established. If another operator attempts to open the interface, the connection will be rejected.

Once a peer connection is established, the interface will render the default layout (see [render logic flow](#render-logic-flow) for more details) on the operator browser and the user will be able to control the robot.

For example, assume the user clicks a button to drive the robot forward, the command is sent to the robot browser. This command is passed through rosbridge which translates the `JSON` message into a `ROS2` message. The robot browser can also send information, such as joint limits and collision information, to the operator browser. When the user closes the browser, the peer connection is disconnected and another user can connect to the interface.

## Render Logic Flow

![Render Logic Flow](/files/9ykMM3Arb70Kvfdetdjg)

The `StorageHandler` persists and retrieves the operator's `layout` — including action mode, speed, and pilot controls — and passes it to `MobileOperator` on startup. `MobileOperator` is the root React component and manages all top-level state (button collisions, camera selection, velocity scale, active scene, etc.). It initializes a set of **Function Providers** (e.g. `ButtonFunctionProvider`, `RunStopFunctionProvider`, `BatteryVoltageFunctionProvider`) that abstract the communication layer between UI components and the remote robot, exposing functions and firing callbacks back into `MobileOperator` when robot state changes.

`MobileOperator` renders a **Scenes** container that holds the currently active scene alongside `FooterGlobal`. Each scene (currently `PilotMode` and `AutoNav`) is a self-contained view: `PilotMode` hosts the overhead camera, drive controls, movement recorder, and gripper picture-in-picture, while `AutoNav` hosts the occupancy-grid map and autonomous navigation controls. `FooterGlobal` is always visible and provides the **Scene Switcher** (via `MainMenu`) to transition between scenes, a **Battery Indicator**, and the **Run-Stop** button.


# pages


# operator


# tsx


# Firebase

Firebase is a set of application development platforms and backen cloud computing services. We will be using Firebase's Realtime Database for data storage.

## Setting up Firebase

### Creating a Firebase Project

Sign into [Firebase](https://firebase.google.com/) with your google account then open the Firebase [console](https://console.firebase.google.com/) and create a new project. The project will default to using the no-cost [Spark plan](https://firebase.google.com/pricing?hl=en\&authuser=1).

Add a web app to your firebase project. You shouldn't need to worry about installing the Firebase SDK because it is already in the `package.json` dependencies for this repo. This will generate a configuration for your web app that looks something like this:

```
const firebaseConfig = {
  apiKey: ...,
  authDomain: ...,
  projectId: ...,
  storageBucket: ...,
  messagingSenderId: ...,
  appId: ...,
  measurementId: ...
};
```

Create a file named `.env` in `stretch-web-interface` and add the config to the `.env` file. The config will need to be reformatted slightly so the contents of `.env` look like this:

```
apiKey=DEzaSyAzZEQ89KBuKXgKJ-UWV9vm3xM
authDomain=stretch-teleop-interface.firebaseapp.com
projectId=stretch-teleop-interface
storageBucket=stretch-teleop-interface.appspot.com
messagingSenderId=124457856584
appId=1:364440456284:web:1e2603a456f839280det99
measurementId=T-6GMDF5W03Z
```

### Setup the Realtime Database

Select the `Realtime Database` option under Build in the Firebase console for your project, then create a database. Select "Start in **locked mode**" in `Security Rules` and click `Enable`. Add the following to the database rules:

```
{
    "rules": {
        ".write": "auth.token.email == 'user1@example.com' || auth.token.email == 'user2@example.com'",
        "users" : {
	        "$user_id" : {
            ".write": "$user_id === auth.uid",
            ".read": "$user_id === auth.uid"
          }
        },
        "layouts": {
        	".read": "auth != null"
        },
        "currentLayouts": {
        	".read": "auth != null"
        }
    }
}
```

Replace `'user1@example.com'` and `'user2@example.com'` with the email addresses of the users you'd like to give write access to. You can add as many users as you'd like by separating them with `||`.

### Setup Authentication

Select the `Authentication` option under Build in the Firebase console for your project, then click `Get Started`. Click `Email/Password` and enable it. Do not enable passwordless sign-in. Click `Add new Provider` and `Anonymous` then enable it and click `Save`. Finally, add another provider, click `Google` and add a `Project public-facing name`, select a support email and click `Save`. We will primarily be using `Google` for authentication.


# Overview

This package provides robot description and mesh files for Stretch 4, as well as code for manipulating the kinematic description (e.g. adding virtual joints) and exporting for use in other programs (e.g. ROS2, pinocchio). The repository for Stretch 3 and earlier hardware versions can be found in [stretch\_urdf](https://github.com/hello-robot/stretch_urdf). This package can be installed by:

```
python3 -m pip install -U hello-robot-stretch4-urdf
```

<div align="center"><img src="/files/n1rxMzZ5Obl4vhsIdCEP" alt="Stretch 4 robot model" width="50%"></div>

<p align="center"><br></p>

## Details

The URDF and meshes are installed to your Python site directory. You can load them dynamically using:

```python
from stretch4_urdf import get_urdf

# configure the specifications for your robot
model_name = "SE4"
batch_name = "francis"
tool_name = "eoa_wrist_dw4_tool_sg4"

print(get_urdf(
    model_name=model_name,
    batch_name=batch_name,
    tool_name=tool_name,
))
```

Currently supported options for the `model_name`, `batch_name`, and `tool_name` are:

```python
supported_model_names = ["SE4"]
supported_batch_names = ["francis"]
supported_tool_names = ["eoa_wrist_dw4_tool_sg4"]
```

A single composable xacro combines a robot's base model with its attached tool. Util functions will handle processing the xacro file and return the URDF for the robot's current configuration:

```python
from stretch4_urdf import get_urdf_from_robot_params
urdf_string = get_urdf_from_robot_params() # store the urdf contents in a str variable
urdf_filepath = get_urdf_from_robot_params(out_dir="/tmp") # save the urdf to a file in the /tmp directory
```

You can also load accessory URDFs via:

```python
from stretch4_urdf import get_accessory

dock_urdf = get_accessory("docking_station")
print(dock_urdf)
```

Utility methods are provided to extract 4x4 coordinate transforms:

```python
from stretch4_urdf import get_transform

# Get the 4x4 transform between two frames
T_dock_to_leftaruco = get_transform(dock_urdf, frame_to="left_aruco_marker_link", frame_from="docking_station_link")
print(T_dock_to_leftaruco)
```

## Tools

### URDF Visualization

The `stretch_urdf_viz` tool allows you to visualize the robot's URDF in [Rerun](https://rerun.io/).

```bash
stretch_urdf_viz
```

By default, the tool will visualize the robot's nominal (uncalibrated) URDF. If the robot has been calibrated, the calibrated URDF will be automatically overlaid in green.

Key features:

* **Calibrated vs Uncalibrated**: Automatically shows both if calibration data is available.
* **Toggles**: Meshes, coordinate frames (TFs), and labels are organized into separate trees in Rerun, allowing them to be toggled on/off easily to reduce clutter.

Options:

* `--model`: Specify the robot model (e.g., `SE4`).
* `--batch`: Specify the robot batch (e.g., `francis`).
* `--tool`: Specify the end-of-arm tool (e.g., `eoa_wrist_dw4_tool_sg4`).

## Bringing in URDF's from CAD

The package follows a strict directory strucutre:

```yaml
> {model}_{batch}
    > meshes
        visual and collision mesh STL files
    > xacro
        stretch_main.xacro
```

```yaml
> {model}_tools
    > {tool_name}
        > meshes
            visual and collision mesh STL files
        {tool_name}.urdf
```

The URDF and meshes from the CAD model are added to the {model}\_{batch} base directory and meshes folder. From there, `utils/preprocessing/process_new_robot_model.py` creates the stretch\_main.xacro file and necessary collision meshes for the batch. See the urdf\_conventions.md for the specifics of this processing step.

A directory is created for each new tool, with the urdf in the base directory and a folder for meshes. `utils/preprocessing/process_new_tool.py` is then used to add the necessary edits to the urdf and generate collision meshes.

## Documentation

The following files provide deeper documentation on various parts of the system:

| Primer                                                                                                             | Description                                                                                               |
| ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| [Batches](/stretch4_urdf/stretch4_urdf/batches)                                                                    | Explains URDF batch organization, compiling the URDF dynamically, and how to add new batch models.        |
| [End Effectors](/stretch4_urdf/stretch4_urdf/end_effectors)                                                        | Outlines the available end effector tools and provides instructions on how to add new tools to the robot. |
| [URDF Conventions](/stretch4_urdf/urdf_conventions)                                                                | Outlines which conventions are used in the Stretch 4 URDF.                                                |
| [Calibration](/stretch4_urdf/stretch4_urdf/calibration)                                                            | Explains how static calibration values are applied to the URDF at load time.                              |
| [Kinematic Changes](https://github.com/hello-robot/stretch4_urdf/tree/main/stretch4_urdf/kinematic_changes.md)     | Tracks breaking kinematic and coordinate frame changes for Stretch 4.                                     |
| [Camera Orientations](https://github.com/hello-robot/stretch4_urdf/tree/main/stretch4_urdf/camera_orientations.md) | Deep dive into the camera coordinate systems, mounting angles, and optical frame conventions.             |


# Changelog

The changes between releases of Stretch 4 URDF are documented here.

## [2026.08.21](https://pypi.org/project/hello-robot-stretch4-urdf/2026.8.21)

* Collision mesh for new calibration board
* Custom tools via `stretch_add_user_tool` CLI
* Documentation for new user tools API

## [2026.07.31](https://pypi.org/project/hello-robot-stretch4-urdf/2026.7.31)

* Updated head camera sensor link frames to all share the same orientation with z pointing up
* Corrected the center camera's optical frame to match the OAK-FFC-IMX378 W sensor orientation
* Split mesh for arm link 0 from mesh for lift link
* Tune tablet collision mesh

## [2026.07.08](https://pypi.org/project/hello-robot-stretch4-urdf/2026.7.8)

* Add docking station URDF
* Add `get_tranform()` method to get TF between 2 links
* Organize preprocessing scripts and deps
* Improve fetching of calibrated URDF

## [2026.07.02](https://pypi.org/project/hello-robot-stretch4-urdf/2026.7.2)

* Fix to head collision mesh
* Fix to link name in gripper
* Fix to line sensor link name

## [2026.06.25](https://pypi.org/project/hello-robot-stretch4-urdf/2026.6.25)

* Bugfix for outputting urdf to a nonexistant folder - calls os.mkdir
* Methods to generate planar\_ik\_urdf, make\_rotary\_ik\_urdf, make\_translation\_ik\_urdf
* Generate calibrated URDFs
* Cleanup URDF post processing and optical frames
* Bugfix for is stretch4\_body isn't installed
* Simplify dependencies
* Improved docs in README

## [2026.05.27](https://pypi.org/project/hello-robot-stretch4-urdf/2026.5.27)

* Bugfix for generating URDF with nil tool
* Bugfix for permission error
* Util function to read joint limits from URDF

## [2026.05.14](https://pypi.org/project/hello-robot-stretch4-urdf/2026.5.14)

Bugfix to include the .obj meshes in the release

## [2026.05.12](https://pypi.org/project/hello-robot-stretch4-urdf/2026.5.12)

The initial release of this description repository. It contains the URDF and meshes for the "francis" batch.


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"). The Contents consist of software and data used with the Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

***

The Clear BSD License

Copyright (c) 2021-2026 Hello Robot Inc. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met:

```
 * Redistributions of source code must retain the above copyright notice,
 this list of conditions and the following disclaimer.

 * Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution.

 * Neither the name of the copyright holder nor the names of its
 contributors may be used to endorse or promote products derived from this
 software without specific prior written permission.
```

NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


# Stretch URDF Standardization Conventions

This document outlines the enforced kinematic tree and coordinate axis conventions for Stretch robot models. The following rules are implemented in `process_new_robot_model.py` and `process_new_tool.py` to create xacro files from CAD-exported URDFs.

## Rule 0: Preserve original mechanical structure and behavior

Enforcing the following rules may result in a change to the defined axes of motion, but crucially, the mechanical structure of the robot will be unchanged.

## Rule 1: Identity Alignment for Primary Structural Elements

Primary structural links inherit the orientation of the base\_link frame.

* The base `origin` relative rotation matrix is forced to Identity (`rpy="0 0 0"`).
* For rotation joints (e.g. wrist and wheel links), the zero position is aligned to the base\_link frame's orientation.

## Rule 2: Proximal to Distal Naming Convention

Telescoping link chains are named numerically from the proximal link to the distal link.

* All structural arm links and joints `arm_l*` must order incrementally.
* **`arm_l0`** attaches to the lift
* **`arm_l4`** anchors the wrist

## Rule 3: Positive Axis Consistency

All axes of motion are positive unit vectors.

* If after Rule 1 is applied, any joint has a negative axis of motion, the axis is changed to positive and a warning is raised to advise checking the motor polarity.
* If Rule 1 causes the axis of motion to not align with a primary axis (x, y, or z), the axes are reoriented to align the axis of motion with the nearest primary axis.

## Rule 4: Sensor & Optical Link Coordinate Conventions

Optical and sensor frame orientations follow available sensor documentation and ROS standards

* **Sensor Base Frame (`camera_*_link`, `line_sensor_*_link`, `gripper_camera_link`):** A sensor's mounting frame's x axis is pointed forward, with z pointing towards the top of the sensor and y pointing to the sensor's left.
* **Optical Frame (`_optical`):** Z points out of the optical sensor. X points to the sensor's right along the horizontal axis of the image and y points down.
  * **Right and Center Head Cameras:** Physically mounted with a counterclockwise (+90 degrees) rotation about the sensor's z axis. For the sensor base frame, the y axis points up (away from the base) and the z axis points horizontally outward (the robot's right). For the optical frame, the x axis points down towards the robot's base and y points to the robot's left.
  * **Left Head Camera:** Physically mounted with a clockwise (-90 degrees) rotation. For the sensor base frame, the y axis points down (towards from the base) and the z axis points horizontally outward (the robot's left). For the optical frame, the x axis points upwards away from the base and y points to the robot's right.
* **Range Sensors (`lidar_*_link`)**: The z axis points out of the sensor, aligning with the field of view, and the y axis is oriented to point towards the socket connector. The Lidars on Stretch4 are mounted such that y axes are pointing towards the head center and the x axes are pointing up.

## Rule 5: Grasping Geometry Conventions

A virtual grasp center link is added following the orientation of primary links

* **Grasp Center (`link_grasp_center`):** The main tool center point has the same x forward direction convention as the primary links. It has a parallel roll, pitch, and yaw orientation to the roll, pitch, and yaw wrist joints.
* **Fingertips (`link_fingertip_*`):** The x axes are normal to the finger tip surface, pointing inwards towards the grasp center.

## Rule 6: Wheel Conventions

* Wheels rotate about their z axes
* The z axis points towards the center of the base, so positive wheel rotation results in a ositive rotation about the base link z axis
* At the joint's zero position, the x axis points in the wheel's direction of motion
* Wheels are named counter-clockwise rotating about the base link's z axis, starting with wheel\_0 for the wheel closest to forward direction of travel, followed by wheel\_1 and wheel\_2

## Rule 7: Joint and Link Naming Conventions

Joints and links are named with the suffix \_joint or \_link, respectively. For a body with multiple frames or a body that is repeated, names are kept consistent, with suffixes added as needed before the final \_link or \_joint (e.g. line\_sensor\_1\_optical\_link)


# stretch4\_urdf


# URDF Batch Organization

We do batch production, and minor changes are introduced batch to batch. Most are internal or only affect cosmetics, but if there are kinematic changes, those will be flagged clearly in this markdown file. Most batches only see mesh changes.

## Known Batches and Kinematic Changes

* **francis**: Initial batch. No kinematic changes from baseline SE4.

## Organization

The folder organization separates the base configurations from batch-specific assets and end-of-arm tools:

* `SE4_francis/`: Contains the meshes and specific `stretch_main.xacro` for the `francis` batch.
* `SE4_tools/`: Contains available tools for the SE4 model.

## Getting a URDF

### Compiling `SE4.xacro`

The top-level `SE4.xacro` file determines which set of xacros to compile into a URDF. It requires two primary arguments: `batch` and `tool`.

```mermaid
graph TD
    SE4xacro --> BatchArgument
    SE4xacro --> ToolArgument
    BatchArgument --> BatchXacroFile
    ToolArgument --> ToolURDFFile
    BatchXacroFile --> FinalCompiledURDF
    ToolURDFFile --> FinalCompiledURDF
```

When `SE4.xacro` runs, it dynamically includes the `stretch_main.xacro` from the directory specified by the `batch` argument. It also optionally includes a tool URDF from the `SE4_tools` directory based on the `tool` argument.

The tool is chosen strictly through the `tool` parameter passed to the xacro compiler. Details regarding specific tools and their geometries will be covered in a separate primer.

### `get_urdf()` Utility Function

The repository provides utility functions to streamline generating and loading URDFs in 3rd party applications.

#### Key Python Functions

* `get_urdf`: Takes the model name, batch name, and tool name as arguments to compile `SE4.xacro` and generate the raw URDF string dynamically.
* `get_urdf_from_robot_params`: Designed for use on the robot itself. It reads the specific model, batch, and tool directly from the `stretch4_body` system parameters and returns the compiled URDF.
* `generate_ik_urdfs`: Creates simplified URDFs specifically meant for Inverse Kinematics solvers by stripping unnecessary collision meshes and visuals.

#### Why Package as a Python Package?

Packaging these URDF assets and xacro files as a Python package is a deliberate design choice to encourage dynamic loading.

Codebases and 3rd party applications should load the URDF dynamically using the provided utility functions rather than bundling static URDF assets directly. This is a good idea because:

1. **Always Up-to-Date**: By generating the URDF dynamically, applications automatically inherit the latest cosmetic and kinematic changes corresponding to the robot's specific batch.
2. **Eliminates Stale Assets**: Bundling static URDF files in downstream repositories quickly leads to out-of-date assets that do not reflect the physical robot being used.
3. **Simplicity**: The Python utility functions handle the complexity of resolving absolute file paths for meshes and running the xacro parser, leaving the downstream application with a clean, fully-formed URDF string ready for use.

## Adding a New Batch

Hello Robot engineers can use the `process_new_robot_model.py` script located at the root of the repository to process new batch models exported from Solidworks into the appropriate Xacro format.

### How to Use `process_new_robot_model.py`

1. **Export the URDF**: Export the new robot batch model from Solidworks as a raw `.urdf` file.
2. **Create the Batch Folder**: Create a new folder under `stretch4_urdf/` following the naming convention `Model_batch` (e.g., `SE4_newbatch`).
3. **Add the Assets**: Place the exported `.urdf` file and its associated `meshes/` folder directly inside this new batch folder.
4. **Run the Script**: Execute the script from the root of the repository:

   ```bash
   python process_new_robot_model.py
   ```

   The script is interactive. It will list all available batch folders containing a raw `.urdf` file and prompt you to select which one(s) to process.

### What the Script Does

Once a batch is selected, the script automates the conversion process:

1. **Generates Collision Meshes**: It runs `generate_collision_mesh.py` to automatically generate convex hull collision meshes from the visual geometries.
2. **Creates the Xacro File**: It creates an `xacro/` subdirectory within the batch folder and copies the raw `.urdf` into it as `stretch_main.xacro`.
3. **Applies Collision Geometries**: It updates the new `stretch_main.xacro` to point to the newly generated collision meshes (replacing visual meshes in collision tags where appropriate) and explicitly removes collision geometry from optical links.
4. **Parameterizes Mesh Paths**: It modifies the mesh filepaths inside the `stretch_main.xacro` file to use the dynamic variable `$(arg model_mesh_dir)`. This allows `SE4.xacro` to properly resolve the absolute paths at compile time.
5. **Sets up the Xacro Namespace**: It replaces the standard URDF `<robot>` tag with the proper `<robot xmlns:xacro...>` definition to make it a valid xacro file.


# URDF Calibration

The Stretch URDF can be dynamically calibrated at load time by applying static calibration values to the joint origins. This ensures that the robot's physical dimensions (as measured during calibration) are reflected in the URDF used for visualization, planning, and kinematics.

## How it Works

When a calibrated URDF is requested (e.g., via `get_urdf_calibrated()` or `get_urdf_from_robot_params(apply_calibration=True)`), the system looks for the `stretch_calibration_values.yaml` file within the stretch\_user directory. If available, the nominal urdf joint origins are replaced by the saved values. These values represent the static offsets of the joint origins. They are typically generated by systems like the Stretch factory calibration or user-run calibration scripts (e.g., camera-to-body calibration).

## List of Existing Calibration Procedures

The `stretch_calibration_values.yaml` file is used to store static offsets for any joint in the URDF. Currently, the following procedures are known to create or update this file:

| Joint Calibrated | Calibration Script           | Repository                        | Parent Link      | Child Link  |
| ---------------- | ---------------------------- | --------------------------------- | ---------------- | ----------- |
| `base_ref`       | `ros_find_floor_calibration` | `stretch4_dual_lidar_calibration` | `base_footprint` | `base_link` |

### Application Mechanism

The `get_urdf_calibrated` function performs the following steps:

1. Loads the nominal URDF (generated from the Xacro for the specific model, batch, and tool).
2. Parses the `stretch_calibration_values.yaml` file.
3. For every joint defined in the URDF, it checks if there is a matching entry in the YAML under `robot_calibration.joints`.
4. If a match is found, it replaces the `<origin>` element's `xyz` and `rpy` attributes with the values from the YAML.

## YAML Structure

The `stretch_calibration_values.yaml` file follows this structure:

```yaml
version: '2.0'
joint_calibration:
  base_ref:
    data: # Absolute joint origin data to swap into the URDF
      xyz: 0.0 0.0 0.027117761948801805
      rpy: -0.002335365281590182 0.008538679539879857 0.0
      parent: base_footprint
      child: base_link
    robot_id: stretch-se4-4010 # ID of the robot that performed the calibration
    timestamp: '2026-07-02T13:55:41.393513' # ISO timestamp of when the calibration was performed
    # Extra information can be added for additional context relevant to specific calibration procedures
    fit_method: svd
    rmse: 0.011093677602830953
```

## Adding Calibrations

Users can add their own calibration entries to `stretch_calibration_values.yaml`. If you have measured an offset for a joint, you can manually add it to the calibration file, or programmatically add calibrations using the exported `record_joint_calibration` utility function. The calibration will be automatically applied to the URDF when it is loaded through util functions (e.g. `get_urdf_from_robot_params`, `get_urdf_calibrated`).

```python
from stretch4_urdf import record_joint_calibration

record_joint_calibration(
    joint_name="base_ref",
    xyz="0.0 0.0 0.027",
    rpy="-0.002 0.008 0.0",
    parent="base_footprint",
    child="base_link",
    robot_id="stretch-se4-4010"
)
```

> **NOTE** The values provided in the calibration YAML are **absolute origin values** for the joint, not deltas. The system replaces the nominal `<origin>` attributes in the URDF with these exact values.

## Using Calibrated URDFs Example

To load a calibrated URDF in your Python scripts, use the `get_urdf_from_robot_params` utility. By default, `apply_calibration` is set to `True`:

```python
from stretch4_urdf import get_urdf_from_robot_params

# This will automatically detect the robot model and load calibration if available
urdf_contents = get_urdf_from_robot_params()
```

If you are not running on a robot or want to provide parameters explicitly, use `get_urdf_calibrated`:

```python
from stretch4_urdf import get_urdf_calibrated

# Explicitly provide model, batch, and tool
urdf_contents = get_urdf_calibrated(
    model_name="SE4",
    batch_name="francis",
    tool_name="eoa_wrist_dw4_tool_sg4"
)
```


# End Effector Tools for Stretch

This document outlines the available end effector tools for Stretch and provides instructions on how to add new tools to the robot.

## Available Tools

### Standard Gripper (SG4)

A compliant gripper with suction cup fingertips and calibrated kinematics for free-space opening/closing using the fingertip ArUco markers, allowing for grasp estimation.

### Parallel Gripper (PG4)

A robust parallel jaw gripper designed for precise manipulation and secure grasping of rigid objects.

### Tablet

A 13" tablet holder. This tool is often paired with Web Teleop, enabling you to use Stretch as a telepresence robot.

### Calibration Grid

A ChArUco calibration grid with visual and retroreflective fiducials. The robot can wave this tool around and calibrate its own head sensing suite autonomously.

## Adding a New Tool To Stretch

To add new end of arm hardware to Stretch and connect it to the software interface, you will need the mesh files and the URDF. These are typically exported from the CAD assembly or obtained from the manufacturer.

### 1. Create Tool Directory Structure

In the `stretch4_urdf` shared model tool directory (e.g. `SE4_tools`), create a directory for your tool with the following structure:

```
> {model}_tools 
    > {tool_name} 
        > meshes 
        {tool_name}.urdf
```

### 2. Process the auto-generated tool URDF

First, ensure the tool root link is named `link_quick_connect_interface`. This is the connection point to the rest of the arm.

* This can be accomplished in a few ways:
  1. Name the connecting link `link_quick_connect_interface` before generating the URDF.
  2. Rename the existing root link. Ensure all instances of the name are updated.
  3. Add a new link and joint to the URDF file manually. This will be a "ghost" link with no collision or visual geometry, and the joint will be an identity transform to the existing root link.
     * Example:

       ```xml
       <link name="link_quick_connect_interface" />

       <joint name="joint_quick_connect_interface" type="fixed">
           <origin xyz="0 0 0" rpy="0 0 -0" />
           <parent link="link_quick_connect_interface" />
           <child link="{existing_tool_root_link}" />
       </joint>
       ```

Next, use the `process_new_tool.py` script located at the root of the repository to automate the remaining URDF setup.

#### How to Use `process_new_tool.py`

Run the script from the root of the repository:

```bash
python process_new_tool.py
```

The script is interactive. It will list all available tool folders containing a raw `.urdf` file and prompt you to select which one(s) to process.

#### What the Script Does

Once a tool is selected, the script automates the conversion process:

1. **Generates Collision Meshes**: It creates a default `collision_mesh_config.yaml` (if one doesn't exist) and runs `generate_collision_mesh.py` to automatically generate convex hull collision meshes from the visual geometries.
2. **Applies Collision Geometries**: It updates the `.urdf` file to point to the newly generated collision meshes (replacing visual meshes in collision tags where appropriate) and explicitly removes collision geometry from optical links.
3. **Parameterizes Mesh Paths**: It modifies the mesh filepaths inside the `.urdf` file to use the dynamic variable `$(arg tool_mesh_dir)`. This allows `SE4.xacro` to properly resolve the absolute paths at compile time.
4. **Cleans Up Unused Meshes**: It checks for unreferenced meshes in the `meshes/` folder and interactively offers to delete them.

### 3. Update Robot Configuration

Add the tool to `nominal_params` in [robot\_params\_SE4.py](https://github.com/hello-robot/stretch4_body/blob/main/src/stretch4_body/robot/robot_params_SE4.py#L628).

* Add the tool name to the `supported_eoa` list. This must be the same name as the folder in `stretch4_urdf`.
* Add a new entry for the tool in the [self\_collision\_mujoco](https://github.com/hello-robot/stretch4_body/blob/main/src/stretch4_body/robot/robot_params_SE4.py#L1462) field.
* Add the tool name as a new key in the `nominal_params` dictionary:
  * If the new tool is not actuated, it can use the existing `SE4_eoa_wrist_dw4_tool_nil` object.
  * If the new tool requires a control interface, add a dictionary for the new tool in the EndOfArm section above. Follow the structure of `SE4_eoa_wrist_dw4_tool_sg4` or another existing end of arm tool.
    * Define a new class in the top level `py_module_name` and create the class in [end\_of\_arm\_tools.py](https://github.com/hello-robot/stretch4_body/blob/main/src/stretch4_body/subsystem/end_of_arm/end_of_arm_tools.py) and a class of the same name with `_Client` appended in [robot\_client.py](https://github.com/hello-robot/stretch4_body/blob/main/src/stretch4_body/robot/robot_client.py).
    * Add the tool actuator under `devices` along with the `wrist_pitch`, `wrist_roll`, and `wrist_yaw`.
      * `device_params` expects another dictionary defined in the [EOA joints section](https://github.com/hello-robot/stretch4_body/blob/main/src/stretch4_body/robot/robot_params_SE4.py#L42) above the EndOfArm section.
      * `py_module_name` and `py_class_name` point to the tool's API.
    * For ROS support, include the `ros` section. `py_module_name` and `py_class_name` point to the command group that will be used by the ros driver. The standard module is [command\_groups.py](https://github.com/hello-robot/stretch4_ros2/blob/jazzy/stretch_core/stretch_core/command_groups.py).


# Overview

This repo provides a simulation stack for Stretch 4, built on [MuJoCo](https://github.com/google-deepmind/mujoco). The simulation for Stretch 3 can be found in the [stretch\_mujoco](https://github.com/hello-robot/stretch_mujoco) repo. The simulation includes position control for the arm and gripper joints, velocity control for mobile base, calibrated camera RGB + depth imagery, 3D lidar clouds, and more. There is a visualizer that supports user interaction, or a more efficient headless mode. There is a [ROS2 package](https://github.com/hello-robot/stretch4_ros2/tree/jazzy/stretch_simulation), built on this library, that works with Nav2 and more. There is 100s of permutations of Robocasa-provided kitchen environments that Stretch can spawn into. The MuJoCo API can be used for features like deformables, procedural model generation, SDF collisions, cloth simulation, and more.

Check out a video of Stretch 4 in Robocasa environments in Mujoco:

<https://github.com/user-attachments/assets/ea683561-998b-44d3-9d45-41ab1b1664ab>

## Getting Started

First, install [`uv`](https://docs.astral.sh/uv/#getting-started). Uv is a package manager that we'll use to run this project.

Then, clone this repo:

```
git clone https://github.com/hello-robot/stretch4_mujoco --recurse-submodules
cd stretch4_mujoco
```

> If you've already cloned the repo without `--recurse-submodules`, run `git submodule update --init` to pull the submodule.

Then, install this repo:

```
uv venv
uv pip install -e .
```

Lastly, run the simulation:

```
uv run launch_sim.py
```

> Note: If you see a build error mentioning `evdev` on linux, please run `sudo apt install python3-dev`.

To exit, press `Ctrl+C` in the terminal.

> On MacOS, if `mjpython` fails to locate `libpython3.10.dylib` and `libz.1.dylib`, run these commands:

```shell
# Before proceeding, please reload your terminal and/or IDE window, to make sure the correct UV environment variables are loaded.

source .venv/bin/activate

# When `libpython3.10.dylib` is missing, run:
PYTHON_LIB_DIR=$(python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LIBDIR"))')
ln -s "$PYTHON_LIB_DIR/libpython3.10.dylib" ./.venv/lib/libpython3.10.dylib

# When `libz.1.dylib` is missing, run:
export DYLD_LIBRARY_PATH=/usr/lib:$DYLD_LIBRARY_PATH
```

## GPU Acceleration

On Linux, by default, the Python code will attempt to use GPU acceleration by setting the necessary environment variables at runtime. However, if you are running examples outside the typical Python execution flow or need to ensure these are set system-wide, you can manually export the following environment variables:

```bash
export MUJOCO_GL=egl
export PYOPENGL_PLATFORM=egl
export XLA_FLAGS=--xla_gpu_triton_gemm_any=true
```

These variables ensure that MuJoCo and any associated rendering libraries utilize hardware acceleration, which is highly recommended for performance when working with cameras or complex scenes.

## Example Scripts

[Keyboard teleop](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/keyboard_teleop.py)

```
uv run examples/keyboard_teleop.py
```

[Gamepad teleop](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/gamepad_teleop.py)

Control Stretch in simulation using any xbox type gamepad (uses xinput)

```
uv run examples/gamepad_teleop.py
```

[Robocasa environments](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/robocasa_environment.py)

```
# Setup
uv pip install -e "robocasa@third_party/robocasa"
uv pip install -e "robosuite@third_party/robosuite"
uv run third_party/robosuite/robosuite/scripts/setup_macros.py
uv run third_party/robocasa/robocasa/scripts/setup_macros.py
uv run third_party/robocasa/robocasa/scripts/download_kitchen_assets.py

# Run sim
uv run examples/keyboard_teleop.py --select_env
uv run examples/gamepad_teleop.py --select_env
uv run examples/robocasa_environment.py
```

Ignore any warnings.

## Writing Code

Use the Stretch4MujocoSimulator class to:

* start the simulation
* position control the robot's ranged joints
* velocity control the robot's mobile base
* read joint states
* read camera imagery

Try the code below using `uv run ipython`. For advanced Mujoco users, the class also exposes the `mjModel` and `mjData`. See the [official Mujoco documentation](https://mujoco.readthedocs.io/en/stable/python.html).

```python
from stretch4_mujoco import Stretch4MujocoSimulator

if __name__ == "__main__":
    sim = Stretch4MujocoSimulator()
    sim.start(headless=False) # This will open a Mujoco-Viewer window

    # Poses
    sim.stow()
    sim.home()

    # Position Control (Subsystem API)
    sim.lift.move_to(1.0)
    sim.end_of_arm.wrist_yaw.move_by(0.2)
    sim.base.translate_by(0.1)

    sim.wait_command()

    # Base Velocity control
    sim.base.set_velocity(vx_m=0.3, vy_m=0.0, w_r=-0.1)

    # Get Joint Status
    from pprint import pprint
    pprint(sim.pull_status())

    # Get Camera Frames
    camera_data = sim.pull_camera_data()
    pprint(camera_data)

    # Kills simulation process
    sim.stop()
```

### Loading Robocasa Kitchen Scenes

The `stretch4_mujoco.robocasa_gen.model_generation_wizard()` method gives you:

* Wizard/API to generate a kitchen model for a given task, layout, and style.
* If layout and style are not provided, it will take you through a wizard to choose them in the terminal.
* If robot\_spawn\_pose is not provided, it will spawn the robot to the default pose from robocasa fixtures.
* You can also write the generated xml model with absolutepaths to a file.

```python
from stretch4_mujoco import Stretch4MujocoSimulator
from stretch4_mujoco.robocasa_gen import model_generation_wizard

# Use the wizard:
model, xml, objects_info = model_generation_wizard(stretch_xml_absolute=StretchMujocoSimulator.get_robot_xml_path(),)

# Or, launch a specific task/layout/style
model, xml = model_generation_wizard(
    stretch_xml_absolute=StretchMujocoSimulator.get_robot_xml_path(),
    task=<task_name>,
    layout=<layout_id>,
    style=<style_id>,
    wrtie_to_file=<filename>,
)

sim = Stretch4MujocoSimulator(model=model)
sim.start()
```

### ROS2

You can use this simulation in ROS2 using the [`stretch_simulation` package](https://github.com/hello-robot/stretch4_ros2/tree/jazzy/stretch_simulation) in `stretch4_ros2`.

### Docs

Check out the following documentation resources:

* [Architecture](/stretch4_mujoco/docs/architecture)
* [Stretch 4 MJCF and URDF](/stretch4_mujoco/stretch4_mujoco/models/stretch_4)

### Feature Requests and Bug reporting

All the enhancements/bugfixes are tracked by [Github Issues](https://github.com/hello-robot/stretch4_mujoco/issues) filed. Please feel free to file an issue if you would like to report a bug or request a feature addition.

## Acknowledgment

The assets in this repository contain significant contributions and efforts from [Kevin Zakka](https://github.com/kevinzakka) and [Google Deepmind](https://github.com/google-deepmind), along with others in Hello Robot Inc. who helped us in modeling Stretch in Mujoco. Thank you for your contributions.

## License

The license covering the code and assets in this repo can be found in the [LICENSE](https://github.com/hello-robot/stretch4_mujoco/tree/main/LICENSE/README.md) file.


# docs


# Using the Mujoco Simulator with Stretch

When using Mujoco to simulate Stretch, you can command [joints](https://github.com/hello-robot/stretch4_mujoco/tree/main/stretch4_mujoco/enums/actuators.py) and access joint poses and [camera](https://github.com/hello-robot/stretch4_mujoco/tree/main/stretch4_mujoco/enums/stretch_cameras.py) data.

## Getting Started

1. Read the [README](/stretch4_mujoco) to install the required dependencies.
2. Check out the controller examples, such as:

* [keyboard\_teleop.py](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/keyboard_teleop.py)
* [gamepad\_teleopy.py](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/gamepad_teleop.py)

3. Check out the headless examples, such as:

* [draw\_circles.py](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/draw_circles.py)
* [camera\_feeds.py](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/camera_feeds.py)

4. Check out the sensor example: [laser\_scan.py](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/laser_scan.py)

### Terminology

The following words apply to this document only, to make it easier to read:

* [Mujoco](https://mujoco.readthedocs.io/en/stable/overview.html): An open-source physics engine.
* [Mujoco Viewer](https://mujoco.readthedocs.io/en/stable/programming/samples.html#sasimulate): An interactive Mujoco GUI that ships with Mujoco. This is spawned when you don't use `headless` mode.
* [Headless Mode](https://mujoco.readthedocs.io/en/stable/APIreference/APIfunctions.html#main-simulation): Using the Mujoco simulation without the Mujoco Viewer. This calls `mj_step` directly to step the simulation. For the purposes of Stretch Mujoco Simulations, this is a performant mode to run simulations in.
* [Stretch Mujoco Simulator](https://github.com/hello-robot/stretch4_mujoco/tree/main/stretch4_mujoco/stretch4_mujoco_simulator.py): A scaffolding that enables you to send commands and receive sensor data to and from Stretch in a Mujoco environment.

## Control Flow

All simulations using [Stretch Mujoco Simulator](https://github.com/hello-robot/stretch4_mujoco/tree/main/stretch4_mujoco/stretch4_mujoco_simulator.py) should have an entry point that looks similar to this:

```python
if __name__ == "__main__":

    # You can use all the camera's, but it takes longer to render, and may affect the overall simulation FPS.
    # cameras_to_use = StretchCameras.all_stretch4()
    cameras_to_use = [StretchCameras.cam_gripper_rgb]

    sim = StretchMujocoSimulator(cameras_to_use=cameras_to_use)

    sim.start(headless=True)
```

> You can use the `start()` method to specify headless or UI view modes. The Passive Viewer is the default and recommended non-headless mode.

> If you are using the simulation for machine-learning applications, it is recommended to use the headless mode for better performance.

> If you are displaying camera data or doing heavy computations on your Control Loop, it is recommended to move your control commands to a thread, and display the camera data on the main thread. See [Displaying camera data using OpenCV](#displaying-camera-data-using-opencv) below and the [camera\_feeds.py](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/camera_feeds.py) example for more information.

### Commanding Stretch

When the simulator is running, you can use your script to send commands to Stretch, or read data from the simulation.

Use the following command to move the lift to `0.5m`: `sim.lift.move_to(0.5)`

### Reading data from Stretch

There are two methods for pulling data from the simulation: `sim.pull_status()` and `sim.pull_camera_data()`.

#### Stretch Status

Use `sim.pull_status()` to fetch the joint states of the robot.

This method returns a `StatusStretchJoints` [dataclass](https://github.com/hello-robot/stretch4_mujoco/tree/main/stretch4_mujoco/datamodels/status_stretch_joints.py) with the names of all the joints populated.

The statuses of all the joints are fetched at the same time.

#### Stretch Sensors

Use `sim.pull_sensor_data()` to fetch data from sensors on Stretch.

This methods returns a `StatusStretchSensors` [dataclass](https://github.com/hello-robot/stretch4_mujoco/tree/main/stretch4_mujoco/datamodels/status_stretch_sensors.py)

The statuses of all the sensors are fetched at the same time.

All the sensors defined in [`stretch.xml`](https://github.com/hello-robot/stretch4_mujoco/tree/main/stretch4_mujoco/models/scene.xml) are fetched:

```
  <sensor>
    <gyro name="base_gyro" site="base_imu"/>
    <accelerometer name="base_accel" site="base_imu"/>
    <rangefinder name="base_lidar" site="lidar" cutoff="10.0"/>
  </sensor>
```

> Note: The Lidar sensor (implemented as `<rangefinder/>`) is compute intensive. Comment it out in the XML, if you are not using it.

#### Stretch Cameras

Use `sim.pull_camera_data()` to fetch the pixel values from Stretch's cameras.

This method returns a `StatusStretchCameras` [dataclass](https://github.com/hello-robot/stretch4_mujoco/tree/main/stretch4_mujoco/datamodels/status_stretch_camera.py) with the names of all the cameras populated.

The pixel values of all the renderings of the cameras are fetched at the same time.

Note: this operation is computationally heavy. The more cameras that are requested, the slower the simulation may run.

**Displaying camera data using OpenCV**

The [camera\_feeds.py](https://github.com/hello-robot/stretch4_mujoco/tree/main/examples/camera_feeds.py) example shows a sample to display camera data using `cv2.imshow()`.

```python

camera_data = sim.pull_camera_data()

for camera in cameras_to_use:
    cv2.imshow(camera.name, camera_data.get_camera_data(camera))

cv2.waitKey(1)
```

> Important Note: you should call `cv2.imshow()` on the MAIN THREAD to avoid getting graphics library (GL) related errors from your OS.

### Misc Stretch Mujoco Simulator API calls

#### World Coordinate Frame Arrows

You can use `sim.add_world_frame((0.1, 0.0, 0.0))` to add arrows dynamically to the Mujoco viewer:

<img src="/files/ZcsdDg4MEFfby1P8ZZDe" alt="" width="400">

This also supports rotating the frame:

```
sim.add_world_frame((0.1,0,0), (0,0,0))
sim.add_world_frame((0.2,0,0), (1.57,0,0)) # (x, y, z), (r, p, y)
```

<img src="/files/L388UAA3tdDOvbJfOSNo" alt="" width="400">

## More to know

### Behind the scenes

When you call `sim.start()`, the following process diagram explains how Mujoco is launched and your Control Loop are managed.

> tl;dr The Mujoco simulator is started on a spawned process, and data is communicated between your main process and the Mujoco process using a [Multiprocesing Manager](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Manager).

<img src="/files/9zui028UELOBbCAi4EPG" alt="" width="600">

### Mujoco rendering locks

Mujoco's [passive](https://mujoco.readthedocs.io/en/stable/python.html#passive-viewer) and headless modes need some access governance over the `mjdata` and `mjmodel` objects to behave correctly.

When using either of these modes, we are responsible for calling `mj_step` to step the simulation. This also means we're responsible for managing when Mujoco should render scenes. Calling `mj_step` is not a thread-safe operation, and if it's done while a render is rendering, bad things happen. So we use some mutexes to ensure these steps are in sync.

> Note: ignoring the use of mutexes to lock `mjdata` and `mjmodel` before rendering could cause Mujoco to crash, or instability errors such as "WARNING: Inertia matrix is too close to singular at DOF 11. Check model. WARNING: Nan, Inf or huge value in QPOS at DOF 0. The simulation is unstable."


# stretch4\_mujoco


# models


# A room with four walls and a ceiling.

Apache License 2.0 Acknowledgement: Vikash Kumar (<vikashplus@gmail.com>) <https://github.com/vikashplus/scene\\_sim>


# Stretch 4 MJCF and URDF

The MJCF is now dynamically generated on-the-fly from the `stretch4_urdf` package using `urdf2mjcf` and programmatic DOM manipulations.

When the simulator is initialized, `get_robot_xml_path()` dynamically invokes `mjcf_generator.py` to convert the appropriate URDF into an MJCF and applies custom modifications required by Mujoco.

## Programmatic Transformations Applied

The `mjcf_generator.py` applies the following changes to the raw `urdf2mjcf` output:

1. Removes all tags except `asset` and `worldbody`.
2. Removes specific tags from worldbody (e.g., `light`, `camera`, `ground` plane).
3. Replaces the default wheel joints with custom bodies (`link_left_wheel`, `link_right_wheel`, `link_back_wheel`) containing `class="wheel"` and associated wheel rollers to support omnibase motion, and includes `omniwheels.xml`.
4. Encapsulates lidar and camera geoms with dedicated bodies and includes `hemisphere_lidar_cameras_right/left.xml`.
5. Replaces `link_mast` geometry with appropriate `collision` and `visualgeom` classes.
6. Renames the root body to `base_link` and adds an `imu` site.
7. Encapsulates gripper fingers into `link_gripper_slider` (if present, e.g. for sg4 tool).
8. Adjusts `class="visualgeom"` and adds `class="rubber"` to gripper fingers.
9. Modifies wrist roll to include `gripper_cameras.xml`.
10. Adds `gravcomp="1"` to lift, arm, and wrist components.
11. Maps joints to appropriate classes defined in `defaults.xml` (`lift_stretch4`, `telescope`, `wrist_yaw_stretch4`, etc.).

These steps have been fully automated and replace the previously documented manual processes.


# Stretch 4 Flying Gripper Control

## Introduction

This repository provides a minimal example that enables gamepad control of the Stretch 4 mobile manipulator's gripper using the gripper's coordinate system. For the main gripper-centric control mode, the user can think of themselves as piloting the gripper to fly through the world. The controller enables them to do so without attempting to contol the robot's individual joints in a coordinated way.

### Motivation

The original motivation for this style of control was to support autonomous and teleoperative control via imagery provided by Stretch 4's gripper camera. Control with respect to the gripper's coordinate system results in consistent, interpretable changes to images from the gripper camera. In contrast, joint-space control results in dramatically different changes depending on the robot's current configuration. For example, if the gripper is rotated 90 degrees in yaw such that its direction is orthogonal to the extension direction of the telescoping arm, extending the telescoping arm results in sideways motion in the images instead of forward motion.

### Control Method

The two available gripper-centric control modes primarily change the gripper's orientation by directly controlling the wrist's yaw, pitch and roll joints. Simultaneously, the position of the gripper is changed by controlling the position of the end of the telescoping arm with respect to the world.

To control the end-of-arm position, the controller uses a Jacobian found via a specialized URDF to set the robot's joint velocities using weighted damped pseudo-inverse control. This Jacobian relates changes in the omnidirectional base, lift and telescoping arm (5 degrees of freedom) to the end-of-arm position (3 degrees of freedom). Importantly, it biases solutions to use rotation of the omnidirectional mobile base instead of translation, since rotation provides higher quality motion.

### Joint Limits

Another notable aspect of the controller is how it uses redundancy and whole body motion to handle joint limits. If the telescoping arm extends near its maximum reach, the mobile base begins to help translate the gripper. Once the telescoping arm extends to its joint limit, the mobile base is fully responsible for translating the gripper. At this point, the arm also begins to slowly retract. Doing so gradually improves the quality of motion and increases the ability of the arm to perform high-quality motions without hitting its joint limit. Also, the robot will begin moving backward if the telescoping arm is fully retracted and the gripper is commanded to go backward.

A similar approach is used when controlling the gripper's yaw angle. As the wrist's yaw angle approaches a joint limit the mobile base begins to move so as to continue the gripper's rotation around the wrist yaw's axis of rotation.

## Installation

**Note: Installation of this repository as a package is NOT necessary to use it.** Simply cloning the repository and installing its dependencies allows you to run `stretch_gamepad_teleop_gripper.py` directly from the command line while in the repository's root directory without any further installation steps.

1. Copy or clone this repository to your Stretch 4 robot.
2. The core requirements for connecting to the robot (`stretch4_body`, `stretch4_urdf`) are typically already installed on Stretch 4 systems.
3. Install the unmet third-party dependencies (`numpy`, `pinocchio`, `yourdfpy`) using the included installation script:

```bash
./install_dependencies.sh
```

*(This script securely handles PEP 668 externally-managed environments natively using `--break-system-packages` if required).*

### Advanced: Using as a Python Package

In addition to running as standalone scripts, the repository is formatted using modern Python packaging standards (`pyproject.toml`). This allows it to be installed into other virtual environments or at the system level so that its internals (`teleop_config`, `kinematic_controller`, etc.) can be imported from code in other repositories without having to manage directories or subdirectories.

You can install it locally as an editable package:

```bash
pip install -e .
```

Or directly install it using:

```bash
pip install .
```

## Usage

You can begin teleoperating the robot by executing the main script:

```bash
python3 stretch_gamepad_teleop_gripper.py
```

The controller relies on a standard gamepad. Press the **Top Button (Y)** to toggle between the three available control modes dynamically while operating the robot.

### Quick Start: Flying the Gripper

When you first launch the script, the system defaults to **Mode #1**. The absolute easiest way to get started with this control scheme is:

1. **Aim:** Use the **Right Stick** (yaw and pitch) to physically point the gripper at a target in the world.
2. **Fly:** Move the gripper forward to the target by pushing the **Left Stick** up.
3. **Correct:** While the gripper is flying toward the target, use the **Right analog stick** to continuously correct its direction on the fly.

### Control Modes & Gamepad Mapping

The controller relies on a standard gamepad (like an Xbox controller). Press the **Top Button (Y)** to toggle between the three available control modes dynamically.

#### Universal Actions (All Modes)

```
       [Left Trigger]                 [Right Trigger]
     Dampen / Slow down           Modifier (Mode 3 only)
             |                              |
         ____|______________________________|____
        /                                        \
       /    _                        (Y) Toggle   \
      |   /   \                          (Y)       |
      |  | LS  |                     (X)     (B) -----> Open Gripper
      |   \ _ /                          (A)       |
      |                                     |      |
      |           _                   _     |      |
      |         _| |_               /   \   |      |
      |        |_   _|             | RS  |  v      |
       \         |_|                \ _ / Close Gripper/
        \      (D-Pad)                            /
         \_______________________________________/
          (LS = Left Stick, RS = Right Stick)
```

* **Top Button (Y):** Cycle through control modes (1 -> 2 -> 3 -> 1).
* **Bottom Button (A):** Close Gripper.
* **Right Button (B):** Open Gripper.
* **Left Trigger:** Proportional Speed Dampener. Squeezing this trigger progressively slows down all movements for fine-tuned precision.

***

#### Modes 1 & 2: Cartesian IK Controllers

These modes use the **Pinocchio inverse-kinematics solver** to automatically calculate the combinations of base, arm, and lift movements required to move the gripper through Cartesian space.

* **Mode 1: Gripper Frame Relative ("Flying Gripper Control")** Control is entirely with respect to the gripper's *own* 3D coordinate system. Translating "forward" moves the gripper exactly where it is pointing. Look through the gripper camera to "pilot" it freely.
* **Mode 2: Projected Base Frame Relative ("Camera Intuitive Control")** Locks translation to the horizontal floor plane. "Forward" moves the gripper in its forward direction projected onto the ground, preventing the robot from unintentionally digging the gripper into the floor or lifting up when pointing down.

**Gamepad Mappings (Modes 1 & 2):**

```
[ LS (Left Stick) ]
  Up / Down: Translate Forward / Backward
  Left / Right: Translate Left / Right

[ D-Pad ]                                   [ RS (Right Stick) ]
  Up / Down: Translate Up / Down              Up / Down: Wrist Pitch
  Left / Right: Wrist Roll                    Left / Right: Wrist Yaw
```

***

#### Mode 3: Joint-Space Direct Control

A direct hardware mapping where the gamepad inputs instruct individual physical joints directly. This bypasses the Cartesian inverse kinematics solver entirely.

**Gamepad Mappings (Mode 3 - Standard):**

```
[ LS (Left Stick) ]
  Up / Down: Base Forward / Backward
  Left / Right: Base Translate Left / Right

[ D-Pad ]                                   [ RS (Right Stick) ]
  Up / Down: Lift Up / Down                   Up / Down: Arm Extend / Retract
                                              Left / Right: Base Turn (Rotate)
```

**Gamepad Mappings (Mode 3 - While holding RIGHT TRIGGER):** Holding the `Right Trigger` replaces several chassis controls with wrist controls.

```
[ LS (Left Stick) ]
  Up / Down: Arm Extend / Retract
  Left / Right: Wrist Roll

[ D-Pad ]                                   [ RS (Right Stick) ]
  Up / Down: Lift Up / Down                   Up / Down: Wrist Pitch
                                              Left / Right: Wrist Yaw
```

## How It Works

A key aspect of developing these controllers was making the most of the two redundant degrees of freedom (DOF). The Stretch 4 has 8 controllable DOFs, while the gripper's pose only requires 6 DOFs.

The two new gripper-centric controllers (Modes 1 and 2) use a specialized URDF. This URDF includes a virtual joint that mathematically represents the omnidirectional mobile base.

To map Cartesian intent to joint velocities, the script utilizes the **Pinocchio** dynamics library. Pinocchio calculates the specialized Jacobian for the active controller, identifying how small changes to the 5 translational joints (3-DOF omnidirectional base, 1-DOF lift, 1-DOF arm) relate to the forward/backward, left/right, and up/down Cartesian changes.

To resolve the 2 extra degrees of redundancy, the Jacobian is passed through a **weighted damped pseudo-inverse control** calculation. The logic biases the use of mobile base rotation over mobile base translation, since base rotation results in higher quality motion. Specifically, the solver generally prohibits X/Y base translation. However, if you get close to the physical joint limits of the telescoping arm or the wrist yaw, the algorithm dynamically scales the penalty weights, and the omnidirectional mobile base begins translating to keep the gripper moving along your commanded vector.

### Specialized URDF Configuration

The accuracy of this controller is fundamentally tied to the specialized URDF. Without it, the Jacobian matrix would not account for the omnidirectional drive properly. The specialized URDF specifically introduces a **virtual planar joint** to mathematically model the mobile base's degrees of freedom.

If you would like to use a custom URDF for your specific robot model, you must generate an IK-compatible URDF using the Stretch 4 Urdf package.

1. Navigate to the Hello Robot official `stretch4_urdf` package.
2. Execute the generator script: [urdf\_utils\_generate\_ik\_urdfs.py](https://github.com/hello-robot/stretch4_urdf/blob/main/stretch4_urdf/urdf_utils_generate_ik_urdfs.py)
3. This script will output several files. Identify the specialized URDF containing the text `base_planar_ik` in its filename.
4. **Verify the URDF** by running the internal `check_kinematic_chain.py` tool. You can pass your URDF path to see if your chain differs physically or structurally from other officially tested kinematic chains:

   ```bash
   python3 check_kinematic_chain.py <your_generated_urdf>.urdf
   ```
5. Launch your teleop session by explicitly overriding the default URDF path in the arguments:

   ```bash
   python3 stretch_gamepad_teleop_gripper.py --urdf <your_generated_urdf>.urdf
   ```

**Important Notes for Calder and Dali Robots:**

* By default, `stretch_gamepad_teleop_gripper.py` points to `/tmp/stretch_gamepad_teleop/gamepad_teleop_base_planar_ik.urdf` generated by `stretch4_urdf.generate_ik_urdfs()`. This repository has been successfully tested and works well with the Francis model of Stretch 4.


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# Stretch 4 Human Perception

This repository provides tools and examples for performing real-time human pose estimation on the Stretch 4 mobile manipulator. It primarily uses the RTMO series of models via OpenVINO for inference on edge devices (including CPU, GPU, and NPU), and SAM 3.1 for segmentation.

> \[!NOTE] OpenVINO runs on any x86\_64 compatible CPU. Hardware-specific execution like `--device NPU` or `--device GPU` within standard OpenVINO builds requires compatible Intel hardware. On desktop setups with AMD CPUs or NVIDIA GPUs, use the default `--device AUTO` or `--device CPU`.
>
> **Important NPU Notice:** Due to a known OpenVINO compiler bug with the dynamic Non-Max Suppression (NMS) operators used in RTMO, running these models on the Intel NPU currently produces empty or garbage bounding boxes. If you specify `--device NPU`, the pipeline will automatically detect this limitation and safely fall back to the **GPU**, which executes the model with hardware acceleration.

## Installation

### Installing this package directly

Run the installation script to setup the system hardware drivers (NPU and GPU), create a virtual environment, install the python package, and download all models:

```bash
./install_dependencies.sh
```

> \[!CAUTION] **MANDATORY REBOOT / LOGOUT:** You may be prompted for your `sudo` password to install the required Intel NPU and GPU system drivers. After the script completes, **you must log out and log back in** (or reboot) for hardware permissions to fully take effect. If you do not do this, the system will not detect the NPU/GPU hardware.

Activate the virtual environment before using the tools:

```bash
source venv/bin/activate
```

### Other Dependencies

Please clone and install the following dependencies in the virtual environment associated with this package:

Robot and External Desktop:

* `stretch4_flying_gripper`: <https://github.com/hello-robot/stretch4\\_flying\\_gripper>
* `stretch4_compliant_gripper` : <https://github.com/hello-robot/stretch4\\_compliant\\_gripper>
* `stretch4_rgbd` : <https://github.com/hello-robot/stretch4\\_rgbd>

Robot only:

* `stretch4_pyhesai_wrapper` : <https://github.com/hello-robot/stretch4\\_pyhesai\\_wrapper>

### Installing this package as a dependency in another project

When adding this repository as a depdendency in another package, we added console scripts (such as `install_dependencies.sh` and `setup_models.py`) that install as part of pip installing this repository that you can run from your project's python environment.

1. Add this package to your project's dependencies list: `"stretch4-human-pose-estimation @ git+ssh://git@github.com/hello-robot/stretch4_human_perception.git"`
2. Install your package package: `pip install -e .`
3. Run the install script to setup the system hardware drivers (NPU and GPU), create a virtual environment, install the python package, and download all models: `install_dependencies.sh` or `python3 -m stretch4_human_pose_estimation.utils.install_deps`
4. You can also download models using `setup_models.py` or `python3 -m stretch4_human_pose_estimation.utils.install_deps --size all`

## Downloading Models

The installation script automatically downloads all models by default. If you need to manually download them later, use the provided setup script (make sure your virtual environment is active):

```bash
# Download the medium model (default)
python3 setup_models.py
```

```bash
# Download a specific model size
python3 setup_models.py --size t
```

```bash
# Print setup instructions for SAM 3.1
python3 setup_models.py --sam3
```

```bash
# Download all models and print SAM 3.1 setup instructions
python3 setup_models.py --size all
```

### SAM3 Setup

Please clone the SAM 3.1 repository and install it as described in its README: <https://github.com/facebookresearch/sam3>. You will need to request access for the SAM 3.1 model weights on [Hugging Face](https://huggingface.co/facebook/sam3) and authenticate using the Hugging Face CLI:

```bash
pip install -U huggingface_hub
hf auth login
```

## Running Examples

Example scripts are provided to test the pose estimation pipeline with camera streams, image directories, and 3D RGB-D projection. **Ensure your virtual environment is active** before running the examples.

Run any demos using RTMO directly on the robot.

Run any demos using SAM3 on a remote desktop. These require a high bandwidth connection between the robot and the desktop.

### Key Scripts for Desktop & Robot Communication

For any demos that use the external desktop computer, be sure to update the IP addresses in `stretch4_rgbd/rgbd_networking.py` and `stretch4_compliant_gripper/gripper_networking.py`. Run the following commands from the `stretch4_compliant_gripper` and `stretch4_rgbd` repositories on the robot:

```bash
# robot control interface
python3 stretch4_compliant_gripper/recv_and_execute_gripper_commands.py --remote

# stream RGBD data
python3 stretch4_rgbd/examples/send_rgbd_images_and_joint_states.py --remote
```

### 2D Pose Estimation: Robot Local

Terminal 1 (robot):

```bash
# stream RGBD data locally
python3 stretch4_rgbd/examples/send_rgbd_images_and_joint_states.py
```

Terminal 2 (robot): choose 1 of the following scripts:

```bash
# Run with default settings (Medium model, Left camera, AUTO device)
python3 examples/rtmo_pose_estimation.py
```

```bash
# Run the large model on the GPU using the right camera
python3 examples/rtmo_pose_estimation.py --size l --device GPU --camera right
```

```bash
# Run on a directory of images
python3 examples/rtmo_pose_estimation.py --dir /path/to/images
```

```bash
# Run the stereo camera example (combines left and right camera streams side-by-side)
python3 examples/stereo_rtmo_pose_estimation.py
```

### 3D RGB-D Pose Estimation (ReRun): Robot Local

We also provide an advanced example that uses the RGB-D camera streams to infer and visualize 3D human pose keypoints alongside the point cloud in ReRun.

Terminal 1 (robot):

```bash
# Run with left camera stream and visualize 3D skeletons
python3 examples/rgbd_rtmo_pose_estimation.py --camera left --lidar left
```

### 3D RGB-D SAM 3.1 Body Segmentation (ReRun): Robot + Desktop

We provide an example that uses SAM 3.1 to segment people in the RGB-D camera streams and visualize the 2D masks and 3D point clouds in ReRun.

Terminal 1 (robot):

```bash
# stream RGBD data remotely
python3 stretch4_rgbd/examples/send_rgbd_images_and_joint_states.py --remote
```

Terminal 2 (desktop):

```bash
# Run with left camera stream and visualize SAM 3.1 segmentations
python3 recv_and_sam3_rgbd.py --remote --tracking --prompt "people"
```

The script on the desktop has many options for further processing the segmented humans. For example, mediapipe can be used to infer human skeleton, face, and hand keypoints by adding the following flags:

```bash
# 2D pose estimation
--mediapipe_body
# 2D hand pose estimation
--mediapipe_hands
# 2D face pose estimation
--mediapipe_faces
# simultaneously estimates pose, face, and hands
--mediapipe_holistic  
```

The script `recv_and_sam3_rgbd_simple.py` strips away the additional options for pose processing as a minimal example.

### 3D Robot Body Prediction (ReRun): Robot Local or Robot + Desktop

We also provide an example that tracks the robot's physical links using time-synchronized joint states, computing exact 3D Cartesian coordinates with Pinocchio and visualizing them against the RGB-D point cloud in ReRun.

#### Example usage on the robot:

Terminal 1 (robot):

```bash
# stream RGBD data locally
python3 stretch4_rgbd/examples/send_rgbd_images_and_joint_states.py
```

Terminal 2 (robot):

```bash
# Run to visualize the robot links and coordinate frames inside the point cloud
python3 examples/robot_body_prediction.py
```

#### Example usage on the robot and desktop:

Terminal 1 (robot):

```bash
# stream RGBD data remotely
python3 stretch4_rgbd/examples/send_rgbd_images_and_joint_states.py --remote
```

Terminal 2 (desktop):

```bash
# Run to visualize the robot links and coordinate frames inside the point cloud
python3 examples/robot_body_prediction.py --remote
```

> \[!NOTE] This script natively integrates with the `stretch4_emulated_rgbd` package to provide high-performance, temporally synchronized streams. It will automatically load and apply any optimized Extrinsics calibration present for the current robot without requiring any additional command line arguments.

### Moving Relative to Humans: Robot + Desktop

We provide two examples of moving Stretch 4 relative to human pose estimates from SAM 3.1. Be sure to run both of the scripts in the "Key Scripts for Desktop & Robot Communication" section above on the robot, and then run one of the following on a desktop computer:

1. Follow a human around the room:

```bash
python3 examples/follow_person_demo.py --remote
```

The human following demonstation commands Stretch 4 to move its omnibase to follow a human around a room. **Note:** the robot will run into obstacles, but should stop when near the human.

2. Give a human a fist bump:

```bash
python3 examples/fist_bump_demo.py --remote
```

The fist bump demonstration commands Stretch 4 to track a human's hand and to perform a "fist bump" motion when the user moves their hand upwards and towards the robot. In order to trigger the fist bump with the robot, the human must move their hand upwards and towards the robot. Detection of this gesture may not work consistently for different sized users; view the parameters beginning with `START_FIST_BUMP_` in `examples/fist_bump_demo_config.py` to adjust the detection thresholds.

## Python API

You can easily integrate RTMO into your own Python code:

```python
import cv2
from stretch4_human_pose_estimation import RTMOPipeline

# Initialize the pipeline (downloads model if needed)
pipeline = RTMOPipeline(size='m', device='AUTO')

# Load an image
image = cv2.imread('test_image.jpg')

# Predict
results = pipeline.predict(image, conf_threshold=0.5)

# Visualize
vis_image = pipeline.visualize(image, results)
cv2.imshow("RTMO", vis_image)
cv2.waitKey(0)
```

You can similarly use SAM 3.1:

```python
import cv2
from stretch4_human_pose_estimation import SAM3Pipeline

# Initialize the SAM 3.1 pipeline (requires HuggingFace login and sam3 installed)
pipeline = SAM3Pipeline(prompt='people')

# Load an image
image = cv2.imread('test_image.jpg')

# Predict
results = pipeline.predict(image, conf_threshold=0.5)

# Visualize
vis_image = pipeline.visualize(image, results)
cv2.imshow("SAM 3.1", vis_image)
cv2.waitKey(0)
```


# stretch4\_hybrid\_marker\_demo

This repository provides demonstrations of the use of hybrid markers with the Stretch 4 mobile manipulator from Hello Robot Inc. Hybrid markers combine LiDAR-reflective material with a visible light ArUco marker to support efficient and robust autonomy.

This package is structured as a standard ROS 2 Python package.

## Installation

You have two options for installing this package: using the standard ROS 2 `colcon` build system or installing it directly as a regular Python package via `pip`.

### Option 1: Standard ROS 2 Install (colcon)

To install this package in a ROS 2 workspace, clone it into your workspace's `src` directory and build it using `colcon`:

```bash
# Assuming your workspace is ~/ros2_ws
cd ~/ros2_ws/src
git clone https://github.com/hello-robot/stretch4_hybrid_marker_demos.git
cd ~/ros2_ws
colcon build --packages-select stretch4_hybrid_marker_demos
source install/setup.bash
```

### Option 2: Python Package Install (pip)

If you prefer to avoid the complexities of a full ROS 2 workspace, you can install the package directly into your current Python environment (or virtual environment) using `pip`. The package is configured as a standard `setuptools` Python package:

```bash
git clone https://github.com/hello-robot/stretch4_hybrid_marker_demos.git
cd stretch4_hybrid_marker_demos
pip install -e .
```

*(Make sure you have sourced your base ROS 2 installation first so `rclpy` and other dependencies are available).*

## Usage

To run the full hybrid marker pursuit demo, you must start several components in separate terminals. First, ensure the foundational robot drivers and protocols are running:

**Terminal 1: Launch Stretch Driver**

```bash
ros2 launch stretch_core stretch_driver.launch.py
```

**Terminal 2: Launch the Hesai LiDAR**

```bash
ros2 launch stretch_core dual_hesai.launch.py
```

**Terminal 3: Launch the Zenoh pub/sub/query protocol**

```bash
ros2 run rmw_zenoh_cpp rmw_zenohd
```

**Terminal 4: Start the RViz2 visualization**

```bash
cd ~/repos/stretch4_hybrid_marker_demos
rviz2 --display-config ./rviz/pursue_target.rviz
```

> \[!NOTE] **Important Calibration Step:** For best performance, the calibrated transform between `base_footprint` and `base_link` should be used. However, the latest Stretch 4 URDF has a `base_footprint` frame with a nominal transform that can conflict with this broadcasted static transform (a known issue to be fixed in the future). If you wish to use the calibrated transform, run the following in a separate terminal from the calibration repository:
>
> ```bash
> cd ~/repos/stretch_dual_lidar_calibration
> python3 ./stretch_dual_lidar_calibration/ros_broadcast_calibration.py
> # Expected Output: [INFO] [...] [calibration_broadcaster]: Broadcasted static transform base_link -> base_footprint
> ```

### Running the Demo Nodes

Depending on how you installed the package, use one of the following methods in **Terminal 5** and **Terminal 6** to start the tracker and pursuit nodes.

#### Method A: Using Standard ROS 2 Install (colcon)

**Terminal 5: Start the high-intensity LiDAR object tracker**

```bash
ros2 run stretch4_hybrid_marker_demos ros_track_object
```

**Terminal 6: Start the hybrid marker cube pursuit demo**

```bash
ros2 run stretch4_hybrid_marker_demos ros_pursue_target
```

#### Method B: Using Simple Python Install (pip)

**Terminal 5: Start the high-intensity LiDAR object tracker**

```bash
cd ~/repos/stretch4_hybrid_marker_demos
python3 ./stretch4_hybrid_marker_demos/ros_track_object.py
```

**Terminal 6: Start the hybrid marker cube pursuit demo**

```bash
cd ~/repos/stretch4_hybrid_marker_demos
python3 ./stretch4_hybrid_marker_demos/ros_pursue_target.py
```

### Command Line Arguments

You can customize the pursuit behavior using several command-line flags:

* `--speed {slow,default,fast,max}`: Sets the dynamic speed profile for the robot's base, lift, arm, and gripper (default: `default`).
* `--disable_translation`: Prevents the base from driving linearly toward the target (X/Y translation).
* `--disable_rotation`: Prevents the base from rotating to face the target.
* `--disable_lift`: Prevents the lift from dynamically tracking the target's height.
* `--disable_arm`: Disables arm extension/retraction behavior.
* `--disable_gripper`: Disables dynamic opening/closing of the gripper.

**Example with custom arguments:**

```bash
ros2 run stretch4_hybrid_marker_demos ros_pursue_target --speed fast --disable_arm
```

## Technical Details: How the Demo Works

The `ros_pursue_target` demo is implemented as a ROS 2 node that interacts with the robot hardware using `stretch_body_ii` and subscribes to perception topics from the hybrid marker tracking pipeline.

### 1. State Machine

The robot operates via a simple finite state machine consisting of three states:

* **WAITING**: The robot evaluates the incoming clusters and waits for a robust track to fulfill lock-on conditions.
* **FIXATED**: The robot actively commands its joints (base, lift, arm, gripper) to pursue the locked target and align its gripper with the target's position.
* **GRASPING**: Initiated when the robot is sufficiently close to the target. The base stops, and the arm and gripper attempt to engage.

### 2. Target Acquisition & Filtering

The node subscribes to `/lidar_tracked_clusters` (a `visualization_msgs/MarkerArray`). It filters for tracks that appear as `SPHERE` markers, using the `color.r` channel to extract the "agent categorization score" generated by the upstream perception stack.

To prevent false positives, a target candidate must satisfy three conditions:

1. It must remain within a predefined 3D spatial bounding box relative to the `base_footprint` (in front of the robot).
2. It must be continuously tracked for at least 2.0 seconds (`wait_time_sec`).
3. It must have at least 5 highly confident "agent" classifications (`agent_count_min`).

### 3. Control Loop & Hardware Interface

A 30Hz ROS 2 timer (`self.control_timer`) dictates the hardware control loop.

* **Transformations**: The target's coordinates are continuously transformed into the `base_footprint` frame using `tf2_ros` so that the robot can accurately calculate positional errors regardless of the camera's pose.
* **Kinematic Pursuit**: A proportional controller reduces the distance and angular error between the target and the gripper. The system accounts for the gripper's right-side offset (`gripper_offset_y = -0.095m`) and aims for a set distance in front of the base (`target_distance_x = 0.6m`).
* **Command Aggregation**: Joint commands for the base, lift, arm, and gripper are aggregated and sent to the firmware simultaneously using `self.robot.push_command()` to ensure synchronized motion.

### 4. Grasp Evaluation

When the distance error drops below 0.02 meters, the state transitions to **GRASPING**. The base holds position while the end-of-arm attempts a grasp. If the gripper's aperture matches the target aperture, the grasp is considered successful, and the arm retracts.


# Stretch 4 Grasping Demo

This repository contains a grasping demo for the Stretch 4 mobile manipulator from Hello Robot. Grasping is based on RGB and depth images from Stretch 4's gripper camera.

Perception uses the [Molmo 2](https://github.com/allenai/molmo2) Vision-Language Model (VLM) to output pixel coordinates for a target object described with text. The pixel coordinates are then used to prompt the [Segment Anything Model 2 (SAM 2)](https://github.com/facebookresearch/sam2) to segment the target object, after which SAM 2 tracks and segments the target object over time.

A finite state machine (FSM) consisting of a sequence of visual servoing behaviors controls the robot's motions. The behaviors use the segmentation mask output by SAM 2, the depth image from the wrist-mounted camera, and estimates of the gripper's fingertip frames of reference to decide how to move the robot.

The FSM behaviors use three control modes provided by [flying gripper control](https://github.com/hello-robot/stretch4_flying_gripper/):

* Mode 1: Gripper Frame Relative Motions
* Mode 2: Gripper Frame Projected into the Base Frame Relative Motions
* Mode 3: Direct Joint-Space Control of the Joints (Relative and Absolute Motions)

More details about the FSM and tunable parameters for the behaviors can be found in [visual\_servo\_fsm\_params.py](https://github.com/hello-robot/stretch4_grasping_demo/blob/main/visual_servo_fsm_params.py).

## Installation

### Other Dependencies

Please clone and install the following dependencies in the virtual environment associated with this package:

* `stretch4_flying_gripper`: <https://github.com/hello-robot/stretch4\\_flying\\_gripper>
* `stretch4_compliant_gripper`: <https://github.com/hello-robot/stretch4\\_compliant\\_gripper>

> \[!NOTE] **Architecture Note:** Both Molmo 2 and SAM 2.1 are loaded dynamically on the fly using the Hugging Face `transformers` library. You do *not* need to clone their respective repositories or manually download checkpoints.

## Installation on Stretch 4

Install the standard package requirements on the robot as follows; no need for a specific `torch` installation.

### 1. Create a Virtual Environment

Navigate to the `stretch4_grasping_demo` directory and create a new Python virtual environment:

```bash
cd ~/repos/stretch4_grasping_demo
python3 -m venv .venv
source .venv/bin/activate
```

### 2. Install Standard Dependencies

Install the remaining required packages using the provided `requirements.txt`:

```bash
pip install -r requirements.txt
```

### 3. Install Local Repositories

Install the related Stretch 4 packages in editable mode:

```bash
pip install -e ../stretch4_compliant_gripper/
```

```bash
pip install -e ../stretch4_flying_gripper/
```

## Installation on Remote Desktop (Ubuntu 24.04 + RTX 5090)

Install both the standard package requirements and the visual servoing perception system packages on your desktop computer. The visual servoing perception system used by `grasping_demo.py` and `recv_and_molmo_sam2_gripper_images.py` requires a powerful GPU. These instructions are tailored for an NVIDIA GeForce RTX 5090 running on Ubuntu 24.04.

### 1. Create a Virtual Environment

Navigate to the `stretch4_grasping_demo` directory and create a new Python virtual environment:

```bash
cd ~/repos/stretch4_grasping_demo
python3 -m venv .venv
source .venv/bin/activate
```

### 2. Install PyTorch (CUDA 12.8)

The RTX 5090 (Blackwell architecture) requires at least CUDA 12.8. Ensure your `.venv` is activated, then run:

```bash
# Upgrade pip first
pip install --upgrade pip

# Install PyTorch with CUDA 12.8 support
pip install --pre --force-reinstall torch torchvision torchaudio
```

*(Note: Once a stable release of PyTorch with built-in cu128 or higher support becomes available, you can replace this with the standard stable installation command.)*

### 3. Install Standard Dependencies

Install the remaining required packages using the provided `requirements.txt`:

```bash
pip install -r requirements.txt
```

### 4. Install Local Repositories

Install the related Stretch 4 packages in editable mode:

```bash
pip install -e ../stretch4_compliant_gripper/
```

```bash
pip install -e ../stretch4_flying_gripper/
```

## Running the Code

### Networking Configuration

This grasping demo relies on a high-bandwidth, low-latency connection between the robot and the remote computer. A dedicated, high-performance WiFi access point is recommended.

After you have ensured that the robot and the remote computer are connected and able to communicate with each other, you can proceed to edit the IP addresses in `gripper_networking.py` to match the robot's IP address and the remote computer's IP address.

This file should be edited on both the robot and the remote computer. The file is in the `src/stretch4_compliant_gripper/` directory of the stretch4\_compliant\_gripper repository.

You can see the file on GitHub via the following link:

[stretch4\_compliant\_gripper/src/stretch4\_gripper\_modeling\_and\_control/gripper\_networking.py](https://github.com/hello-robot/stretch4_compliant_gripper/blob/main/src/stretch4_gripper_modeling_and_control/gripper_networking.py)

The two variable to change are at the top of the file, as shown in the following excerpt:

```bash
# Set these values for your network
robot_ip = '100.90.83.97'
remote_computer_ip = '100.69.89.24'
```

Once networking is configured, you can proceed to copy the gripper calibration files from the robot to the remote computer.

### Sync the Gripper Calibration Files

On your remote computer, run the following script to copy the gripper calibration files from the robot to the desktop:

```bash
python3 ../stretch4_compliant_gripper/sync_calibration_models.py
```

This tool automatically connects to the robot using the IP specified in `gripper_networking.py`, downloads the calibration files in a single `scp` command (minimizing password prompts), and extracts the correct `HELLO_FLEET_ID` directly from the downloaded data. It then automatically organizes the models into your desktop's local `~/stretch_user/<robot_id>/calibration_gripper` directory, keeping both machines in sync without requiring you to manually specify the robot's ID.

### Start Robot-Side Services

On the Stretch robot (in the `stretch4_compliant_gripper` repository directory), open two separate terminals and run:

**Terminal 1 (Receive Commands):**

```bash
python3 recv_and_execute_gripper_commands.py --remote
```

**Terminal 2 (Publish Images & States):**

```bash
python3 send_gripper_images_and_joint_states.py --remote
```

### Test the Perception System (Desktop)

Prior to making the robot move via closed-loop control, you should first test the perception system. On your remote desktop computer, ensure your `.venv` is activated, then run the following script with an OBJECT\_DESCRIPTION that describes the object you want the robot to grasp. Prior to running the script, the target object should be in view of the robot's gripper camera. This script will prompt Molmo 2, use the pixel coordinates it provides to prompt SAM 2, and then use SAM 2 to track and segment the target object over time. The results will be visualized in a window.

While the script is running, you can move the object around, occlude it, deform it, and otherwise manipulate it to test the robustness of the perception system.

> A text description of the target object (OBJECT\_DESCRIPTION) should be provided on the command line.

```bash
source ~/repos/stretch4_grasping_demo/.venv/bin/activate
python3 recv_and_molmo_sam2_gripper_images.py --remote OBJECT_DESCRIPTION
```

The object description text is used to prompt the Molmo 2 VLM. Examples of object descriptions that have been used successfully follow.

**Example Object Descriptions:**

* "sunscreen"
* "cleaning wipe container"
* "coffee mug"
* "white plastic cup"
* "black rubber rocket with red nozzle"
* "brown paper cup"

**The VLM Prompt**

The full Molmo 2 prompt is defined by the `get_molmo_pointing_prompt(object_description)` function in [vlm\_utils.py](https://github.com/hello-robot/stretch4_grasping_demo/blob/main/vlm_utils.py). Advanced users can edit this prompt to better match their application.

### Run Visual Servoing (Desktop)

If the previous test of the perception system was successful at tracking and segmenting the target object at a high frame rate with low latency, you can proceed to run the visual servoing finite state machine (FSM) that commands the robot to move and attempts to grasp the object.

First, make sure that the perception system test code is no longer running.

Then, in a terminal on your remote desktop, activate the environment and run the visual servoing FSM. As with the perception test, a text description of the target object (OBJECT\_DESCRIPTION) should be provided on the command line.

> \[WARNING] This visual servoing demo does not avoid obstacles. It is strictly driven by joint states and depth and RGB camera images from the wrist-mounted camera. Make sure that the scene is clear of obstacles before running this script. You should also be prepared to stop the code and run-stop the robot via the button on the robot's head.

```bash
source ~/repos/stretch4_grasping_demo/.venv/bin/activate
python3 grasping_demo.py --remote OBJECT_DESCRIPTION
```

**Optional Arguments:**

* `--move_to_front`: By default, the robot will approach the object directly. Passing this flag enables an exploration phase where the robot moves sideways to find the front face of the object (minimizing the apparent width) before approaching.

### Testing Manual Gripper Control (Optional)

To test the gripper control manually, you can plug a gamepad dongle into the desktop and run the `send_gripper_commands.py` script from the installed model repository:

```bash
cd ../stretch4_compliant_gripper
python3 send_gripper_commands.py --remote
```


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# Stretch 4 Gripper Modeling and Control

This repository provides tools and utilities for modeling, calibrating, and controlling the standard compliant gripper on the Stretch 4 robot. It includes capabilities for kinematic modeling of the fingertips, teleoperation, and visually estimating fingertip configurations via the wrist-mounted gripper camera.

## Table of Contents

1. [Installation](#installation)
2. [Calibrate the Kinematic Model](#calibrate-the-kinematic-model)
3. [Visualizing Fingertips](#visualizing-fingertips)
4. [Estimating Fingertip Loads](#estimating-fingertip-loads)
5. [Teleoperation](#teleoperation)
6. [Synergy with Grasping Demo](#synergy-with-grasping-demo)

***

## Installation

This repository has been structured as a pip-installable Python package. Collecting gripper calibration data, fitting the calibrated gripper kinematic model, and running teleoperation scripts can be done with or without a remote desktop connection.

For integration with `stretch4_grasping_demo`, you should install this repository on **both** the Stretch 4 robot's onboard NUC computer (Ubuntu 24.04) and your offboard desktop machine (Ubuntu 24.04) running the GPU-accelerated code.

### Prerequisites

Both machines must be running Ubuntu 24.04 and have Python 3.10+ installed. Ensure you have the `stretch_body` ecosystem installed and configured, particularly `HELLO_FLEET_PATH` and `HELLO_FLEET_ID` environment variables.

### Other Dependencies

Please clone and install the following dependencies in the virtual environment associated with this package:

* `stretch4_flying_gripper`: <https://github.com/hello-robot/stretch4\\_flying\\_gripper>

### Installation Steps

1. Clone the repository to your machine:

   ```bash
   git clone <repository_url>
   cd stretch4_gripper_modeling_and_control
   ```
2. Activate the appropriate Python virtual environment:
   * **On the desktop:** You should install this into the existing `stretch4_grasping_demo` virtual environment:

     ```bash
     source ~/repos/stretch4_grasping_demo/.venv/bin/activate
     ```
   * **On the robot:** If you are using a virtual environment for your Stretch software, activate it. Otherwise, if creating a new one:

     ```bash
     python3 -m venv .venv
     source .venv/bin/activate
     ```
3. Install the package in editable mode:

   ```bash
   pip install -e .
   ```

   *(Note: This automatically installs all required dependencies like `numpy`, `opencv-python`, `scipy`, `pyyaml`, and `pyzmq`.)*

### Network Configuration

If you plan to use this code with a remote machine connected to the robot, you need to configure the network settings in `src/stretch4_gripper_modeling_and_control/gripper_networking.py`. `robot_ip` should be set to the robot's IP address and `remote_computer_ip` should be set to the remote computer's IP address.

The two variables to change are at the top of the file, as shown in the following excerpt:

```bash
# Set these values for your network
robot_ip = '100.90.83.97'
remote_computer_ip = '100.69.89.24'  # if using a remote desktop computer
```

## Calibrate the Kinematic Model

Because the flexible fingers on the gripper deform non-linearly, this repository utilizes a data-driven approach to fit a 3D kinematic B-Spline model.

Prior to running the scripts below, the robot needs to be homed. You can home the robot by running the following command in a terminal:

```bash
stretch_robot_home
```

### 1. Data Collection

On the robot, first run the following code to send high-resolution images and synchronized joint states:

```bash
python3 send_gripper_images_and_joint_states.py --resolution 800
```

Then, in a separate terminal, run the data collection script:

```bash
python3 capture_gripper_data.py
```

This automatically exercises the gripper through its full range of motion while recording synchronized joint states and ArUco visual estimations. It generates a timestamped `calib_dir` containing the raw YAML data.

### 2. Fit the Kinematic Model

Once data is collected, fit the kinematic model to the data that you just collected:

```bash
python3 fit_fingertip_model.py <calib_dir>
```

*Motivation:* This script uses robust RANSAC to fit a generic plane to the fingertip trajectories, then uses univariate splines to smoothly map the actuator's kinemaic state (i.e., `pos_pct`) to kinematic predictions of the fingertips' frames of reference. It saves the resulting model as `latest_model_planar.yaml` in your robot's `$HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_gripper/` directory.

### 3. Fit Mirror Transforms

To robustly predict one fingertip from the other (in case one is occluded), the following code estimates mirror transforms that predict the right fingertip's frame from the left's and vice versa.

```bash
python3 fit_mirror_transforms.py
```

*Motivation:* This computes the SE(3) mirror symmetries of the gripper mechanism. It saves `latest_mirror_transforms.yaml` to the same calibration directory.

### 4. Inspect the Results

At this point, it is a good idea to visually inspect the results of the kinematic calibration.

First, use ReRun to visualize the 3D kinematic model via the following command. The visualization shows the camera's frame of reference, the plane in which the fingers move, and the suction cup fingertips with their frames of reference.

```bash
python3 visualize_fingertip_model.py
```

Second, use the following command to generate a video overlaying the kinematic model estimates onto the gripper images that were used for calibration. The rendered suction cups in blue should closely match the appearance of the actual suction cups in the images. The resulting video can be found within the calibration directory.

```bash
python3 create_model_video.py
```

If the calibration results do not appear to be accurate, you can use the following command to generate a video that visualizes the visually-estimated fingertip frames of reference used for calibration. The resulting video can be found within the calibration directory. If the suction cups in this video do not appear to be correctly positioned, you should re-run the calibration procedure. For example, poor lighting can result in poor fingertip estimation based on the ArUco markers, leading to a poor kinematic model.

```bash
python3 create_calibration_video.py <calib_dir>
```

### 5. Syncing to a Desktop

To use these calibration files on your offboard desktop, simply run the provided sync tool on your **desktop**:

```bash
python3 sync_calibration_models.py
```

This tool automatically connects to the robot using the IP specified in `gripper_networking.py`, downloads the calibration files in a single `scp` command (minimizing password prompts), and extracts the correct `HELLO_FLEET_ID` directly from the downloaded data. It then automatically organizes the models into your desktop's local `~/stretch_user/<robot_id>/calibration_gripper` directory, keeping both machines in sync.

***

## Visualizing Fingertips

To see the real-time visual estimates of the fingertips and the rendered suction cups overlaid on the camera feed, you can use the following scripts.

1. **`recv_and_detect_fingertips.py`**: This script receives live images from the robot, runs the ArUco marker detection, and draws the visually-estimated fingertip frames of reference on the images.
2. **`visualize_fingertip_depth_range.py`**: This script can visualize the visually-estimated fingertip frames, the kinematically-estimated fingertip frames, and the swept volume of the fingertips associated with closing the gripper. While running this visualization, move an object into the gripper as though the gripper were about to grasp it. The pixels on the object's surface that fall within the swept volume of the fingertips should be highlighted in the video.

***

## Estimating Fingertip Loads

In one terminal, the following command should be run:

```bash
python3 send_gripper_images_and_joint_states.py
```

Then in another terminal, the user can run one of the following commands:

```bash
python3 estimate_fingertip_loads.py
```

or:

```bash
python3 estimate_fingertip_loads.py --mode cross_hud
```

Both of these commands visualize how the kinematically predicted fingertip poses differ from the visually estimated fingertip poses. Because the fingers are compliant, these differences correspond with the loads applied to the gripper fingers. Running the script without any command line arguments visualizes differences related to the applied grip force. Running the script with `--mode cross_hud` visualizes quantities associated with other types of loads applied to the finger tips, such as when the gripper makes contact with horizontal or vertical surfaces.

***

## Teleoperation

You can teleoperate the robot's gripper using an Xbox-style gamepad connected to your remote desktop. This relies on the control logic provided by the `stretch4_flying_gripper_control` package.

### Local Teleoperation

1. In **Terminal 1** (on the robot): Start the command receiver to listen for incoming UDP/ZMQ joint targets:

   ```bash
   python3 recv_and_execute_gripper_commands.py
   ```
2. In **Terminal 2** (on the robot): Run the gamepad mapping script to broadcast your controller inputs:

   ```bash
   python3 send_gripper_commands.py
   ```

### Remote Teleoperation

1. On the **Robot**: Start the command receiver to listen for incoming UDP/ZMQ joint targets:

   ```bash
   python3 recv_and_execute_gripper_commands.py --remote
   ```
2. On the **Desktop**: Run the gamepad mapping script to broadcast your controller inputs:

   ```bash
   python3 send_gripper_commands.py --remote
   ```

   *Motivation:* This enables you to test that the desktop can control the gripper with acceptable latency and fidelity.

***

## Synergy with Grasping Demo

This repository is designed to run in conjunction with code in the `stretch4_grasping_demo` repository. The grasping demo relies on the network streams and calibration models generated here. The two repositories are packaged in such a way that you can execute a full remote grasping pipeline by running the following commands across three terminal windows.

**ON YOUR DESKTOP MACHINE** *(Ensure you are in the `stretch4_gripper_modeling_and_control` repository)*

```bash
python3 sync_calibration_models.py
```

**ON THE ROBOT: TERMINAL #1** *(Starts listening for gripper commands)*

```bash
python3 recv_and_execute_gripper_commands.py --remote
```

**ON THE ROBOT: TERMINAL #2** *(Starts broadcasting the camera feed and joint telemetry)*

```bash
python3 send_gripper_images_and_joint_states.py --remote
```

**ON THE DESKTOP IN THE stretch4\_grasping\_demo REPOSITORY** *(Sync the latest calibration models from the robot first!)*

```bash
# On your desktop, within this repository:
python3 sync_calibration_models.py

# Then, switch to your grasping demo repository:
source ~/repos/stretch4_grasping_demo/.venv/bin/activate
python3 visually_servo_gripper.py --model ~/stretch_user/stretch-se4-4010/calibration_gripper/latest_model_planar.yaml --grasp_estimator ellipsoid_2d --remote OBJECT_DESCRIPTION
```

*(Note: Adjust the model path to match your specific `HELLO_FLEET_ID` as synced).*


# LICENSE

The following license applies to the entire contents of this directory (the "Contents"), which contains software for use with Stretch mobile manipulators, which are robots produced and sold by Hello Robot Inc.

Copyright 2026 Hello Robot Inc.

The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at

<http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc.


# README

The stretch4\_install repository provides scripts required to install the Stretch 4 software.

### Guides

| Guide                                                                   | Purpose                                                                                    |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| [Adding a New User](/stretch4_install/docs/add_new_user)                | Creating a new Ubuntu user and setting it up with Stretch packages and robot configuration |
| [Updating your ROS Workspace](/stretch4_install/docs/ros_workspace)     | Creating and compiling a new ROS workspace                                                 |
| [Upgrading your Operating System](/stretch4_install/docs/robot_install) | Installing a new operating system and setting it up with the full Stretch software stack   |

### Contributing

Thank you for considering contributing to this repo! Please take a look at the [contribution guide](/stretch4_install/docs/contributing) for more details.

### License

All Hello Robot installation materials are released under the GNU General Public License v3.0 (GNU GPLv3). Details can be found in the LICENSE file.


# docs


# Adding a New User

## Why

If you're sharing Stretch with other developers, it can be helpful to create separate accounts for yourself and your team members. Your code and data will be password protected in your own account, and other developers can modify their own code without accidentally affecting yours.

!!! warning

```
User accounts cannot completely insulate your account from changes in another. For example, if someone attaches a new gripper or end-effector tool to the robot, your account's software would have an outdated configuration for what tool is attached to the robot. A non-exhaustive list of changes that could break/affect accounts:

  - Making hardware changes to the robot
  - Updating the firmware
  - Installing/changing [APT packages](#apt-package-manager)
```

## How

From the admin account, open a terminal and pull down the latest Stretch Install repository:

```{.bash
cd ~/stretch4_install && git pull
```

Run the new user install script and provide the new username using the `-u` flag. This will automatically create the user account (prompting for their new password), provide them admin privileges, set up their home directory, and install the new user environment:

```{.bash
./stretch_new_user_install.sh -u new_developer_name
```

Finally, log out and log back in as the new user account. Reboot the robot and run a system check to confirm everything was set up correctly.

```{.bash
stretch_system_check.py
```

### Calibration Data Copy

During the installation process, the setup script automatically runs the `stretch_copy_calibration.sh` utility. This utility copies the calibration and robot configuration files from the main `hello-robot` account (specifically from `/home/hello-robot/stretch_user/$HELLO_FLEET_ID`) to the new user's account. This ensures that the new user has access to all current robot calibrations (e.g. stepper calibrations, camera extrinsics) rather than defaulting to factory configuration.

The `stretch_copy_calibration.sh` script supports the following command-line flags:

* `-s, --source USER` : Source user account to copy calibration from (default: `hello-robot`).
* `-u, --user USER` : Target user account to copy calibration to (default: current user).
* `-f, --force` : Force the copy to run without prompting the user. If this flag is omitted, the script lists the files to be copied and prompts the user for confirmation.

If you ever need to manually copy or sync the calibration files from the `hello-robot` account to a new user account later, you can run the utility script with `sudo` at any time:

```{.bash
sudo ~/stretch4_install/stretch_copy_calibration.sh -u new_developer_name
```

Your new user account is now set up successfully!

## Manually add a new user

If you would like to manually create the new user and setup the account, you can follow the steps below:

Start by logging into the admin Hello Robot user. Go to Users system settings and unlock adminstrator actions.

![](/files/FznjMiMSLtOJA5JKiYHY)

Click "Add User..." and complete the subsequent form. The new user needs to be an administrator.

![](/files/qtFtfph02wfQd7p0cfQO)

Log out and back in as the new user. Open a terminal and execute the following to pull down the Stretch Install repository:

```{.bash
git clone https://github.com/hello-robot/stretch4_install ~/stretch4_install
```

Make sure it's up-to-date:

```{.bash
cd ~/stretch4_install && git pull
```

Run the new user install script to set up the SDK for this new account:

```{.bash
./stretch_new_user_install.sh
```

Finally, reboot the robot and run a system check in the new user account to confirm everything was set up correctly.

***

All materials are Copyright 2020-2026 by Hello Robot Inc. Hello Robot and Stretch are registered trademarks.


# Configuring the BIOS

This documentation describes how to configure the BIOS of an Intel NUC for compatibility with the stretch installation procedure.

## Accessing the NUC BIOS Settings

First plug in the NUC to a 19V DC power supply. Next power on the NUC using the power button on the front of the NUC.

When powered on, the NUC should display a welcome screen similar to the picture below:

![](/files/AxtsiwmbEfXUqap1qwtH)

When this label becomes visible press 'F2' to enter into the BIOS configuration menu.

!!! note

```
If you're using a Bluetooth keyboard, the BIOS likely won't recognize the F2 keypress.
```

The BIOS Settings page should look like the picture below:

![](/files/CRiMWGiocmvcHcTm58Fp)

Select the 'Advanced' drop down menu near the top right of the screen, and then slect the option 'Boot'

![](/files/0nXekZpwyPdxLAJ4pMut)

From the 'Boot' settings page select the 'Secure Boot' tab.

![](/files/4h0zmKk2JwHbxjGKl8zL)

Turn off 'Secure Boot' by toggling the checkbox labeled 'Secure Boot' to unchecked.

![](/files/8KA7LcPrgx8AwqtgEmHx)

Next Select the 'Power' tab

![](/files/qlSx8OhYWMpYROp7XDEi)

From the power settings screen select the 'Power On' option from the 'After Power Failure' drop down selection.

![](/files/qeIgDqyeXYPsDdfLmrNo)

Next Select the Security tab

![](/files/IeJ8tsswbG91ygBWjwe2)

Turn on UEFI third party drivers compatibility by toggling the checkbox labeled 'Allow UEFI Third Party Driver loaded' to checked.

![](/files/jJvJZYtdb3kSRVR2iS0u)

Now use the F10 key to save BIOS configuration changes and exit.

***

All materials are Copyright 2020-2026 by Hello Robot Inc. Hello Robot and Stretch are registered trademarks.


# Contributing to Stretch Install

Thank you for considering contributing to this repository. Stretch Install houses bash scripts and tutorials that enable users to setup/configure their robots. This guide explains the layout of this repo and how best to make and test changes.

## Repo Layout

* `README.md` & `LICENSE.md` - includes info about the repo and a table of tutorials available
* `stretch_new_*_install.sh` - high level scripts meant to be run by the user
* `factory/` - subscripts and assets not meant to be run by the user
  * `24.04/` - subscripts and assets specific to performing a Ubuntu 24.04 software install
    * `stretch_initial_setup.sh` - a bunch of checks and initial setup that are run before performing a robot install
    * `stretch_install_*.sh` - helper scripts that install a specific set of packages
    * `stretch_create_*_workspace.sh` - creates a ROS/ROS2 workspace
    * `stretch_ros*.repos` - the ROS packages that are included and compiled in the ROS workspace by the `stretch_create_*_workspace.sh` script
    * `hello_robot_*.desktop` - autostarts programs to run when the robot boots up
  * `<>.04/` - Ubuntu <>.04 software install related subscripts/assets. Similar in layout to 24.04/
* `docs/` - contains tutorials for using the scripts in this repo

Once you're ready to make changes to this repo, you can [fork it on Github](https://github.com/hello-robot/stretch_docs/fork).

## Contributing to the tutorials

The tutorials in the `docs/` folder are markdown files. You can make additions or changes to the source markdown files and see the changes reflected live on Github.

## Contributing to the installation scripts

If you are looking to change scripts/assets of an existing software installation (e.g. Ubuntu 24.04), look within the `factory/<>.04/` directory and make changes to the appriopriate files. If you're looking to add support for a new Ubuntu distro (e.g. Ubuntu 25.04), create `factory/25.04` with assets from a previous installation and tweak the scripts until they works correctly for the Ubuntu distro you are targeting. Then, edit the high level scripts (e.g. `stretch_new_*_install.sh`) to call your distro's specific assets correctly. Ensuring that the tutorials in the `docs/` work for your new distro is a good way to ensure that your `factory/<>.04/` directory works correctly. Since bash scripts change behavior based on the underlying system, it can be helpful to use containers to create reproducible behaviors while you're developing support for the new distro. [Multipass](https://multipass.run/install) works well on Ubuntu systems. You can create a new container emulating any Ubuntu distro using the command:

```bash
multipass launch -c 6 -d 30G -m 16G -n <container-name> 25.04
```

Swap `25.04` in the above command with the distro you're targeting. The above command creates a containers with 30GB disk space, 16GB swap, 6 cores, and the name `<container-name>`. We've found that at least 16GB swap and 30GB disk space is needed for the Ubuntu 24.04 installation.

Then, you can access the shell of your new container using:

```bash
multipass shell <container-name>
```

Other helpful multipass subcommand include `transfer`, which allows you to transfer files to the container, and `delete`, which allows you to delete the container. See the [multipass docs](https://multipass.run/docs) for more details.

## Filing a Pull Request

Once your changes are committed to your fork, you can [open a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) towards the Stretch Install main branch. A member of Hello Robot's software team will review your new PR and get it merged and available for all Stretch users.

***

All materials are Copyright 2020-2026 by Hello Robot Inc. Hello Robot and Stretch are registered trademarks.


# Ubuntu 24.04 Installation

This guide describes how to perform an OS installation of Ubuntu 24.04 LTS onto Stretch.

## Ubuntu Image

Download the [24.04.3 amd64 Ubuntu desktop image](https://releases.ubuntu.com/jammy/).

Create a bootable drive with this Ubuntu image. There are many ways to do this, but the recommended way is to use [Etcher](https://www.balena.io/etcher/) on your personal machine. Open the Etcher software and follow it's instructions to create the bootable drive. There is a good video tutorial [available here](https://youtu.be/c0TK0ynXLOo) that explains the procedure.

## Installation

1. Insert a bootable drive into a USB port in the robot's trunk, as well as a monitor and keyboard. Next, power on the robot and at the bios startup screen (shown below) press *F10* when prompted to enter the boot menu.

   * ![](/files/NO7yOsiST1pmYC7dczpQ)

   !!! note

   ```
   If you're using a Bluetooth keyboard, the BIOS likely won't recognize the F10 keypress.
   ```
2. From the boot menu, select 'OS BOOTLOADER' or look for a similar option that mentions "USB", "LIVE INSTALLATION", or "UBUNTU". This will take you to the grub menu.
   * ![](/files/5uQefmXw1ecrvJeCkaiK)
3. From the grub menu, select 'Ubuntu' or look for a similar option that mentions "Install Ubuntu".
   * ![](/files/0UUt4G6hFz0sqPAoDsNh)
4. A disk errors checker will start and then the Ubuntu 24.04 installer will be launched.
   * ![](/files/gKHkqibHEGKWTGS1hNY8)
5. The first screen of the installer will prompt you to select a language for the system and choose between trying or installing Ubuntu. Select 'English' and "Install Ubuntu".
   * ![](/files/zoyxuWVpUa9hRK5q8gz8)
6. Next you will be prompted to select a keyboard layout. Select 'English(US)'.
   * ![](/files/JH6y6gxEdE5IROF4nmt9)
7. The next page will show a menu to select a Wifi network if you are not already connected. For a faster and more reliable install, we suggest using a wired connection if one is available to you.
   \*
   * Your connection status will be visible in the top right of the display.

     | Wifi            | Ethernet        |
     | --------------- | --------------- |
     | { width="400" } | { width="400" } |
8. The next page configures what gets installed. Select 'Minimal Installation' under 'What apps would you like to install to start with?' Check the box next to 'Download updates while installing Ubuntu' (this option will be unavailable if there is no interent connection) and uncheck 'Install third-party software for graphics and Wi-Fi hardware and additional media formats'.
   * ![](/files/yZYPQVQUbOV71ZTOJvlL)

### Erase & Reinstall vs Install Alongside

9. On the next page titled 'Installation type', you may choose between 'Erase disk and reinstall Ubuntu' or 'Install Ubuntu 24.04 alongside Ubuntu XX.04'. If you've already backed up data from the previous partition, or the previous partition is corrupted, select the erase & reinstall option. If you'd like to preserve your previous Ubuntu partition, select the alongside option. If you choose the alongside option, another screen will allow you to change the size of each partition. It's recommended to give each partition at least 50GB.

   | Erase & Reinstall | Install Alongside |
   | ----------------- | ----------------- |
   |                   |                   |
10. There will be a prompt to confirm you wish to create the appropriate partitions for the Ubuntu install. Clicking 'Continue' will begin making changes to your robot's hard drive, so ensure there is nothing on the hard drive you wish to save before selecting continue.
    \*
11. Next, select your timezone.
    * ![](/files/6KiOmZr9Os5b7q1joYc8)
12. Finally, enter the identifying information as written below, replacing 'stretch-yyy-xxxx' with the appropriate name for the robot. `yyy` is your robot model number ("re1" for a Stretch RE1, "re2" for a Stretch 2, or "se3" for a Stretch 3), and `xxxx` is your robot's serial number. The robot's serial number can be found on a sticker on the left wall of the robot's trunk. Also select the 'Log in automatically' option.
    * **name:** Hello Robot Inc.
    * **computer name:** stretch-yyy-xxxx
    * **username:** hello-robot
    * **password:** choose your own
    * ![](/files/8yGqQg33i9WigKj0HXWC)
13. Ubuntu will now be installed.
    * ![](/files/c7LoVHiOEvfNe1LcRJpn)
14. After the installation is completed, you will be prompted to restart. Select 'Restart Now'.
    * ![](/files/BK16s6SnMjLLslBYNL69)
15. Remove the installation medium and press ENTER to restart.
    * ![](/files/4NbroJ8qJ1Xgf3mCuFZf)

**Ubuntu 24.04 is now installed successfully.**

***

All materials are Copyright 2020-2026 by Hello Robot Inc. Hello Robot and Stretch are registered trademarks.


# Upgrading your Operating System

## Why

This guide will lead you through installing a new robot distribution, which can be used to:

* Upgrade Stretch by installing a newer software stack alongside the previous OS
* Erase the previous OS and set up Stretch with an entirely fresh software stack
* Erase a corrupted OS and set up Stretch with an entirely fresh software stack

Each OS installs on a separate partition on the hard drive. You can create as many robot-level installs (i.e. new partitions) as will fit in your robot's hard drive.

## How

There are a few steps to performing a new robot install:

1. Plug in charger
2. Backup robot calibration data
3. Setup the BIOS (only necessary for NUCs not previously configured by Hello Robot)
4. Install Ubuntu
5. Run the new robot installation script

It typically takes \~2 hours to go through these steps. Before we get started, you'll need:

* 1 Stretch robot
* Keyboard, Mouse, and Monitor
  * Bluetooth keyboards/mouses will not work because they are not recognized by the BIOS. Use a wired USB or wireless USB dongle keyboard/mouse.
  * Use a computer monitor instead of a TV. Some TVs have trouble displaying Ubuntu when rebooting.
* 2 USB sticks
  * The first flashdrive is used to backup robot calibration data and any other important files
  * The second flashdrive will contain the Ubuntu installer image. This flashdrive will need to be >8gb in capacity.
* A fast connection to the internet

  !!! note

  ```
   It's recommended that you use an Ethernet connection to the internet. You can use a slow Wifi connection if it's difficult to obtain a wired connection at your institution, but expect the install process to take longer because the scripts are downloading gigabytes of software/data to the robot.
  ```

### Back up robot calibration data

It is a good idea to backup all valuable data beforehand. If your new robot install will replace a previous one, **data from the previous robot install will be deleted.** Even if your new robot install will live alongside the previous one(s), **data from the previous robot install(s) can be lost.**

In particular, your new robot install will require the old install's robot calibration data. The steps to copy this material from an existing install is:

1. Boot into the robot's original Ubuntu partition and plug in a USB key.
2. The robot calibration data lives inside of a directory called `stretch-<yyy>-<xxxx>`, where `<yyy>` is your robot model number ("re1" for a Stretch RE1, "re2" for a Stretch 2, "se3" for a Stretch 3, or "se4" for a Stretch 4), and `<xxxx>` is your robot's serial number. There's a few versions of this directory and you will need to decide which version to backup. Each Ubuntu user has a version of this directory located at `/home/$USER/stretch_user/stretch-<yyy>-<xxxx>`. These user versions are updated when the user runs a URDF calibration, swaps out an end effector, updates Stretch parameters, and more. There's also a system version located at `/etc/hello-robot/stretch-<yyy>-<xxxx>`, which is likely the oldest version since it was created at Hello Robot HQ. If you're not sure which version to backup, use the version at `/etc/hello-robot/stretch-<yyy>-<xxxx>` for the next step.
3. Copy the `stretch-<yyy>-<xxxx>` directory to a USB key.
   * For example, if you're copying the system version, you can run a command similar to `cp -r /etc/hello-robot/stretch-<yyy>-<xxxx> /media/$USER/<USBKEY>` from the command line, where `<USBKEY>` and `<xxxx>` is replaced with the mounted USB key's name and the robot's serial number, respectively.
   * Or, you can open the file explorer to copy the directory.

If your previous partition is corrupted or inaccessible, contact Hello Robot support and they will be able to supply an older version of the `stretch-<yyy>-<xxxx>` directory.

### Setup the BIOS

This step can be skipped if your robot had an existing software install on it. Otherwise, follow the [guide to set up the BIOS](/stretch4_install/docs/configure_bios).

### Install Ubuntu

Choose between the following guides based on which version of Ubuntu you're installing. Within these guides, you'll have the choice of whether to replace the previous OS partition or to install alongside it. If you choose to install alongside it, you'll also be able to choose the size of each partition on the hard drive.

* [Ubuntu 24.04 Installation guide](/stretch4_install/docs/install_ubuntu_24.04)

After the Ubuntu install, the default `hello-robot` user account will be set up.

### Run the robot installation script

Login to the `hello-robot` user account on your new Ubuntu partition, open a terminal, and run:

```bash
sudo apt update
sudo apt install git zip network-manager
```

!!! note

```
The system may not be able to run APT immediately after a reboot as the OS may be running automatic updates in the background. Typically, waiting 10-20 minutes will allow you to use APT again.
```

Next, place the robot's calibration data in the home folder using the following steps:

1. Plug in the USB key that contains the backed up calibration data.
2. Copy the `stretch-<yyy>-<xxxx>` directory from the USB key into the home folder (i.e. `/home/$USER/`).
   * For example, you can run a command similar to `cp -r /media/$USER/<USBKEY>/stretch-<yyy>-<xxxx> /home/$USER/` from the command line, where `<USBKEY>` and `<xxxx>` are replaced with your USB key's name and your robot's serial number, respectively.
   * Or, you can open the file explorer to copy the directory.

Next, use git to pull down the [Stretch4 Install](https://github.com/hello-robot/stretch4_install) repository and begin the installation process. While Stretch 4 development is happening in private, you'll need the `credential.helper` command below which uses an Hello Robot-private access token to clone our private repos:

```bash
git clone https://github.com/hello-robot/stretch4_install ~/stretch4_install
cd ~/stretch4_install
git pull
./stretch_new_robot_install.sh
```

Once the script has started, it will ask you for your robot's serial number, Y/N confirmation, and the password. Then, the script will typically take 20-30 minutes to complete on a wired connection. Once it finishes, it should print out something similar to:

```
#############################################
DONE! INSTALLATION COMPLETED SUCCESSFULLY.
[...]
#############################################
```

If it has not printed out 'DONE', then the robot install did not complete successfully. Take a look at the [troubleshooting](#troubleshooting) section below for solutions to common issues, or contact Hello Robot support via email or [the forum](https://forum.hello-robot.com/).

Your robot is now set up with a new operating system! If you're new to Stretch, consider going through the [Getting Started](https://github.com/hello-robot/stretch4_install/blob/main/getting_started/hello_robot/README.md) tutorials.

***

## Troubleshooting

This section provides suggestions for common errors that occur during installation. If you become stuck and don't find an answer here, please email us or contact us through [the forum](https://forum.hello-robot.com/).

### 'Expecting var HELLO\_FLEET\_ID to be undefined' error

If you are seeing the following error:

```
[...]
Checking ~/.bashrc doesn't already define HELLO_FLEET_ID...
Expecting var HELLO_FLEET_ID to be undefined. Check end of ~/.bashrc file, delete all lines in 'STRETCH BASHRC SETUP' section, and open a new terminal. Exiting.

#############################################
FAILURE. INSTALLATION DID NOT COMPLETE.
[...]
```

You are performing a new robot install on a robot that has already gone through the robot install process. If this is intentional, you will need to manually delete lines that a previous robot install appended to the `~/.bashrc` dotfile. Open the `~/.bashrc` file in an editor and look near the end for a section that looks like:

```
######################
# STRETCH BASHRC SETUP
######################
export HELLO_FLEET_PATH=/home/ubuntu/stretch_user
export HELLO_FLEET_ID=stretch-re1-1000
export PATH=${PATH}:~/.local/bin
export LRS_LOG_LEVEL=None #Debug
source /opt/ros/noetic/setup.bash
source /home/ubuntu/catkin_ws/devel/setup.bash
[...]
```

Delete this section from the `~/.bashrc`. Note that it's common for other programs (e.g. Conda, Ruby) to append to your `~/.bashrc` as well, and deleting those lines accidentally can impede their functionality. Take care to only delete lines related to 'STRETCH BASHRC SETUP' section. Next, open a new terminal. Every new bash shell (i.e. the terminal you open when searching for 'Terminal' in system applications) automatically runs the commands in the `~/.bashrc` dotfile when opened, so the new terminal won't be set up with the lines that were just deleted. Now you can run a new robot install and this error should gone.

### 'Expecting stretch-yyy-xxxx to be present in the home folder' error

If you are seeing the following error:

```
[...]
Checking robot calibration data in home folder...
Expecting robot calibration stretch-yyy-xxxx to be present in the the home folder. Exiting.

#############################################
FAILURE. INSTALLATION DID NOT COMPLETE.
[...]
```

The install scripts exited before performing the robot install because it was unable to find the robot's calibration data folder, 'stretch-yyy-xxxx'. Please ensure you have [backed up your robot's calibration data](#back-up-robot-calibration-data) to a USB key and copied the 'stretch-yyy-xxxx' folder to the home folder of your new partition. See the [Run the robot installation script](#run-the-robot-installation-script) section for more details. Then, run the install scripts again and the error should be gone.

### 'Repo not up-to-date' error

If you are seeing the following error:

```
[...]
Checking install repo is up-to-date...
Repo not up-to-date. Please perform a 'git pull'. Exiting.

#############################################
FAILURE. INSTALLATION DID NOT COMPLETE.
[...]
```

The version of Stretch Install being used is out of date. In a terminal, go to the Stretch Install folder (should be in the home folder: `cd ~/stretch4_install`), and perform a `git pull` to pull down the latest version. If the git pull fails, ensure Stretch Install has a clean working tree using `git status`. If you see any red files, save them if important, delete Stretch Install, and reclone it.

### 'Failed to fetch' error

If you are seeing the following error:

```
Install <some package>
E: Failed to fetch <url to some .deb file>  Connection failed [IP: <some IP address>]
E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?

#############################################
FAILURE. INSTALLATION DID NOT COMPLETE.
[...]
```

Ubuntu's system package manager, Apt, has failed to contact the server that hosts some package that the install scripts need to download. Typically, these issues are transient and waiting some time before rerunning the install script will solve the issue.

### 'dpkg returned an error code' error

If you are seeing the following error:

```
Install <some package>
E: Sub-process /usr/bin/dpkg returned an error code (1)

#############################################
FAILURE. INSTALLATION DID NOT COMPLETE.
[...]
```

Ubuntu's system package manager, Apt, has failed to complete some step of the install process for a package that the install scripts need to install. Typically, these issues are transient and waiting some time before rerunning the install script will solve the issue. If you continue to see this error, contact Hello Robot support via email or [the forum](https://forum.hello-robot.com/).

### 'Firmware protocol mismatch' error

If you are seeing the following error:

```
----------------
Firmware protocol mismatch on hello-<X>.
Protocol on board is p<X>.
Valid protocol is: p<X>.
Disabling device.
Please upgrade the firmware and/or version of Stretch Body.
----------------
```

Contact Hello Robot Support.

***

All materials are Copyright 2020-2026 by Hello Robot Inc. Hello Robot and Stretch are registered trademarks.


# Updating your ROS Workspace

## Why

ROS1 and ROS2 organize software by "workspaces", where ROS packages are developed, compiled, and made available to run from the command line. By default, a ROS1 workspace called `catkin_ws` is available in the home directory. Similarly, a ROS2 workspace called `ament_ws` is available in the home directory. The operating system installed (and therefore version of ROS installed) on your robot dictates whether you'll have a `catkin_ws` or `ament_ws` folder.

This guide will show you how to replace existing or create new ROS1/2 workspaces for developing ROS software.

## How

Open a terminal and execute the following.

```bash
cd ~
git clone https://github.com/hello-robot/stretch4_install
cd stretch4_install
git pull
git checkout main
git pull
./stretch_update_ros_workspace.sh
```

## Wrap up

Close your current terminal and open a new one. The new terminal will have automatically activated the ROS workspace(s).

Your new ROS workspace is now set up successfully!

***

## Troubleshooting

This section provides suggestions for common errors that occur during installation. If you become stuck and don't find an answer here, please email us or contact us through [the forum](https://forum.hello-robot.com/).

### UV not found failure

If you see the errors regarding the `uv` CLI not being found in your log file (from `~/stretch_user/log/stretch_create_ament_workspace.<timestamp>_log.txt`), follow the [uv installation instructions](https://docs.astral.sh/uv/#getting-started) and then try updating the ROS workspace again.

### Rosdep failure on RTabMap

After a failure, if you see the following error in your log file (from `~/stretch_user/log/stretch_create_ament_workspace.<timestamp>_log.txt`):

```
ERROR: the following rosdeps failed to install
  apt: command [sudo -H apt-get install -y ros-humble-rtabmap-ros] failed
```

Open a terminal, run `sudo apt install ros-humble-rtabmap-ros`, and then try updating the ROS workspace again.

### 'Conflicting ROS version sourced' error

If you are seeing the following error:

```
###########################################
CREATING <ROS VERSION> WORKSPACE at <WS DIR>
###########################################
[...]
Ensuring correct version of ROS is sourced...
Cannot create workspace while a conflicting ROS version is sourced. Exiting.
```

The ROS workspace is not created because the check that a conflicting ROS version isn't already sourced has failed. For example, if you're creating an ROS2 Ament workspace, but ROS1 Noetic was previously sourced in the same environment, the check will error out since the new ROS2 workspace would fail to find its dependencies correctly in this environment. Sourcing a version of ROS typically happens using the following command: `source /opt/ros/<ros version>/setup.bash`. If you ran this command to source a conflicting version previously, simply open a new terminal and the new environment won't have the conflicting ROS version sourced. If you didn't run this command and you're still getting the error, it's likely because the command exists in the `~/.bashrc` dotfile. Every new bash shell (i.e. the terminal you open when searching for 'Terminal' in system applications) runs the commands in the `~/.bashrc` dotfile. Look at the bottom of this dotfile for this command, comment it out temporarily, and open a new terminal. This new shell environment should have no trouble creating the ROS workspace.

### 'ROS\_DISTRO was set before' warning

If you are seeing the following warning:

```
ROS_DISTRO was set to '<ROS VERSION>' before. Please make sure that the environment does not mix paths from different distributions.
```

Multiple versions of ROS are being sourced in the same environment. This is known to cause issues with the `rosdep` tool, and might cause issues elsewhere as well. If you haven't explicitly sourced conflicting versions by using the `source /opt/ros/<ros version>/setup.bash` (a variant on this command could look like `source ~/<ws dir>/develop/setup.bash`) command twice, then it's likely that one or two versions of ROS are implicitly being sourced in the `~/.bashrc` dofile. Every new bash shell (i.e. the terminal you open when searching for 'Terminal' in system applications) runs the commands in the `~/.bashrc` dotfile. Look at the bottom of this dotfile for the `source` command and ensure conflicting versions aren't being sourced.

***

All materials are Copyright 2020-2026 by Hello Robot Inc. Hello Robot and Stretch are registered trademarks.


# Stretch 4 Emulated RGB-D

This repository contains the software for optimizing and visualizing RGB-D images on the Stretch 4 robot. RGB-D images are created by combining images from a head-mounted RGB camera with scans from one or two of the head-mounted LiDAR sensors. The code provides methods to generate temporally-synchronized and spatially-aligned RGB-D images at 10Hz with low latency.

Tools to optimize the rigid-body alignment between an RGB camera and a LiDAR sensor (i.e., camera-LiDAR extrinsics) achieve spatial alignment between the RGB image and depth image components of the emulated RGB-D images. Specialized data capture and synchronization code provide RGB-D images with temporally-synchronized RGB image and depth image componentsat at up to 10Hz (i.e., the maximum LiDAR sensor frame rate) with low latency.

## Table of Contents

* [Installation](#installation)
  * [Option 1: Standard Pip Installation](#option-1-standard-pip-installation)
  * [Option 2: Automated Install Script](#option-2-automated-install-script)
* [Usage](#usage)
  * [1. Data Capture](#1-data-capture)
  * [2. Preprocessing](#2-preprocessing)
  * [3. Visualize Data](#3-visualize-data)
  * [4. Optimize the Camera-LiDAR Extrinsics](#4-optimize-the-camera-lidar-extrinsics)
  * [5. Visualize the Optimization Results](#5-visualize-the-optimization-results)
  * [6. Installation of Optimized Calibrations (On Robot)](#6-installation-of-optimized-calibrations)
  * [7. Validity Mask Estimation (On Robot)](#7-validity-mask-estimation-on-robot)
  * [8. Visualize Live RGB-D Imagery with the New Calibration (On Robot)](#8-visualize-live-rgb-d-imagery-with-the-new-calibration-on-robot)
* [API Usage and Reference](#api-usage-and-reference)
* [Configuration (`emulated_rgbd_config.py`)](#configuration-emulated_rgbd_configpy)
  * [Extrinsic Optimization Parameters](#extrinsic-optimization-parameters)
  * [Depth Alignment & Spatial Corrections](#depth-alignment--spatial-corrections)
  * [Sparsity Shadow Filter](#sparsity-shadow-filter)
  * [Native Image Orientation](#native-image-orientation)
* [Spatial Alignment Overview](#spatial-alignment-overview)
  * [Motivation](#motivation)
* [Temporal Quality Overview](#temporal-quality-overview)
  * [Frame Rate & Phase Alignment](#frame-rate--phase-alignment)
  * [Latency](#latency)
  * [High-Frequency Pipeline Methods](#high-frequency-pipeline-methods)
* [Extrinsic Calibration Details](#extrinsic-calibration-details)
  * [Example Output File](#example-output-file)
  * [Mathematical Interpretation and Application](#mathematical-interpretation-and-application)

## Installation

Many of the optimization and visualization components of this repository can be run on a desktop computer without installing the `stretch4_body` package.

### Option 1: Standard Pip Installation

If you already manage your own virtual environments (e.g., via `conda`, `venv`, or `pyenv`), you can install the package directly using `pip`. This will automatically install the package and its minimal dependencies as defined in `setup.py`:

```bash
pip install -e .
```

> **Note for On-Robot Usage:** If you are running this on the Stretch 4 robot to capture data, the script requires the `stretch4_body` system package. Make sure to create your virtual environment with the `--system-site-packages` flag (e.g., `python3 -m venv --system-site-packages venv`) so it can access system-level dependencies.

### Option 2: Automated Install Script

Alternatively, to automatically create a new isolated virtual environment and install the minimal set of dependencies into it, run the provided install script. This script automatically uses the `--system-site-packages` flag, making it ideal for both desktop and on-robot usage:

```bash
./install_dependencies.sh
```

To activate the environment created by the script:

```bash
source venv/bin/activate
```

## Usage

### 1. Data Capture

To capture raw data for alignment from the robot, use the following script. The system supports calibration of either the left or right camera and LiDAR combinations.

*(Note: It's important to capture images of a static scene. For better results, the images should include foreground objects with prominent depth edges across the RGB camera's field of view).*

Run the script, position the robot to a view you want to capture, and then press the space bar to capture the view for calibration. Repeat this process to acquire at least two more views and then press 'Q' to quit.

To capture using the left camera and left LiDAR:

```bash
python3 scripts/capture_emulated_rgbd.py --camera left --lidar left
```

To capture using the right camera and right LiDAR:

```bash
python3 scripts/capture_emulated_rgbd.py --camera right --lidar right
```

*(Note: This must be run on the Stretch 4 robot. It requires `stretch4_body` and the use of the robot's cameras and LiDARs)*

### 2. Preprocessing

Before optimizing, ensure you have computed the static validity masks for the sensors:

```bash
python3 scripts/create_validity_masks_for_extrinsic_optimization.py ./data/captured_emulated_rgbd_<timestamp>/
```

You can estimate the maximum pixel distance between the sparse depth points resulting from the LiDAR 3D points using the following script. This can help you determine an appropriate value for MAX\_LIDAR\_INTERPOLATION\_DIST\_PX in the optimization configuration file, `stretch4_emulated_rgbd/emulated_rgbd_config.py`

```bash
python3 scripts/estimate_lidar_gap.py --data_path ./data/captured_emulated_rgbd_<timestamp>/
```

### 3. Visualize Data

You should now visualize the captured data to make sure that it is of sufficient quality for extrinsic optimization. For example, it's important that the camera images have reasonable brightness without overly dark or bright areas. Similarly, it's important that the LiDAR points appear to be accurate without missing sections due to a partial scan. As noted above, it is also important to have at least three distinct views with prominent foreground objects across the field of view. Foreground objects are important since they will typically result in edges in both the LiDAR-based depth image and the RGB camera image that can be aligned via mutual information.

To visualize the data, run the following script.

```bash
python3 scripts/visualize_emulated_rgbd.py --data_path ./data/captured_emulated_rgbd_<timestamp>/
```

*(Note: The visualization will first have you review the validity masks using OpenCV windows. Click on a window and press 'y' to approve. Then the visualization will use Rerun to visualize the RGB-D calibration data. The Rerun timeline can be used to visualize different captured views in the data.)*

### 4. Optimize the Camera-LiDAR Extrinsics

Run the core CMA-ES optimization to refine the 6D rigid body extrinsics representing the pose of the camera:

For the left camera/lidar calibration:

```bash
python3 scripts/optimize_extrinsics.py --camera left --lidar left --data_path ./data/captured_emulated_rgbd_<timestamp>/ 
```

For the right camera/lidar calibration:

```bash
python3 scripts/optimize_extrinsics.py --camera right --lidar right --data_path ./data/captured_emulated_rgbd_<timestamp>/ 
```

This script leverages Mutual Information (MI) to structurally align the edges of the projected LiDAR depth map with the visual edges in the RGB image. It does not optimize camera intrinsics.

Using the `--visualize` command line argument will visualize the optimizations progress via Rerun. Using the `--debug` command line argument will result in OpenCV windows that show the extracted image gradients and intersection masks used by the mutual information objective function.

### 5. Visualize the Optimization Results

Now, run the following script to visualize the results of the calibration. The left Rerun panel shows the captured data before calibration and the right panel shows the data after calibration.

For the left camera/lidar calibration:

```bash
python3 scripts/visualize_optimized_rgbd.py --data_path ./data/captured_emulated_rgbd_<timestamp>/ --opt_yaml ./data/captured_emulated_rgbd_<timestamp>/optimization_results_mi_rgb_left_camera_left_<calibration_timestamp>.yaml
```

For the right camera/lidar calibration:

```bash
python3 scripts/visualize_optimized_rgbd.py --data_path ./data/captured_emulated_rgbd_<timestamp>/ --opt_yaml ./data/captured_emulated_rgbd_<timestamp>/optimization_results_mi_rgb_right_camera_right_<calibration_timestamp>.yaml
```

*(Note: The visualization will first have you review the validity masks using OpenCV windows. Click on a window and press 'y' to approve.)*

### 6. Installation of Optimized Calibrations (On Robot)

Once you are satisfied with the calibration, you can install the resulting YAML file as the robot's default calibration. The provided helper script checks for fleet mismatches (i.e. capturing data on one robot and installing on another) and prevents accidental downgrading to older calibrations.

```bash
python3 scripts/install_optimized_calibration.py ./data/captured_emulated_rgbd_<timestamp>/optimization_results_mi_rgb_<camera_name>_camera_<lidar_name>_<calibration_timestamp>.yaml
```

Once installed, the `FastEmulatedRGBDStreamer` and `rgbd_rtmo_pose_estimation.py` will automatically load and apply this optimized calibration.

### 7. Validity Mask Estimation (On Robot)

After optimizing the extrinsics and installing them, new physical validity masks should be estimated directly on the robot using the optimized calibration. This step dynamically calculates masks that zero out invalid RGB pixels caused by hardware vignetting and prevents the interpolation of the dense depth map from bleeding into regions with no physical LiDAR coverage.

To capture the live frames and estimate the masks for the left sensor pair:

```bash
python3 scripts/estimate_validity_masks.py --camera left --lidar left
```

To capture the live frames and estimate the masks for the right sensor pair:

```bash
python3 scripts/estimate_validity_masks.py --camera right --lidar right
```

To capture and estimate masks for both sides simultaneously:

```bash
python3 scripts/estimate_validity_masks.py --camera left_right --lidar both
```

*(Note: This must be run on the Stretch 4 robot. The script captures exactly 30 synchronized frames, computes the masks at the highest active resolution, and saves them locally to `data/validity_masks/` for the visualizers to use automatically).*

### 8. Visualize Live RGB-D Imagery with the New Calibration (On Robot)

To visualize captured or live data streams run:

For the left camera/lidar:

```bash
python3 scripts/visualize_emulated_rgbd.py --camera left --lidar left
```

For the right camera/lidar:

```bash
python3 scripts/visualize_emulated_rgbd.py --camera right --lidar right
```

For both cameras and lidars simultaneously:

```bash
python3 scripts/visualize_emulated_rgbd.py --camera left_right --lidar both
```

### 10. Use PyZMQ to Send RGB-D Imagery

There are two examples of sending RGB-D images via PyZMQ. They can be used to send RGB-D images from the robot to a desktop computer or between processes on the same computer.

The first example sends RGB-D images, receives them, and then visualizes them using OpenCV.

To run the publisher (e.g. for both cameras):

```bash
python3 examples/send_rgbd_images.py --camera left_right --lidar both
```

To run the subscriber:

```bash
python3 examples/recv_rgbd_images.py
```

The second example send RGB-D images with the synchronized joint state of the robot, receives them, and then visualizes the colored 3D point cloud in Rerun along with the joint states.

To run the publisher (e.g. for both cameras):

```bash
python3 examples/send_rgbd_images_and_joint_states.py --camera left_right --lidar both
```

To run the subscriber:

```bash
python3 examples/recv_rgbd_images_and_joint_states.py
```

Both examples can send data over the network. To do so, you will need to use the `--remote` flag for both the publisher and the subscriber and provide IP and port information. Sending RGB-D images with joint states uses the following file for IP and port information:

`stretch4_rgbd/stretch4_emulated_rgbd/rgbd_networking.py`

The example code for sending RGB-D images alone uses IP and port information provided as command line arguments.

## API Usage and Reference

For developers writing custom applications, the repository provides a unified API in `stretch4_emulated_rgbd.api` to stream and process synchronized RGB-D frames.

> \[!TIP] **Quick Start:** For a complete, runnable demonstration of the API capabilities—including lazy properties, calibration extraction, validity masking, dense depth interpolation, and colored 3D point cloud generation—see [`examples/api_example.py`](file:///home/hello-robot/repos/stretch4_rgbd/examples/api_example.py). **WARNING** The Rerun visualization used with the script has two notable issues: 1. The left and right RGB-D images are displayed in the same panel. To visualize one of them, you can hide the overlayed images from the other. 2. The middle 3D point cloud shows the result of generating a point cloud from every depth point in the dense depth image, which uses interpolation. Currently, this results in substantial artifacts due to some of the interpolated depth points being invalid.

#### Summary of Processing Steps

When the `FastEmulatedRGBDStreamer` captures and aligns an RGB-D frame, it executes the following steps internally *before* yielding it to you:

1. **Temporal Synchronization**: Waits for the LiDAR sweep to finish and grabs the temporally closest high-frequency RGB image to minimize phase lag.
2. **Spatial Transformation & Culling**: Projects 3D LiDAR points into the camera's frame of reference, dropping any points physically behind the camera.
3. **Zero-Copy Distorted Projection**: Projects the 3D points directly into the raw, distorted 2D fisheye image space.
4. **Z-Buffer Sorting**: If multiple LiDAR points land on the exact same 2D pixel, only the closest point (minimum Z) is kept.
5. **Sparsity Shadow Filter**: If enabled, checks local neighborhoods to aggressively remove background depth points that visually "bleed" onto foreground objects due to parallax.

> \[!IMPORTANT]\
> The streamer returns a **sparse** depth map (only specific pixels hit by LiDAR have non-zero depth) and does **not** apply validity masks internally. You must apply masks or compute dense depth explicitly using the API as shown below.

#### Code Examples

**Receiving the Stream (Simultaneous Left and Right)**

```python
from stretch4_emulated_rgbd.api import get_emulated_rgbd_stream

# Automatically loads the optimized calibrations for the current fleet
streamer, generator = get_emulated_rgbd_stream(
    use_left_right=True, 
    use_left_lidar=True,
    use_right_lidar=True,
    emulated_rgbd_fps=10.0
)

# Fetch synchronized frames from both cameras
multi_frame = next(generator)
left_frame = multi_frame.left
right_frame = multi_frame.right
```

**Applying Validity Masks**

```python
from stretch4_emulated_rgbd.api import ValidityMaskManager
import cv2

mask_manager = ValidityMaskManager()
# Automatically loads the robot's physical vignetting and LiDAR bounds masks
vig_mask, depth_mask = mask_manager.get_masks("left", "left_lidar right_lidar", left_frame.image.shape)

# Apply vignette mask to black out the physical camera housing
masked_rgb = left_frame.image.copy()
masked_rgb[~vig_mask] = 0
```

**Generating a Dense Depth Image**

```python
from stretch4_emulated_rgbd.api import DenseDepthImage

# Initialize the processor with raw RGB and sparse depth
dense_processor = DenseDepthImage(frame.image, frame.depth_image)

# Combine masks to prevent depth bleeding into physically impossible areas
combined_mask = vig_mask & depth_mask if (vig_mask is not None and depth_mask is not None) else None

# Interpolate using distance transforms
dense_depth = dense_processor.compute_dense_depth(valid_region_mask=combined_mask)
```

**Creating a Point Cloud from an RGB-D Image**

```python
from stretch4_emulated_rgbd.api import get_camera_intrinsics, create_point_cloud_from_depth

# Fetch intrinsics dynamically
cam_matrix, dist_coeffs = get_camera_intrinsics(streamer, "left")

# Re-project the 2D dense depth image back into a 3D Nx3 point cloud array
pts_cam, colors = create_point_cloud_from_depth(
    dense_depth, 
    frame.image, 
    cam_matrix, 
    dist_coeffs
)
```

**Obtaining the 3D Location of a Single Pixel**

```python
from stretch4_emulated_rgbd.api import get_pixel_3d_location

u, v = 640, 400 # Center of the image

# Get the 3D coordinate of the pixel in the camera frame and the base frame
pt_cam, pt_base = get_pixel_3d_location(
    u, v, 
    dense_depth, 
    cam_matrix, 
    dist_coeffs, 
    T_base_to_cam=T_base_to_cam
)

if pt_cam is not None:
    print(f"Pixel ({u}, {v}) is at {pt_cam} in camera frame.")
    print(f"Pixel ({u}, {v}) is at {pt_base} in base frame.")
else:
    print(f"Pixel ({u}, {v}) has no valid depth.")
```

**Accessing Intrinsics and Extrinsics** To ensure you never accidentally use the wrong calibration, the exact intrinsic and extrinsic matrices used to generate an `RGBDFrame` are permanently attached to the frame object itself.

```python
# Camera Intrinsics
cam_matrix = frame.camera_matrix
dist_coeffs = frame.distortion_coefficients

# Extrinsic transform from the robot's base frame to the camera optical frame
T_base_to_cam = frame.T_base_to_cam

# Extrinsic transforms from the LiDAR frames to the robot's base frame
T_lidar_to_base_left = frame.T_lidar_to_base_left
T_lidar_to_base_right = frame.T_lidar_to_base_right

print("Optimized Extrinsics (T_base_to_cam):\n", T_base_to_cam)
```

**Visualizing with ReRun**

```python
import rerun as rr
from stretch4_emulated_rgbd.api import visualize_rgbd_frame

rr.init("Stretch API Example", spawn=True)

# Helper function automatically visualizes the raw RGB, sparse depth, 
# dense depth (if computed via DenseDepthImage), and 3D point cloud overlays
visualize_rgbd_frame("left", frame, vig_mask=vig_mask, depth_mask=depth_mask)
```

## Configuration (`emulated_rgbd_config.py`)

All key hyperparameters used throughout the optimization, depth alignment, and shadow filtering processes are centralized in `stretch4_emulated_rgbd/emulated_rgbd_config.py`. You can adjust these settings to fine-tune the pipeline for your specific requirements.

The configuration file is organized into several main categories:

### Extrinsic Optimization Parameters

Because the factory calibration from Hello Robot is already of high quality, the CMA-ES optimizer is constrained to a local search radius to prevent divergent or absurd alignments. These settings control the boundaries and behavior of the optimization:

* **Optimization Constraints**: Bounds on the maximum allowed translational shift (e.g., `MAX_TRANSLATION_M` = 0.03 m) and rotational shift (e.g., `MAX_ROTATION_DEG` = 10.0 deg) applied to the `T_delta` matrix. Exceeding these bounds applies a heavy penalty (`BOUNDS_PENALTY_WEIGHT`). You can tune these values if you suspect a larger physical shift occurred (e.g., if a camera was physically bumped or remounted).
* **CMA-ES Hyperparameters**: Settings such as population size, maximum iterations, and early stopping tolerances (e.g., `EXTRINSICS_CMA_TOLFUN` and `EXTRINSICS_CMA_TOLX`) to balance accuracy and optimization speed.
* **Objective Function Settings**: Adjusts how Mutual Information is calculated, including `EXTRINSICS_USE_NMI` (normalized vs standard), `EXTRINSICS_GRAD_KSIZE` (Sobel kernel size for edges), and `EXTRINSICS_MI_BINS` (histogram bins).
* **Initialization**: `EXTRINSICS_IGNORE_PRIOR_OPTIMIZATIONS` allows starting from factory calibration instead of resuming from a previously optimized state.

### Depth Alignment & Spatial Corrections

These parameters control how sparse LiDAR points are processed and how invalid regions are masked out:

* **Depth Masking**: Thresholds and window sizes for density-based depth validity masks and morphological closing operations.
* **Interpolation Limits**: Maximum pixel distance (`MAX_LIDAR_INTERPOLATION_DIST_PX`) a sparse depth point can be interpolated before being marked invalid.
* **Saturation Masking**: Thresholds for excluding overexposed RGB pixels from the edge alignment process.
* **Physical Boundaries**: Minimum and maximum physical depths (e.g., `MIN_PHYSICAL_DEPTH_M`, `MAX_PHYSICAL_DEPTH_M`) to clip the synthetic depth map.

### Sparsity Shadow Filter

This filter resolves an artifact where the edges on one side of objects appear to have alternating foreground and background "stripes" in the dense depth image.

Because the left and right LiDAR sensors are physically offset from the RGB cameras, they can see 3D points that should be occluded from a camera's perspective. Since the LiDAR points are sparse, both the foreground depth points and the background depth points that should be occluded project to the same region on the camera's image plane without overlapping exactly. This effect is most prominent when an object is close to the sensors and reduces as the object moves farther away, resulting in more similar views of the object from the LiDAR sensors and camera.

A moving window shadow filter is used to reduce this effect. The filter acts as a sparse Z-buffer:

* **`ENABLE_SHADOW_FILTER`**: When enabled, the filter attempts to remove occluded background points that "bleed" into foreground objects by utilizing the depth information from neighboring pixels.
* **`SHADOW_FILTER_WINDOW_SIZE`**: This is the pixel width of the square window used to decide if a depth point should be removed. If a point in the local neighborhood is closer to the RGB camera than the depth point being evaluated by more than `SHADOW_FILTER_DEPTH_THRESHOLD_M`, the point is considered an occluded background point (i.e., shadowed point) and removed. If the window size is too large, it can filter points that should be visible to the RGB camera. If it is too small, it leaves points that should not be visible to the RGB camera.
* **`SHADOW_FILTER_DEPTH_THRESHOLD_M`**: This is the depth threshold used to determine if a point is an occluded background point. A large value will only remove points on objects with a background that is far away. A small value will remove points with a background that is nearby, but will also remove points on surfaces that are angled away from the RGB camera.
* **`SHADOW_FILTER_USE_CIRCULAR_WINDOW`**: Toggles whether the moving window uses a circular (elliptical) structuring element instead of a square. A circular window provides more mathematically accurate, isotropic filtering and prevents blocky artifacts around object contours, making it advantageous for larger window sizes (e.g., >= 5). Circular kernels are non-separable and introduce latency, so they are disabled by default.

*Implementation Note:* Under the hood, the filter uses an image erosion operation (`cv2.erode`) to efficiently find the minimum depth within the local neighborhood window for all pixels simultaneously. Any point whose original depth exceeds this local minimum by more than the threshold is classified as a shadowed point and removed.

### Native Image Orientation

The raw images from the head-mounted fisheye cameras natively output in a rotated, horizontal orientation due to how the sensors are physically mounted. To address this, the pipeline can rotate the images 90 degrees to an upright vertical orientation early on.

* **`ROTATE_IMAGES_TO_VERTICAL`**: The master toggle that enables 90-degree rotations across the pipeline, ensuring downstream applications receive upright images.
* **`USE_BOARD_LEVEL_ROTATION`**: Attempts to perform the 90-degree rotation directly on the Luxonis OAK-FFC board's hardware before MJPEG compression.

> \[!WARNING] **Hardware Limitations of Board-Level Rotation** The Luxonis board (Myriad X VPU) lacks a fast, zero-cost 90-degree memory transpose operation for the NV12 format. Instead, it processes 90-degree rotations using a generic hardware warp engine (`ImageManip`), which is heavily compute-intensive.
>
> Because of this:
>
> 1. Board-level rotation is incompatible with the 600p resolution when using MJPEG compression, as the rotated width (600) is not a multiple of 16.
> 2. Attempting to rotate high-resolution frames overwhelms the hardware, resulting in dropped frames and noticeable pipeline latency between the RGB and depth components. **When `USE_BOARD_LEVEL_ROTATION` is True, the `camera_fps` must be restricted to 10 or lower.**
>
> **STRONGLY RECOMMENDED:** For applications requiring precise temporal synchronization or high frame rates, keep `USE_BOARD_LEVEL_ROTATION = False`. The pipeline utilizes a **"lazy evaluation"** architecture that automatically transports the natively compressed MJPEG frames over the network to save bandwidth. The software fallback then transparently applies a highly efficient (<1ms) 90-degree rotation on the host CPU exactly when the developer accesses the `frame.image` or `frame.depth_image` properties, completely insulating the user from the native orientation.

## Spatial Alignment Overview

This repository provides optimization methods for spatially aligning the RGB image and depth image components of emulated RGB-D images from Stretch 4. The optimization uses Covariance Matrix Adaptation Evolution Strategy (CMA-ES) to optimize the extrinsic 6D rigid body transformation between a LiDAR sensor and a camera sensor. The objective function uses the normalized mutual information (NMI) between the gradients of the RGB image and the gradients of the projected LiDAR depth image.

In practice, this optimization results in the RGB and depth images being well-aligned, which greatly simplifies their use. The code also provides visualizations of the alignment results and the optimization procees. Other helpful utilities includes a script that generates data-driven masks representing the valid pixels in the depth and RGB images.

### Motivation

The RGB image comes directly from one of the three cameras in Stretch 4's head: the left fisheye camera, the right fisheye camera, or the high-resolution wide-angle center camera. The depth image is created by transforming 3D points from one or both LiDAR sensors into the camera's frame of reference and then projecting them onto the focal plane of a camera model.

Prior to shipping a Stretch 4 robot, Hello Robot uses an extensive calibration procedure involving a calibration pattern mounted to the end of the robot's arm. This procedure yields high-quality intrinsic camera parameters, including the focal length, principal point, and distortion coefficients for each of the three cameras. The calibration pattern also has reflective fiducial markers whose 3D locations relative to the visible-light calibration pattern are known. This information is used to compute an extrinsic 6D rigid body transformation between each LiDAR sensor and each camera sensor.

While this extrinsic calibration is of high quality, the spatial alignment between the depth image and the RGB image are highly sensitive to the 6D rigid body transformation (i.e., extrinsics for the LiDAR and camera). The remaining error creates challenges for applications. For example, points and regions output by computer vision models applied to the RGB image cannot be easily associated with the corresponding 3D points in the depth image.

## Temporal Quality Overview

### Frame Rate & Phase Alignment

The Emulated RGB-D pipeline is driven by the physical rotation of the Hesai LiDAR, which completes a 360-degree sweep at 10Hz (100ms per rotation). The pipeline's target output rate (`--emulated_rgbd_fps`) is strictly tied to fractions of this rotation (e.g., 10Hz, 5Hz).

Because there is no hardware sync signal aligning the *phase* of the RGB camera's exposure with the LiDAR's physical rotation, running the camera at 10Hz can result in up to 50ms of phase misalignment latency. For example, if the LiDAR sweep midpoint occurs at $T=50$ms, but the camera exposes at $T=0$ms and $T=100$ms, the minimum temporal gap between the data is 50ms.

**Software Over-Sampling**: To minimize phase lag, the pipeline employs an inverted, LiDAR-driven architecture. The camera hardware runs at a higher frame rate (configured via `--camera_fps`, e.g., 30Hz), continuously buffering compressed MJPEG frames in a background thread. The pipeline waits for a LiDAR sweep to finish, calculates its exact temporal midpoint, and immediately pulls the single RGB frame from the buffer that minimizes the temporal gap. At 30Hz, this drops the maximum phase misalignment from \~50ms to \~16ms without incurring the CPU cost of decompressing unused frames.

### Latency

Maintaining low latency and strict temporal synchronization between the instantaneous RGB image capture and the 100ms LiDAR sweep requires several advanced software mitigations to overcome hardware and network limitations:

1. **Hardware Clock Offset Estimation**: The PyHesai driver outputs point clouds tagged with the LiDAR's internal hardware clock timestamp. However, this clock is often desynchronized from the host computer's monotonic clock (which the Luxonis RGB cameras use). Furthermore, standard UDP buffers within the driver can create significant "buffer bloat," causing timestamps captured at the *reception* of the packet to lag the true physical capture time by up to 300ms. To eliminate this, the `LidarPoller` continuously calculates the minimum transmission delay between the LiDAR's hardware clock and the host's monotonic clock (`time.monotonic()`). By dynamically maintaining this `clock_offset`, it assigns precise, jitter-free host timestamps to the LiDAR points that reflect the exact physical moment the light hit the sensor.
2. **Physical Sweep Lookahead**: Because a global shutter RGB camera captures an image instantaneously at time `T`, the LiDAR sweep that optimally captures the same state of the world is the one spanning from `T - 50ms` to `T + 50ms`. Since this physical rotation will not finish until `T + 50ms`, eagerly requesting the "closest" point cloud at time `T` previously caused the system to fetch the *prior* sweep (ending at `T - 50ms`), injecting 100ms of structural latency. The `get_closest_frame` method solves this by intentionally blocking and waiting until the ideal sweep finishes rotating and arrives over the network, achieving near-perfect temporal synchronization at the cost of a strictly bounded \~50ms mechanical delay.
3. **Global Shutter & Sweep Midpoint Synchronization**: The synchronization logic mathematically matches the instantaneous RGB timestamp with the temporal *midpoint* of the LiDAR sweep (calculated from the first and last point timestamps). This perfectly balances the temporal error across the 100ms rotation window, ensuring that moving objects in the center of the RGB image map tightly to the corresponding LiDAR points.

### High-Frequency Pipeline Methods

To achieve and stabilize the 10Hz target without CPU or USB bottlenecking, the default `stretch4_body` pipeline was heavily refactored into the low-latency `FastEmulatedRGBDStreamer` and `HeadCamera` classes:

1. **Direct Hardware Access & Zero-Copy Structuring**: The overhead of inter-process message passing and excessive buffering was removed. Data is polled directly from the sensor drivers into a tight Python generator loop.
2. **Non-Blocking Queues & Memory Pools**: The Luxonis OAK-FFC DepthAI pipeline is configured with strictly bounded memory pools (`setNumFramesPools=2`) and a non-blocking output queue (`maxSize=1`). If the host CPU stalls, the camera driver instantly overwrites the oldest frame rather than queuing it, guaranteeing the host *always* pulls the absolute freshest physical frame.
3. **USB MJPEG Compression**: Passing uncompressed 1200p or 800p RGB arrays at 10Hz concurrently with intense UDP LiDAR traffic can easily saturate the USB bus, leading to dropped frames or variable latency. The streamer delegates MJPEG compression directly to the OAK-FFC's hardware VideoEncoder, securing deterministic transmission times.
4. **Inverse Distorted Projection**: Instead of executing an expensive dense image unwarp (`cv2.undistort`) on the high-resolution RGB stream at 10Hz—a heavy $O(N\_{pixels})$ operation—the math is inverted. The sparse 3D LiDAR point cloud is projected directly into the raw, distorted fisheye image space—a lightweight $O(N\_{lidar\_points})$ operation. This drastically reduces CPU load and garbage collection pauses.

## Extrinsic Calibration Details

The output of the `optimize_extrinsics.py` script is a YAML file containing the results of the CMA-ES optimization process. This file contains metadata, the system profile, hyperparameter values used during the run, the convergence metrics, and the optimized transform matrices.

### Example Output File

A typical `optimization_results_mi_rgb_{camera}_camera_{lidar}_<timestamp>.yaml` file looks like this:

```yaml
convergence:
  final_cost: -0.6432
  initial_cost: -0.4281
hyperparameters:
  grad_ksize: 5
  maxiter: 500
  method: mi_rgb
  num_iterations_executed: 142
  popsize: 30
  sigma0: 2.0
  tolfun: 1.0e-11
  tolx: 1.0e-11
  use_nmi: true
metadata:
  camera: left
  lidar: left_lidar
  data_path: ./data/captured_emulated_rgbd_20260429_162908
  data_subdirectories:
  - left_camera_left_lidar_20260502_214533
  duration_seconds: 481.52
  num_sequences_evaluated: 3
  timestamp: '20260502_214533'
  capture_fleet_id: stretch-se4-4010
  validity_masks:
    depth_valid_mask:
      filename: depth_valid_mask_left_camera_left_lidar.png
      generated_at: '20260502_214533'
    rgb_vignette_mask:
      filename: rgb_vignette_mask_left_camera.png
      generated_at: '20260502_214533'
results:
  best_delta_transform_array:
  - 0.0125
  - -0.0034
  - 0.0011
  - 0.0452
  - 0.0121
  - -0.0210
  best_delta_transform_matrix:
  - [0.9997, 0.0210, 0.0121, 0.0125]
  - [-0.0210, 0.9988, 0.0452, -0.0034]
  - [-0.0111, -0.0455, 0.9989, 0.0011]
  - [0.0, 0.0, 0.0, 1.0]
system_profile:
  hostname: stretch-se4-4010
  machine: x86_64
  os: Linux
```

### Mathematical Interpretation and Application

The optimized transformation output under `results.best_delta_transform_matrix` (often referred to as `T_delta`) represents a highly precise 6D rigid body correction to the *camera's pose in the robot's base frame*.

To project LiDAR points into the camera frame, you typically multiply a 3D point in the base frame by the camera's extrinsic matrix:

```
P_camera = T_base_to_cam @ P_base
```

The optimizer is parameterized to find a positional shift `T_delta` for the camera relative to its initial calibrated position. To correctly apply this correction, you must pre-multiply the camera's original extrinsic matrix by the **inverse** of the optimized transform:

```python
import numpy as np

# Load original extrinsics and optimization delta
T_base_to_cam_original = ... 
T_delta = np.array(yaml_data["results"]["best_delta_transform_matrix"])

# Compute corrected camera extrinsics
T_base_to_cam_corrected = np.linalg.inv(T_delta) @ T_base_to_cam_original

# Project points using the corrected extrinsics
P_camera_corrected = T_base_to_cam_corrected @ P_base
```

**Using the Shared Utilities:** To make this process seamless for downstream applications, the repository provides the `ExtrinsicsCalibration` helper class in `stretch4_emulated_rgbd.shared_utils`.

```python
from stretch4_emulated_rgbd.shared_utils import ExtrinsicsCalibration

# Automatically load the inverse transform and manage the math
calibration = ExtrinsicsCalibration.load_from_yaml("path/to/optimization_results.yaml")

# Apply cleanly to your original transform
T_base_to_cam_corrected = calibration.apply_to_camera_extrinsics(T_base_to_cam_original)
```


# README

## pyhesai\_wrapper

This repository holds code that is intended to provide a Python interface to the Hesai JT128 hemispherical LiDAR.

## How to Build and Run

#### Prerequisites:

* A C++17 compiler (like g++).
* cmake (version 3.14 or higher, e.g., `sudo apt install cmake`).
* Python 3.12+ and pip (or uv).
* Git (for cloning the Hesai SDK).
* The Hesai SDK's system dependencies: `libpcap-dev`, `libssl-dev` (e.g., `sudo apt install libpcap-dev libssl-dev`).

#### Setup:

```bash
python3 -m venv .venv
source .venv/bin/activate
```

The build process is now fully automated. Simply run:

```bash
pip install .
```

This will:

1. Read pyproject.toml
2. Use scikit-build to run CMakeLists.txt.
3. CMake will find pybind11, the SDK headers, and the SDK libraries.
4. It will compile pybind\_hesai\_sdk.cpp and link it against all the .a and .so files.
5. It will create a Python module file (e.g., pyhesai\_wrapper\_cpp.cpython-310-x86\_64-linux-gnu.so) and install it into your Python environment.
6. If the build is successful, the pyhesai\_wrapper module is now installed and available to all Python scripts in your environment.

#### Use in your python code

Both Left and Right Lidars:

```python

from pyhesai_wrapper import stream_lidar_left_right

for left, right in stream_lidar_left_right():
   if left is not None:
      print(f"Points shape: {left.points.shape}, timestamp: {left.timestamp}")
   if right is not None:
      print(f"Points shape: {right.points.shape}, timestamp: {right.timestamp}")
```

Left Lidar:

```python
from pyhesai_wrapper import stream_lidar_left

for frame in stream_lidar_left():
   if frame is not None:
      print(f"Points shape: {frame.points.shape}, timestamp: {frame.timestamp}")
```

Right Lidar:

```python
from pyhesai_wrapper import stream_lidar_right

for frame in stream_lidar_right():
   if frame is not None:
      print(f"Points shape: {frame.points.shape}, timestamp: {frame.timestamp}")
```

Alternatively, you can poll the next frame using `next()`:

```python
left, right  = stream_lidar_left_right()
left_frame = next(left)
right_frame = next(right)
```

**The `LidarPointCloudFrame` Dataclass**

When you fetch points using `lidar.get_next()` or via the streaming generators, the system returns a `LidarPointCloudFrame` object (or `None` if no new data is available yet). The properties of this object are:

* `points`: A NumPy array of shape `(N, 3)` containing the X, Y, and Z Cartesian coordinates of the captured points (`dtype=float32`).
* `intensity`: A NumPy 1D array of shape `(N,)` containing the return intensity values (`dtype=uint8`).
* `timestamp`: A NumPy 1D array of shape `(N,)` containing the microsecond tick timestamps for each point (`dtype=float64`).
* `confidence`: A NumPy 1D array of shape `(N,)` containing the confidence values (`dtype=uint8`).
* `ring`: A NumPy 1D array of shape `(N,)` containing the laser ring IDs (`dtype=uint16`).

#### Tools:

**Live Lidar test (`tools/stretch_lidar_show.py`):**

1. Edit `pyhesai_wrapper/config.yaml` to configure your lidar settings:
   * Update `device_ip_address` to match your lidar's IP (default: `192.168.1.201`)
   * Update `correction_file_path` to point to your lidar's correction file
   * Optionally update other parameters like `udp_port`, `ptc_port`, etc.
2. Make sure your machine is on the same network as the lidar.
3. Run the script:

   ```bash
   stretch_lidar_show
   stretch_lidar_show --cluster_high_intensity
   stretch_lidar_show --left
   stretch_lidar_show --right
   ```

   > Note: You can cluster and display the Euclidean distance to high intensity points by passing the `--cluster_high_intensity` flag
4. You should see point cloud data streaming from the lidar. Press Ctrl-C to stop.

**Download calibration (`tools/REx_hesai_download_calibration.py`):**

1. Edit `pyhesai_wrapper/config.yaml` to configure your lidar settings:
   * Update `device_ip_address` to match your lidar's IP (default: `192.168.1.201`)
   * Update `ptc_port` to match your lidar's PTC port (default: `9347`)
2. Make sure your machine is on the same network as the lidar.
3. Run the script:

   ```bash
   REx_hesai_download_calibration --left
   ```

   or

   ```bash
   REx_hesai_download_calibration --right
   ```
4. You should see calibration data being downloaded from the lidar to the `$HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_hesais`directory.

**PTC getters/setters (`pyhesai_wrapper/ptc_client.py`):**

SDK-backed JT128 PTC client for return mode, point-cloud filter, PTP lock offset, diagnostics, and reachability checks.

```python
from pyhesai_wrapper.ptc_client import (
    FILTER_STRONG,
    FILTER_STRONGEST,
    POINT_CLOUD_MODE_MAPPING,
    get_point_cloud_config,
    get_point_cloud_mode,
    get_return_mode,
    is_new_firmware_supported,
    set_filter_type,
    set_point_cloud_mode,
    set_return_mode,
    get_ptp_lock_offset_us,
    ptc_reachable,
)

ip = '192.168.1.201'
if ptc_reachable(ip):
    print(get_return_mode(ip))
    set_return_mode(ip, 2)
    set_filter_type(ip, FILTER_STRONG)  # ultra_precise unchanged
    print(get_point_cloud_config(ip))

    # Strongest filter (3) and POINT_CLOUD_MODE need FW
    # 15.AF.B0.00.02.Y / 1.b.0028 / 2.b.0692
    if is_new_firmware_supported(ip):
        set_filter_type(ip, FILTER_STRONGEST)
        set_point_cloud_mode(ip, POINT_CLOUD_MODE_MAPPING)  # 0 general, 1 mapping, 2 mapping+ground
        print(get_point_cloud_mode(ip))
```

Noise filter levels: `0` disabled, `1` medium, `2` strong, `3` strongest (new FW only).

`is_new_firmware_supported()` is the single firmware gate used by Strongest filter and POINT\_CLOUD\_MODE. Per Hesai, it requires all three inventory patches at or above `15.AF.B0.00.02.Y` / `1.b.0028` / `2.b.0692` (wrapper fields `hardware_version`, `software_version`, `fpga_version`). When a newer mass-production firmware ships, re-check Hesai’s version naming (especially if APP moves past `…02.Z` / to `…03.X`) and update that function.

**Show configuration (`REx_hesai_show_config`):**

To view complete lidar information, return mode, spin rate, PTP status, and point cloud settings:

```bash
# Show config/status for both lidars
REx_hesai_show_config

# Show config/status for a specific lidar
REx_hesai_show_config --left
REx_hesai_show_config --right
```

This retrieves the serial number, model, hardware and software versions, build ID, MAC address, whether new-FW features are supported, return mode, spin rate, lock offset, ultra-precise mode, noise filter type, point-cloud mode (when supported), PTP status, and active PTP master offset (if PTP is synchronized).

**Modify configuration (`REx_hesai_set_config`):**

> \[!WARNING] Modifying the LiDAR hardware configuration can disrupt the normal operation of your robot. Be cautious when using this utility.

An interactive tool to adjust hardware settings on a specific lidar:

```bash
# Configure left lidar
REx_hesai_set_config --left

# Configure right lidar
REx_hesai_set_config --right
```

After accepting the warning, you can select from the interactive options:

* **10** - Set Return Mode (0 to 5)
* **11** - Set Spin Speed (600 or 1200 RPM)
* **12** - Set PTP Lock Offset (1 to 1000 us)
* **13** - Set Noise Filter Type (0 to 3; `3` / strongest requires new FW)
* **14** - Set Point Cloud Mode (0 to 2; requires new FW)

Each setting operation performs a baseline GET, followed by the SET command, and finishes with a readback verification GET to guarantee that the hardware successfully applied the modification.

**Upgrade firmware (`REx_hesai_upgrade_firmware`):**

> \[!WARNING] Do not power off the lidar during upgrade. The unit reboots after a successful transfer. Upgrade one lidar at a time.

Uploads a **Hesai-provided** JT128 firmware patch via PTC Upgrade Safe Image (`0x83`) and prints transfer progress. The firmware file is not shipped in this repo; obtain it from Hesai.

```bash
# Right lidar (interactive confirm)
REx_hesai_upgrade_firmware --right --firmware /path/to/JT128_upgrade.patch

# Left lidar, skip confirm prompt
REx_hesai_upgrade_firmware --left --firmware /path/to/JT128_upgrade.patch -y

# Explicit IP
REx_hesai_upgrade_firmware 192.168.1.201 --firmware /path/to/JT128_upgrade.patch
```

Optional flags: `--timeout` (PTC connect timeout, default 30s), `--reboot-wait` (wait for lidar to return after transfer, default 120s), `-y` / `--yes` (skip confirmation).

The tool prints inventory versions before upload, streams `Progress: xx.x%`, waits for reboot, then prints versions again. The version after upgrade might not show all the version

```
Versions after upgrade:
  [after]
  Hardware Version:     15.AF.B0.00.02.Y0
  Software/Firmware:    1.b.0028
  FPGA Version:         
  Build/Signature ID:   0x00000000
```

You can run `REx_hesai_show_config` and check the inventory info

```
  INVENTORY INFO
  ------------------------------------------------------------------
  Model:                JT128
  Serial Number:        JT3AC9509338CB50
  MAC Address:          ec:9f:0d:02:f1:cd
  Calibration/Mfg Date: 2025-03-05
  Hardware Version:     15.AF.B0.00.02.Y0
  Software/Firmware:    1.b.0028
  FPGA Version:         2.b.0692
  Build/Signature ID:   0x791C2330
  New FW Features:      supported
```

**Standalone PTC bench test:**

You can run the standalone PTC test menu directly:

```bash
python3 test/ptc_test.py --left
```


# Stretch Dual Lidar Calibration

This repository contains the core calibration nodes and algorithms for aligning, floor-registering, and body-modeling dual LiDAR data on Hello Robot's Stretch.

## Installation

This is a standard ROS 2 Python package. You can install it using `colcon` or locally via `pip`:

```bash
# Via pip (editable mode) from the repository root
pip3 install -e . --break-system-packages
```

***

## Running Calibration

You should perform these calibration steps sequentially to properly configure your dual LiDAR setup. The calibration settings are automatically saved to your fleet directory (or `~/.stretch/calibration/`) so they can be securely loaded from any working directory later.

Prior to running body calibration, stow the robot’s arm by lowering the lift, retracting the telescoping arm, and configuring the wrist so that the wrist and end effector are within the footprint of the mobile base. This will enable the two LiDAR sensors to see the environment better.

You should also run the following ros2 launch files in separate terminals:

```
ros2 run rmw_zenoh_cpp rmw_zenohd

ros2 launch stretch_core stretch_driver.launch.py

ros2 launch stretch_core dual_hesai.launch.py  filter_type:=sor
```

### 1. Dual LiDAR Alignment Calibration

**Preconditions:** Prior to running dual LiDAR alignment calibration, make sure the robot is in a static environment without moving objects. The environment should also have enough geometric structure and size to enable scan matching to find a high-quality rigid body transform between scans taken from the left and right LiDAR.

```bash
ros_align_dual_lidar
```

### 2. Floor Calibration

**Preconditions:** Prior to running the floor calibration method, make sure that the robot is in the middle of a large flat floor that is visible to the robot. The larger, flatter, and more visible the floor is, the better the floor calibration will be.

```bash
ros_find_floor_calibration
```

### 3. Broadcast Calibration

**Preconditions:** Prior to running the visualizer or consuming data, you must broadcast the dual LiDAR alignment and the floor calibrations. You should leave this running in a separate terminal.

```bash
ros_broadcast_calibration
```

## Visualization

A dedicated node has been provided to publish a combined view of all the calibration outputs (unified point cloud, floor inliers):

```bash
ros_visualize_calibration
```

You can find the all-in-one RViz configuration in the `rviz` directory showing the unified views:

```bash
rviz2 -d rviz/dual_lidar_calibration.rviz
```

*(Ensure that `ros_broadcast_calibration` is running to resolve the `floor_plane` transform).*

## Example Calibration File

After using all of the calibration steps, you can find the calibration file at:

```bash
echo $HELLO_FLEET_PATH/$HELLO_FLEET_ID/calibration_dual_lidar/dual_lidar_calibration.yaml
```

An example of a calibration YAML file follows:

```yaml
right_to_left_transform:
  data:
  - - -0.5377963718346568
    - 0.6648722076819781
    - -0.5183821079990824
    - -0.10937678240418136
  - - -0.6615787778171399
    - 0.048309946411164574
    - 0.748317893558433
    - 0.18978458559800884
  - - 0.5225787817960651
    - 0.7453932495869836
    - 0.4138844286593757
    - -0.09976708265415501
  - - 0.0
    - 0.0
    - 0.0
    - 1.0
  robot_id: stretch-se4-4010
  timestamp: '2026-07-02T11:44:40.651212'
  fit_method: gicp
  rmse: 0.6766259118614343
floor_to_base_link_transform:
  data:
  - - 0.9999643043180069
    - -2.4448049205598682e-05
    - 0.008449230266551321
    - 0.0
  - - 0.0
    - 0.9999958137861803
    - 0.002893511727052981
    - 1.3552527156068805e-20
  - - -0.008449265636983897
    - -0.0028934084411785296
    - 0.9999601182536172
    - 0.026050630323511632
  - - 0.0
    - 0.0
    - 0.0
    - 1.0
  robot_id: stretch-se4-4010
  timestamp: '2026-07-02T11:45:43.001395'
  fit_method: svd
  rmse: 0.011044131704765942
floor_model_params:
  data:
    normal:
    - -0.008449265636983897
    - -0.0028934084411785296
    - 0.9999601182536172
    distance: -0.026050630323511636
    description: 'Floor plane: normal [x,y,z] dot point + distance = 0'
  robot_id: stretch-se4-4010
  timestamp: '2026-07-02T11:45:43.001395'
  fit_method: svd
  rmse: 0.011044131704765942
```

### URDF Calibration

A copy of the calibration extrinsic transforms are also written to `echo $HELLO_FLEET_PATH/$HELLO_FLEET_ID/stretch_calibration_values.yaml`, and are loaded as part of the Calibrated URDF. You can read more about the Calibrated URDF at <https://github.com/hello-robot/stretch4\\_urdf/blob/main/stretch4\\_urdf/calibration.md>

## Calibration Details

This package uses a multi-step process to calibrate the dual LiDAR setup, align the robot with the floor, and define the robot's body mode.

### 1. Dual LiDAR Alignment (`ros_align_dual_lidar.py`)

The first step is to find the static rigid body transform between the two LiDARs ($T\_{left \leftarrow right}$).

* **Data Collection**: The script collects synchronized scan pairs from both LiDARs.
* **Registration**: It uses **small\_gicp** (Generalized Iterative Closest Point) to register the point cloud from the right LiDAR to the left LiDAR's frame.
* **Averaging**: To ensure robustness, the script performs this registration across multiple frames (default: 100 samples) and computes the average transform.
* **Result**: The computed transform is saved as `right_to_left_transform` in `dual_lidar_calibration.yaml` (located in the stretch\_user calibration\_dual\_lidar directory).

### 2. Floor Plane Calibration (`ros_find_floor_calibration.py`)

The second step is to determine the floor plane relative to the robot's base link and compute the `base_footprint` frame. This ensures the robot's URDF model sits correctly on the ground.

* **Data Accumulation**: The script transforms point clouds from both LiDARs into the `base_link` frame (using the previously computed dual-lidar transform and URDF transforms) and accumulates them to form a dense representation of the scene.
* **Iterative Plane Fitting**: An iterative algorithm is used to robustly find the floor:
  1. **Height Estimation**: A histogram of z-coordinates is used to find the approximate height of the floor, assuming it is the largest horizontal surface near the robot's feet.
  2. **Outlier Rejection**: Points are filtered based on their distance from the current estimated plane model. The threshold significantly tightens over iterations (from 10cm down to 3mm).
  3. **Normal Estimation**: **SVD (Singular Value Decomposition)** is performed on the inlying points to find the plane normal (the eigenvector corresponding to the smallest eigenvalue).
  4. **Refinement**: The normal and height are iteratively updated until convergence.
* **Frame Computation**: The `base_footprint` frame is defined such that:
  * Its origin is on the floor plane, directly below the `base_link` origin.
  * Its Z-axis is aligned with the floor normal.
  * Its X-axis is aligned with the projection of the `base_link` X-axis onto the floor.
* **Result**: This transform maps points from the `base_link` frame to the `base_footprint` frame. The transform `floor_to_base_link_transform` is saved to `dual_lidar_calibration.yaml` and as a static calibration value (the `base_ref` joint) in `stretch_calibration_values.yaml`.

## Body Shape Modeling

The Body Shape Model provides an advanced mechanism for constructing bounds modeling the actual 2D boundaries of the robot during operation, resulting in an independent, highly tunable calibration file.

### Motivation

A manipulator like Stretch physically elongates its collision footprint as its telescoping arm extends outwards. A static circle wastes navigable space when the arm is stowed and might not accurately shield the arm when extended. By generalizing the tracking to various shapes (such as a circle, ellipse, or tapered capsule bounding box) and coupling it to kinematics states, obstacle detection can maintain tighter margins correctly matched to the actual extension.

### How It Works & Available Shapes

The shape model routine works by capturing LiDAR data across varying dynamic configurations:

1. **Data Collection**: The ROS node iteratively sweeps the robot through programmed configurations, extracting massive point cloud cross-sections of the robot mapped against its immediate kinematics state.
2. **Model Fitting**: For each kinematics step, LiDAR data is mapped to a high-resolution 2D floor histogram to remove noise. The script then applies an optimizer to construct a highly accurate bounding footprint contour matching the geometric scatter.
3. **Available Shape Constraints**:
   * **`circle`**: Finds the minimal enclosing circle wrapping the physical footprint. Constant, legacy-style behavior capable.
   * **`ellipse_opencv`**: Uses algebraic distance minimization. Fast and generally robust.
   * **`ellipse_min_enclosing`**: Computes an optimal minimal area bounding ellipse ensuring total coverage (Khachiyan's algorithm).
   * **`ellipse_axis_aligned`**: Solves for a minimal bounding ellipse whose axes are strictly aligned with the robot's base coordinate frame (X/Y axes). Ideal for bilateral symmetry and avoiding rotational jumps.
   * **`tapered_capsule` (Default)**: Finds the minimal bounding contour forming a tapered capsule (the convex hull of two circles). This shape is ideal for the Stretch base + arm geometry.
4. **Data Parameterization**: The configured footprints are logged into a dynamic parameters YAML file permitting real-time interpolation of limits relative to kinematics joints. Users can toggle dependencies on arm extension (`FIT_ARM_EXTENSION_DEPENDENT_BODY_SHAPE`) and wrist yaw (`FIT_WRIST_YAW_DEPENDENT_BODY_SHAPE`). Note that if properties are disabled, the shape simplifies dynamically down to a static rigid bounding box.

### Visualizing Shape Capabilities

The RViz inspection nodes draw highly detailed boundary profiles augmented by transparent shaded overlays to visually help analyze the fitted bounds and calibration tolerances in real-time. Three distinct boundaries and two critical polygon shaded regions are published dynamically:

* **Minimum Model**: Bounded by a thin blue line and shaded with a transparent blue interior, this marks the bare-metal output of the optimization algorithm outlining the mathematical minimal envelope strictly enclosing the physical bounds of the robot.
* **Inner Boundary**: The inner red curve defining the structural margin buffer. The gap extending between the inner boundary and the blue minimum model explicitly operates as a dead zone to account for modeling errors or sensor noise; any LiDAR hits landing within this bare gap are completely ignored.
* **Obstacle Detection Region**: Shaded with a transparent red overlay between the inner and outer red boundary curves, this spans the active "Stop Zone". Any LiDAR reflections striking within this highlighted red region trigger proximity detections distinguishing against collision obstacles.

### Tuning & Parameter Configuration

The entire sequence is deeply tunable. Configurable aspects include iteration ranges for spatial sweeps, stop zone margins, and algorithmic curve solvers. **To customize these features or switch to a different fitting algorithm, edit the variables defined within the settings code module:** `stretch_dual_lidar_calibration/stretch_dual_lidar_calibration/body_shape_calibration_params.py`

### Usage Directions

This procedure requires access to unified data streams; guarantee that the background `ros_broadcast_calibration` is already active to relay the core `/tf` transforms.

1. **Collect Data**: Execute the automated collection sequence. By default, it manages folder naming automatically, but you can explicitly specify an output directory identifier.
2. **Fit the Shapes**: Route the resulting timestamped directory through to the shape engine.
3. **Verify the Output**: Launch RViz and inspect the new limits bounding curves.

**Example Sequence:**

*Terminal 1:*

```bash
# This creates a folder like 'collected_body_shape_data_2026xxxx_xxxxxx'
# Note: You can also specify a custom directory name as a first argument.
ros_collect_body_shape_data

fit_body_shape_model ./collected_body_shape_data_.../

ros_visualize_body_shape_calibration ./tapered_capsule_body_model_TIMESTAMP.yaml
```

*Terminal 2:*

```bash
rviz2 -d ./rviz/body_shape_calibration.rviz
```

***

## Acknowledgments

### small\_gicp

stretch\_robosense makes use of [small\_gicp](https://github.com/koide3/small_gicp), which is a library for fast 3D lidar scan registration. Kenji Koide from National Institute of Advanced Industrial Science and Technology (AIST) is the creator of small\_gicp. The repository was released in 2024 with an MIT License.

If you use small\_gicp as part of stretch\_dual\_lidar, please cite it using the following citation and consider leaving a comment [here](https://github.com/koide3/small_gicp/issues/) as requested by Kenji Koide. \*"It would help the author receive recognition in his organization and keep working on this project."\_ - [small\_gicp GitHub repository](https://github.com/koide3/small_gicp),

```
@article{small_gicp,
author = {Kenji Koide},
title = {{small\_gicp: Efficient and parallel algorithms for point cloud registration}},
journal = {Journal of Open Source Software},
month = aug,
number = {100},
pages = {6948},
volume = {9},
year = {2024},
doi = {10.21105/joss.06948}
}
```

Thank you Kenji Koide and other contributors for this helpful code!


# README

This is the repo for all high level Stretch 4 guides, primers, etc - distinct from documentation for code repos. It's content is surfaced in the GitBook documentation via GitSync too.


# Getting Started


# Quick Start Guide

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

### Hello, Human!

<figure><img src="/files/tWQnrZtwDTf4ob0Ownso" alt=""><figcaption></figcaption></figure>

Welcome to your new robot! Stretch 4 is a mobile manipulator - a robot consisting of a mobile base as well as an arm and tool, capable of both navigating and interacting with its environment. The lightweight and simplified design of Stretch is intended to facilitate friendlier interactions with people in human environments. Its compact footprint and slender manipulator have unique advantages for working in cluttered, real-world spaces, and a first-class sensor suite provides an unparalleled understanding of the world around it.

Whether this is your first robot or you're an experienced user, these introductory tutorials are a great way to gain an understanding of the unique elements of Stretch and how to begin working with it. We'll explore critical safety features, get familiar with the robot's joints and sensors, and run some out-of-the-box demonstrations of some of Stretch's key features. In this first tutorial, we'll learn how to safely turn on the robot and drive it around using the gamepad controller.

### Before You Begin

#### Safety

If Stretch is not used properly, it has the potential to cause harm. We strongly recommend that all users review the [Stretch 4 Safety Guide](https://docs.hello-robot.com/stretch-4-safety-guide/) before operating the robot.&#x20;

#### **Unboxing**

If you need assistance unboxing the robot, please refer to the following video guide, or contact Hello Robot support for additional guidance.

{% hint style="info" %}
Hello Robot **strongly recommends** that you retain all of Stretch's packaging materials, including the cardboard containers and any foam and plastic structures. These will be required if you ever need to transport the robot to a new location or return it to Hello Robot for repairs. Replacements can be obtained through Hello Robot support for the cost of materials plus shipping.
{% endhint %}

{% embed url="<https://vimeo.com/1205958774/cdff06759a>" %}

<details>

<summary>Unboxing Key Points</summary>

* Move the box around with a wheeled dolly, or carry it with at least two people.
* Open the box carefully from the labeled side, using an Xacto knife or box cutter to remove the tape.
* Pull on the robot mast to slide the whole structure out of the box.
* Lift the top foam block vertically off of the "Hat" structure.
* Use a 4mm Hex key to remove the four screws connecting the "Hat" to the top of Stretch. Make sure to support the "Hat" during this process, as it can fall when the final screw is removed. We recommend a second person assist with this step for maximum safety.
* Remove any blue protective film on the robot lidars and cameras.
* Pink clamps on the arm prevent motion of the shoulder during transit - snap them open as directed and remove them.
* An additional pink clamp locks the wrist and telescoping arm structure in place - open the clip that holds this structure together, then remove it from the wrist by depressing the pink quick-release button, and sliding it downwards.
* A velcro strap holds the bottom foam structure together - remove this along with the box on top of the base shell (if installed).
* Tilting the robot back slightly, remove the front half of the bottom foam block; then gently slide the robot forward to rest on the ground.

</details>

#### **Installing Stretch 4's Battery**

{% hint style="danger" %}
**If the battery or battery cable appear to be damaged, do not power on the robot and contact Hello Robot immediately for support.**
{% endhint %}

Stretch 4 uses a large LiFePO4 battery, which gives the product superior runtime (nominally 4-8 hours) and cycle life. Stretch's battery comes in a heavy protective metal case that keeps the battery safe from damage as well as acting as a significant ballast weight, adding to the robot's stability and ability to exert forces while completing tasks. As a lithium-ion battery, the battery must be shipped as "Dangerous Goods" and therefore must ship separately from the robot.

To unbox and install Stretch's battery, please refer to the following video guide:

{% embed url="<https://vimeo.com/1205958773/e858b9e383>" %}

<details>

<summary>Battery Installation Key Points</summary>

* Use a utility knife or box cutter to open the battery box, and lift the foam structure upwards.
* Rotate the battery so that it is oriented vertically, then remove the cinch strap.
* Remove the foam piece, and lift the battery out safely using the attached handles.
* Remove Stretch's top shell by lifting upwards sharply, and set aside.
* Align the battery between the two clamp connections, and slide into place. When properly aligned, the battery should drop downwards a few millimeters and no longer slide in any direction.
* Close the two clamp connectors to secure the battery in place. Then plug the battery cable into the battery connector.
* Replace the top shell over the battery and confirm that it is properly seated in place.

</details>

{% hint style="info" %}
Stretch batteries typically arrive at a low state of charge due to lithium battery shipping regulations. For best results, plug in the charger for around 30 minutes - 2 hours before using the robot untethered.
{% endhint %}

Stretch's battery can be removed by following those same steps in reverse.&#x20;

{% hint style="warning" %}
**Whenever Stretch is being transported by car, carried by hand, or shipped, the battery must be uninstalled and handled separately.**
{% endhint %}

#### Installing the Stretch Gripper

Stretch 4 comes equipped with a quick-change end effector, which allows you to quickly install and uninstall tools with no cabling. Hello Robot generally recommends doing this while the robot (or at least the end-of-arm system) is powered down, to reduce the risk of an electrical issue.\
\
To install the Stretch gripper:

* Align the gripper with the cutout in the wrist mounting plate
* Slide the gripper upwards until you hear a click, depressing the pink release button if needed.

{% embed url="<https://vimeo.com/1205961955?fe=ci&fl=sv&share=copy>" %}

#### Moving the Robot Shoulder while Powered Off

Stretch 4 has a mechanical brake on the lift joint that engages while the robot is powered off, preventing the arm from dropping due to gravity. If the arm needs to be repositioned while the robot is off, there is a brake release button located in the shoulder. Press and hold the button for at least four seconds - you will hear a click, and the arm will become backdrivable until the button is released. Please note that this function requires power, so the battery must be installed in order to utilize it.

{% embed url="<https://vimeo.com/1205963988>" %}

#### **Powering on the Robot**

To power on the robot, press the power button located in the trunk once, and it will illuminate green. The robot may take 20-30 seconds to fully boot, and you should hear the Ubuntu startup sound when it is complete (unless this is turned off in the settings).

{% embed url="<https://vimeo.com/1205966840>" %}

### **Safety Features**

Before you begin operating Stretch, there are some additional features that are important to understand for safe operation of the robot.

#### **Runstop Button**

The Runstop button is located on the side of the robot's head. Normally, this button will be steadily illuminated white. Pressing this button puts the robot into "runstop"; it interrupts the motion of the robot's primary joints during operation, causing it to stop moving and making all of these joints backdrivable. Just tap it, you'll hear a beep and the button will start flashing. You can now freely move the arm, lift, and wheels of the robot.

This can be useful if the robot makes an unsafe motion, or if you just want to roll the robot around or reposition its arm. To disable the Runstop, hold the button down for two to three seconds. After the beep, the button will illuminate steadily again and motion can resume.

{% columns %}
{% column %}
{% embed url="<https://vimeo.com/1205968902?fe=ci&fl=sv&share=copy>" %}
{% endcolumn %}

{% column %}
{% embed url="<https://vimeo.com/1205968903?fe=ci&fl=sv&share=copy>" %}
{% endcolumn %}
{% endcolumns %}

{% hint style="info" %}
The Runstop button is the easiest way to cancel an unexpected or undesired movement of the robot. If you're ever surprised or made uncomfortable by Stretch moving during these tutorials, just press the button and the robot will immediately pause, preventing collisions and allowing you to figure out what went wrong.&#x20;
{% endhint %}

Some of Stretch's joints can be configured in software to hold their position when runstopped, rather than to become backdrivable. This is useful for contexts where losing motor power may become inappropriate - for example, when holding a large payload with the wrist pitch and gripper motors. <br>

#### **Battery Indicator Lightbar**

The battery indicator lightbar on the side of Stretch's head provides a simple way to quickly ascertain the robot's battery level. Eight LEDs display the robot's current state of charge from 0 to 100%. Hello Robot generally recommends placing the robot on its charger once the charge has reached \~25%, to minimize the chance of accidentally running the robot out of battery (see [#power-management](#power-management "mention") below for instructions). It is perfectly fine to continue to use and develop on the robot while the charger is connected.<br>

<figure><img src="/files/GGYlCcqFRv04EBuAxVV0" alt="" width="375"><figcaption><p>Stretch battery indicator displaying full charge (8/8 LEDs illuminated)</p></figcaption></figure>

When the robot Runstop is active, the battery indicator will flash at \~1 Hz. When the robot battery charger is connected, the battery indicator will slowly strobe to indicate charging.

#### Guarded Contact

Stretch has a built-in contact detection system called Guarded Contact on the base, lift and arm motors, designed to limit unwanted forces that Stretch could apply to a person or its environment. This system sets a threshold of how strongly Stretch can exert its motors, and uses current sensing to determine if actuator effort exceeds this threshold during joint motion. If a strong force is detected, the safety controller for the joint is temporarily enabled, making the joint backdrivable and typically causing a small bounce-back behavior. These thresholds can be tuned or configured depending on the robot's application and the robot's current task.<br>

{% columns %}
{% column %}

<figure><img src="/files/RKD1j4cmCLFb1kdpGodG" alt=""><figcaption></figcaption></figure>
{% endcolumn %}

{% column %}

<figure><img src="/files/b6lPggPHQD0gYD2Pb2jE" alt=""><figcaption></figcaption></figure>
{% endcolumn %}

{% column %}

<figure><img src="/files/MgYmVNpxxjlyyGnOol24" alt=""><figcaption></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

### **Gamepad Teleoperation**

Teleoperation using an Xbox-style gamepad controller is one of the easiest ways to operate and get familiar with Stretch. The robot comes ready to drive out-of-the-box using the included gamepad controller.&#x20;

To start the teleoperation demo:

1. Find the gamepad controller that shipped with Stretch
2. Plug the USB dongle for the controller into one of Stretch's USB ports (we typically recommend the port in the top of the head, to reduce the risk of an accidental collision with the environment).<br>

   <figure><img src="/files/2rWwo7kLLSNM1CIGbRhh" alt="" width="188"><figcaption></figcaption></figure>
3. Make sure the small switch on the back of the controller is in the right-most position to enable wireless control.

   <figure><img src="/files/In7sWNMDJ1CTU1QEqp4Y" alt="" width="188"><figcaption></figcaption></figure>
4. Press the center "Connect" button <img src="/files/TwJMP5OB7d1KnXLnc4WU" alt="" data-size="line"> on the controller - it should flash momentarily, then illuminate solid and vibrate briefly upon connection.
5. Make sure the space around the robot is clear. Hit the Home robot button to the right of the center button. The robot will begin its homing routine, finding the zero position of all its joints. Be careful not to interfere with these movements, as it may cause the zero position to be set incorrectly. When this procedure is completed (\~30 seconds), the robot will beep once. Stretch is now ready to drive!

{% hint style="info" %}
If nothing happens when you hit the Connect or Home buttons, please see the Troubleshooting section below.
{% endhint %}

#### **Using Gamepad Teleop - Direct Control Mode**

This image shows the mapping between the controller buttons and the robot joints:

<figure><img src="/files/ajzuGUiqjFP4Z4VNqEDw" alt=""><figcaption></figcaption></figure>

Here's a few things to try:

* Hold the Stow Robot button for 3 seconds. The robot will assume a stowed pose, with the arm low and the gripper tucked inside the robot's footprint. This position keeps the robot compact and its center of gravity low, so we recommend it when driving the robot base.
* Practice driving the robot around using the left joystick for translation and bumpers for rotation.
* While driving the omnibase, hold down on the left trigger to reduce speed. The trigger is analog, so top speed will be reduced proportional to how far the trigger is depressed.
* Practice positioning the arm using the Lift and Arm controls on the D-pad, and the left trigger if desired.
* Practice positioning the Wrist Yaw and Pitch using the right joystick.
* Try pressing the Runstop button while moving the robot, then reset it by holding the button down for 2 seconds. You can always use this as the safest way to pause the robot if any unwanted behavior occurs.
* Holding down the right trigger modifies the behavior of some of the gamepad buttons. Try holding the right trigger and then pressing the bumper buttons to control the Wrist Roll instead of Omnibase Rotation.
* Gamepad teleop comes with a few preset Speed and Strength options - these control the top speed of the robot joints and the amount of force the robot can apply before Guarded Contacts are triggered. While holding the right trigger, press A to cycle through the Speed presets, or B to cycle through Strength presets. The robot should use the speaker to state the current preset being used.
* Stretch can operate in either a right-handed or left-handed mode, switching which side of the arm  the gripper is offset on. To swap between these quickly, hold the right trigger and then press the Home Robot button.
* Try utilizing all of these controls to grasp an object from the ground or a countertop. Hold the A button to close the gripper, or the B button to open the gripper.
* Try delivering an object to a person.
* Try a task that requires you to use two or more of the robot's joints at once, like opening a door or cabinet.

Once you have explored all of the "Easy Mode" teleoperation mapping, you can press the Y button to switch to the second gamepad teleop mode - Flying Gripper

#### Using Gamepad Teleop - Flying Gripper Mode

In Flying Gripper mode, you simply control the position and orientation of the robot's end effector, and the robot uses inverse kinematics to decide how to move its body to create the proper motions. In this mode, the button mapping is described below:

<figure><img src="/files/aoEV9Li2ATA4SyD2igNr" alt=""><figcaption></figcaption></figure>

* Use the right stick to point the robot's gripper in a direction, then hold up on the left stick. The gripper should travel directly in the direction the fingertips are pointing - moving any of the robot's joints in order to make this happen.&#x20;
* Move the left stick left and right, and the gripper will move orthogonally to the direction it is pointing.
* Pull the left stick backwards, and the gripper will move directly away from the direction it is pointing.
* Try opening the gripper (using the B button) and then pointing it directly at an object. Use the left stick to drive the robot gripper directly towards the object to pick it up.
* The same modifiers for left/right handed switching, as well as Strength and Speed settings, still apply in this mode.&#x20;
* For some types of tasks, you might prefer to switch back and forth between modes. Pressing Y will return you to the original direct control mode.

### Power Management

#### Shutting Down Stretch

Stretch can be powered down with a single press of the power button in the trunk. The button will illuminate Red. The robot PC should initiate a Safe Shutdown procedure and may remain powered for up to 60 seconds before completely depowering.&#x20;

Stretch has two different powered off states - Sleep, and&#x20;

#### **Charging Stretch's Batteries**

Stretch comes with a 36V battery charger that connects to standard wall power and can be used to charge the robot's LiFePO4 battery. The battery charger can be connected whether or not the robot is powered off or on. An onboard Battery Management System maintains the state of charge and manages long-term battery health.

To charge Stretch's battery, connect the battery charger to power, then plug the charger into the barrel jack port located in the robot trunk. Make sure it is plugged in fully and firmly. If Stretch is powered off (or powered on but not drawing much current), it should take approximately two hours to charge the battery near full. It is entirely possible to keep the robot on and even develop on it while the battery is charging, though care should be taken if the mobile base is in use to ensure that the connection does not become loose or unplugged.<br>

<figure><img src="/files/0BPeJN5JecTPd7O1NUHW" alt="" width="270"><figcaption></figcaption></figure>

{% hint style="danger" %}
If you also own an older version of Stretch (1-3), do **not** interchange the two chargers! Only charge Stretch 4 with the included charger that comes with the robot, and do not use this charger with other equipment.
{% endhint %}

### Moving Stretch

Stretch can be easily transported between different rooms, buildings, or research sites. Here are some suggestions for moving Stretch around more easily:

* Over reasonably short distances, backdriving Stretch is the easiest approach. With the robot runstopped or powered down, simple grab the robot by the mast and push it around to where you need to go.
* Over moderate distances or more irregular terrain, we recommend the use of a hand truck. For extra protection, place the robot base into the foam block that it was shipped in, and use the truck to move it around securely.
* Stretch can be transported by car. We recommend packaging Stretch in its original shipping box and hardware, which are designed to protect the sensors from incidental damage. The box fits in most mid-size hatchback vehicles with the back seats folded down (eg Kia Niro, Toyota RAV4).

{% hint style="warning" %}
Stretch is not waterproof! If transporting it outside, make sure to keep it safe from the elements.
{% endhint %}

<details>

<summary>Quick Start Troubleshooting</summary>

* **Gamepad Controller won't connect (center button flashing)**
  * Verify that the USB dongle is connected to a robot USB port, and that its LED is also flashing
  * Verify that the switch on the back of the gamepad is in the rightmost position
  * Try the following steps to force a connection:
    * Do not touch any buttons on the gamepad until its LEDs stop illuminating (10-20 seconds)
    * Press the small button on top of the USB dongle - it should start flashing at a higher speed
    * Without touching any other buttons on the gamepad, hold down the center button for several seconds, until the center button LED also begins flashing at a higher speed
    * Leave the hardware in this state until both stop flashing and the controller vibrates briefly to indicate a connection has been made. This can take up to two minutes.

</details>

In the next section, we'll learn how to connect to stretch and set it up for developing with success.


# Setting Up Stretch

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

### Connecting to Stretch - Tethered

In order to work with Stretch's software, or to develop and test your own code, you'll first need to connect to the computer inside. The simplest way to do this is to directly connect a monitor, keyboard, and mouse to the robot using the exposed ports in the robot trunk.

<figure><img src="/files/nGTBU1JyDeuolx8x5HCY" alt=""><figcaption></figcaption></figure>

Stretch 4 contains a mini PC (model: Asus NUC 15) that can be accessed using the ports as shown above. Connect a monitor to the HDMI port, and a mouse and keyboard (we like to use a wireless dongle) to any of the USB ports, and make sure the robot is powered on (power button illuminated green). The Ubuntu desktop environment should appear on the connected monitor. Stretch 4 is running the Ubuntu 24.04 operating system.

The default user login credentials came in the box with the robot. By default, the robot is not configured to ask for your password on boot, but may ask for it later if the NUC goes to sleep.<br>

{% columns %}
{% column %}

<figure><img src="/files/rcE9BGKj6UQcw5PVUGsg" alt="robot trunk"><figcaption></figcaption></figure>
{% endcolumn %}

{% column %}

<figure><img src="/files/313MIbvfmskphTbjneSm" alt=""><figcaption></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

#### Setting up Wi-Fi

One of the first things you'll probably want to do with Stretch is to connect it to the internet. While there is an Ethernet port in the trunk as well, its much more likely that you'll want to use Wi-Fi. A Wi-Fi connection will also enable you to use untethered connections to the robot in the future (more details below).

This is as simple as opening the Wi-Fi menu at the top right, selecting your network from the list, and inputting your network password if necessary. We recommend selecting the option to auto-connect to the network, in order to make working with the robot easier in the future.

<figure><img src="/files/qSCEv3zTgbaj0BofGpG5" alt=""><figcaption></figcaption></figure>

### Connecting to Stretch - Untethered

Once your robot is on the network, you can access it remotely from another computer. This is the preferred way to work with Stretch for most users. There are multiple options for achieving this, including remote desktop software like RustDesk or TeamViewer, SSH connections, Tailscale, ROS2 networking, PyZMQ, etc. For the purposes of this tutorial, we will focus on Ubuntu Remote Desktop Protocol (RDP) over a local network.

#### RDP with Remote Login

Ubuntu 24.04 ships with a built-in Remote Login feature that allows you to access your Ubuntu User Account remotely using Remote Desktop Protocol (RDP). Multiple users can even log into the robot at the same time for development, if necessary.

The official Ubuntu docs for setting up Remote Login are here: <https://help.ubuntu.com/stable/ubuntu-help/remote-login.html.ro>

You do not need a HDMI Dummy Dongle to use Remote Login. Note that Remote Login is different from Remote Desktop, which shares the same menu in Ubuntu's Settings. Remote Desktop does require a Dummy Dongle and requires you to remain logged in.

#### Robot setup instructions

The following instructions will walk you through setting up Remote Login.

{% hint style="info" %}
Please follow these steps while you are on the hello-robot or default user account - setting up Remote Login on multiple accounts might cause conflicts if the username-password pair is the same.
{% endhint %}

1. On your robot, navigate to Settings -> System -> Remote Desktop <br>

   <figure><img src="/files/Mlki2CgsXIwJ4qZPvDen" alt=""><figcaption></figcaption></figure>
2. Select the Remote Login tab, click Unlock and type in your password. Toggle to Enable Remote Login <br>

   <figure><img src="/files/McAgWA2t8ex7fLHv9LnH" alt=""><figcaption></figcaption></figure>
3. At the bottom of the window, enter a username and strong password combination that will be shared by all users remotely connecting to this robot; it’s a good idea to NOT use the password for your User Account because anyone on the network can connect to the login screen after setting up Remote Login. This password will be shared by all users for remote access to the Login Screen (not a particular user account).&#x20;
4. Retrieve the ip address of this robot using `hostname -I`.

{% hint style="warning" %}
It is important to follow your institution or organization's security best practices for setting up remote access and Ubuntu User Account Credentials.
{% endhint %}

#### Client Setup Instructions

On the computer you want to connect to the robot, follow the below instructions based on your operating system:

{% tabs %}
{% tab title="Linux Client Setup" %}

1. Run `sudo apt install remmina`
2. Open Remmina and click the "+" icon. Enter your robot's IP, username and password you configured in the previous step. Click Save and Connect to test your connection
3. Click the Toggle Dynamic Resolution button on the sidebar to make the RDP window use your monitor’s resolution.
4. Login to your User Account to start using your robot.
5. To configure audio: on the client, right click the Remmina connection -> Edit -> go to the Advanced tab -> Audio output mode. Choose Remote to play audio through the robot’s speakers. Local to play it using the client speakers. Then click Save.
   {% endtab %}

{% tab title="MacOS Client Setup" %}

1. Get the Windows App, offered by Microsoft Corporation, from the [Apple Store](https://apps.apple.com/us/app/windows-app/id1295203466)
2. Create a new Computer connection and change the PC name to the IP Address of the robot. You can add a Friendly Name to help identify the robot. Under Credentials, you can click to “Add Credentials” and save your password so you do not have to enter it every time you connect.

> Note: if you encounter a blank screen error on MacOS, export your connection and change the line that says `use redirection server name:i:1`, and re-import your configuration, as suggested in: <https://askubuntu.com/a/1528263>
> {% endtab %}

{% tab title="Windows Client Setup" %}

1. Open a Command Prompt window by pressing Win + R, then run  `mstsc`&#x20;
2. In the "Computer" field, type the IP address as retrieved from the robot above
3. Click "Connect". You may receive a certificate warning; click Yes to proceed.
4. When the login screen appears, enter your robot username and password
   {% endtab %}
   {% endtabs %}

You are now connected remotely to Stretch! This can be a convenient way to use and develop on the robot. To learn more about different ways of connecting to Stretch, see the full guide here: [Connecting to Stretch](/stretch4_docs/working-with-stretch/general_use/connecting-to-stretch).

### Turning off Gamepad Teleoperation

Out of the box, Stretch is configured to launch the gamepad teleoperation demo in the background at startup. While this is running, other code cannot use the robot. You will need to free the robot process so that your code can use it.

Run the below command in the terminal to kill the server and stop the startup script.

```bash
stretch_body_server --kill
```

You can also disable the autostart feature entirely. Search for "**Startup Applications**" from the Apps menu and uncheck the box for `hello_robot_xbox_teleop`.<br>

In the next section, we'll learn about Stretch's joints and sensors and some of the basic commands for controlling them.


# Robot Overview

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

Stretch comes with a number of command line utilities that you can run by opening a Terminal window. Below, we will explore some commonly used CLI tools as a way of accessing Stretch's individual motors and sensors.

### System Check

One of the most useful tools for Stretch is the System Check function. This checks out the state of Stretch's hardware, firmware, and software, and reports any errors or issues it can find. It is good practice to run this whenever you start using the robot, and can be&#x20;

To run a system check, open a Teminal and enter:

```bash
stretch_system_check
```

If all checks pass, your robot is ready to go. Use `stretch_about` to print robot identity and configuration, and `stretch_params` to inspect all robot parameters.

### Homing

If you've turned off your robot since running the first tutorial, System Check might inform you that the robot needs to be homed.

Homing refers to a procedure where Stretch moves all of its joints to the end of travel one time, to find the zero positions. Until the robot has been homed, it does not know the position of its manipulator and will refuse many types of commands. The homing procedure takes approximately 30 seconds, and must be run once every time the robot is powered on.

To home the robot:

```bash
stretch_robot_home
```

The robot will beep when finished. The motors will remember their homing state while the robot remains powered on — using the runstop, backdriving the robot, or restarting the NUC will **not** require rehoming. Powering the robot down completely will require homing again on the next boot.

If your system check did not complete due to rehoming, run it again now.&#x20;

### Stowing

As a tall robot, Stretch is most stable when it keeps its center of mass relatively low. When navigating the robot through its environment, we recommend keeping the arm and wrist retracted inside the base footprint and the  To stow the robot to its compact travel pose:

```bash
stretch_robot_stow
```

{% columns %}
{% column %}
{% embed url="<https://vimeo.com/1208949624>" %}
{% endcolumn %}

{% column %}
{% embed url="<https://vimeo.com/1208949625>" %}
{% endcolumn %}
{% endcolumns %}

## Motors and Joints

### Omnibase

Stretch 4 has a three-wheeled omnibase, consisting of three holonomic closed-loop stepper motors each driving one wheel. The omnibase supports full planar motion — forward, sideways, diagonal, and rotation in place, all simultaneously. It also supports Guarded Contact sensitivity, which can be calibrated and tuned per use case.

Wheel numbering increases counter-clockwise, with "Wheel 0" to the left of the forward direction. This is consistent with the ROS frame convention where X+ is forward and Y+ is to the left, forming a right-handed coordinate frame.

To jog the base, you can use this CLI tool:

```bash
stretch_omni_base_jog
```

### Lift

The lift provides vertical translation of the arm, reaching up to 47 inches high and all the way down to the ground. It is driven by a closed-loop stepper motor through a low gear-ratio belt drive, providing smooth and precise motion.

To jog the lift, you can use this CLI tool:

```bash
stretch_lift_jog
```

### Arm

The arm comprises 4 telescoping links set on rollers, extending 21.6 inches beyond the base footprint and retracting to stow within it. Its proprietary drivetrain is driven by a stepper motor with closed-loop control and current sensing, enabling contact detection during motion. In combination, the lift, arm, and mobile base provide three orthogonal axes of motion — a Cartesian system for end-effector placement.

To jog the arm, you can use this CLI tool:

```bash
stretch_arm_jog
```

### Wrist and Gripper

#### Dexterous Wrist

Stretch 4 has a three degree-of-freedom wrist with yaw, pitch, and roll actuation — all driven by Feetech smart servo actuators.

| Axis        | Raw Servo Range | Notes                                                                      |
| ----------- | --------------- | -------------------------------------------------------------------------- |
| Wrist Yaw   | 310°            | `range_deg: [-65, 245]`                                                    |
| Wrist Pitch | 310°            | `range_deg: [-65, 245]` — effective range reduced by self-collision limits |
| Wrist Roll  | 310°            | `range_deg: [-65, 245]`                                                    |

To jog individual wrist joints:

```bash
stretch_dex_wrist_jog
```

To home all wrist joints:

```bash
stretch_dex_wrist_home
```

Individual axis homing is also available with `stretch_wrist_yaw_home`, `stretch_wrist_pitch_home`, and `stretch_wrist_roll_home`.

#### Gripper

The compliant gripper is a robust single-degree-of-freedom end-effector. A Feetech smart servo motor drives the center of the spring mechanism, which causes the outer fingers to flex and provide a grasping force.

```bash
stretch_gripper_jog
stretch_gripper_home
```

#### Motor Errors and Reset

The Feetech motors in the wrist and gripper can enter an error state from over-force or over-temperature events. When in this state, the motor becomes backdrivable, stops responding to commands, and the LED on the motor body blinks red.

To clear this error, the motors must be power cycled, either through rebooting the robot or with this CLI tool:&#x20;

```bash
stretch_feetech_reboot
```

This reboots all Feetech motors and resets their error status. You will need to re-run wrist and gripper homing after doing this, since the wrist\_yaw and gripper joints lose their homed positions.

{% hint style="info" %}
Rebooting only the robot PC will not clear errors like these, as the robot motors remain powered during the computer power cycle.
{% endhint %}

## Sensors

Stretch 4 includes the following sensors:

* **Cameras**
  * Luxonis OAK-FFC-3P Camera Module (Head: 3-camera array)
  * Luxonis OAK-D-SR (Gripper: stereo pair)
* **Lidar**
  * 2x Hesai JT128 3D LiDAR (Left and Right, mounted on the head)
* Speaker (mounted at the bottom of the head)
* Pixart Single-Axis Distance Sensor Array (base)

### Head Cameras

Stretch 4 has a Luxonis OAK-FFC-3P camera module mounted in the head with three RGB cameras:

| Camera | Shutter        | Type                  | Resolution | Target FPS |
| ------ | -------------- | --------------------- | ---------- | ---------- |
| Left   | Global Shutter | Fish-eye wide angle   | 1920×1200  | 30         |
| Right  | Global Shutter | Fish-eye wide angle   | 1920×1200  | 30         |
| Center | Global Shutter | High-resolution color | 4032×3040  | 10         |

The left and right cameras are fish-eye wide-angle cameras providing a broad environmental field of view. The center camera is a high-resolution color camera. All three are angled to maximize combined visual coverage in every frame.

To easily display any combination of camera feeds and open them in `rerun` or `opencv` , run the CLI tool stretch\_camera\_show with flags for the camera and for the visualizer - for example:

```bash
stretch_camera_show --left --rerun
stretch_camera_show --center --opencv
stretch_camera_show --right --rerun
stretch_camera_show --left_right_center --opencv
```

{% hint style="info" %}
Use the flag -h to see all options
{% endhint %}

### Gripper Cameras

Stretch 4 has an OAK-D-SR (Short Range) mounted at the gripper, an integrated passive stereo depth camera module optimized for short range sensing, perfect for sensing depth around the fingertips during grasping. These are detected as a separate Luxonis device from the head cameras (2-sensor device vs. the 3-sensor head device).

To visualize this camera view, use the same CLI command as above:<br>

```bash
stretch_camera_show --gripper --rerun
stretch_camera_show --gripper --opencv
```

### 3D LiDAR

Stretch 4 has two Hesai JT128 3D LiDAR units mounted on the head — a left lidar and a right lidar — providing full 3D point cloud coverage of the environment. These are used for mapping, navigation, and RGBD fusion with the head cameras.

Each lidar communicates over Ethernet. The NUC holds a single network profile with two IP addresses to communicate with both lidars and the Jetson simultaneously.

To view the combined point cloud in real time, use the CLI command:

```bash
stretch_lidar_show
```

### Emulated RGBD from Head Cameras

The head cameras can be fused with the head Hesai lidars to produce RGBD (color + depth) point clouds. To visualize this:

```bash
stretch_rgbd_show
```

### Speaker and Microphone

The robot has a speaker and noise-cancelling microphone mounted at the bottom of the head, allowing Stretch to communicate from across a room. To test audio output:

```bash
stretch_audio_test
```

### Line Sensor Array

Stretch 4 includes an array of SAS (single-axis sensors)  floor-facing  line sensor array on the base. This array continuously scans the floor in front of the robot and uses an on-robot model to classify the surface as floor or obstacle, enabling low-latency hazard detection independent of the lidar.

The line sensor runs in a dedicated background worker process at approximately 30 Hz, and its output is used by the omnibase to automatically limit velocity when an obstacle is detected in the direction of travel. To visualize the line sensors:

```bash
stretch_line_sensor_viz_3d
```

#### LED Eyes

The robot head also has two LED eye displays. These support a set of built-in animations including idle glow, blinking, directional gaze, rainbow spin, alert, and happy states:

```bash
stretch_eye_animations
```

## Developer I/O

Stretch 4 contains additional ports connected to the onboard NUC that can be used for accessories:

* **Trunk:** 1× USB-A 3.0 ports, 2x USB-A 2.0 ports, 1× Ethernet port, 1× HDMI port.
* **Head (top):** 1× USB-A 3.0 port, 1x USB-C Port
* **End-of-arm:** 1× USB-A 2.0 port
* **Wrist:** Quick-connect mechanism for tool attachment.

There are also threaded mounting points on the head to add additional sensors.


# Demo - Mapping and Navigation

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

A fundamental feature of mobile robots like Stretch is the ability to create a map of an environment and then navigate around it autonomously. The following tutorial will explain how to quickly build a 2-dimensional map that the robot can use to navigate around your current environment, using ROS 2 software called Nav2.

## Building a Map

Building a map of the environment allows the robot to understand where it is currently located, and where you would like it to travel to. Stretch builds maps using the 3D LiDAR sensors located in the robot's head. These sensors allow Stretch to understand the structure of the space around the robot, including the floor, walls, and obstacles. By driving the robot slowly around the space, Stretch builds its understanding of the space, and will create a 2D map of all of the area that it can traverse freely.

{% stepper %}
{% step %}

### Home and Stow the Robot

To make a map accurately, it is important that Stretch's arm and gripper are tucked out of the way so that they do not block the view of the sensors. To do this, run the following commands on Stretch, making sure the arm and wrist have free space to move.

```
stretch_robot_home
stretch_robot_stow
```

Check the expected homing and stowing behaviours in [Robot Overview](/stretch4_docs/working-with-stretch/getting-started/robot-overview)
{% endstep %}

{% step %}

### Launch the ROS 2 Mapping Node

Start the offline mapping launch file by running the following command in a terminal:

```
ros2 launch stretch_nav2 offline_mapping.launch.py
```

If you want to display the live feed from the robot cameras while driving, you can also run the following in a separate terminal window:

```
stretch_camera_show --opencv --right
```

{% endstep %}

{% step %}

### Begin Teleoperation

In a new terminal, start gamepad teleop by running:

```
stretch_gamepad_teleop
```

Just as you did in the [Broken mention](broken://pages/OHoQ2HEtx1R5cPw2nEaH), use the gamepad left stick and bumpers to move the robot around the space. You can watch the laser scan in RViz to understand what areas the robot has already scanned. Blank sections should be filled in by driving the robot around to get a better view.

Once the map of your space looks fairly complete, you can move to the next step. We recommend starting with just one or two rooms for this initial demonstration.
{% endstep %}

{% step %}

### Save the Map

Open a new terminal (leaving the one with the mapping node running) and use the following command to save the map to your fleet directory, replacing `<map_name>` with the name you want to use (eg `stretch_demo_map`)

```
ros2 run nav2_map_server map_saver_cli -f ${HELLO_FLEET_PATH}/maps/<map_name>
```

{% endstep %}

{% step %}

### Close the Mapping Node

Go to the terminal window with the mapping launch file (the one from Step 2), and press Ctrl+C to close the stop the mapping script.
{% endstep %}
{% endstepper %}

{% embed url="<https://player.vimeo.com/video/1205615528>" %}

## Navigating the Map <a href="#phase-2-navigating-the-generated-map" id="phase-2-navigating-the-generated-map"></a>

#### Phase 2: Navigating the Generated Map <a href="#phase-2-navigating-the-generated-map" id="phase-2-navigating-the-generated-map"></a>

{% hint style="warning" %}
Avoid initiating navigation while the robot is docked in the **Stretch Docking Station**. Always undock the robot completely before sending a navigation goal.
{% endhint %}

{% hint style="danger" %}
This demo does **NOT** utilize the line sensors for small object or cliff detection. Keep the area clear, and **keep the robot away from stairs or ledges at all times** during this demo.
{% endhint %}

Now that we have saved a map, we can use it to tell the robot to autonomously navigate through your environment. Stretch will still look for new obstacles around its body, so if an object or person moves through the space, it will adjust and adapt to avoid collisions and plan its movements intelligently.

{% stepper %}
{% step %}

### Launch the ROS 2 Navigation Node

Use the following terminal command to start Nav2 and point it to the .YAML file for the map you just created. Don't forget to update the `<map_name>` parameter to the name you chose above.

```
ros2 launch stretch_nav2 navigation_mppi.launch.py map:=${HELLO_FLEET_PATH}/maps/<map_name>.yaml
```

{% endstep %}

{% step %}

### Set the Initial Robot Position

You should now see the map open on Stretch in the RViz2 software. However, the robot doesn't yet know where it is currently positioned on the map. We'll need to tell it approximately where it is located and which direction it is facing.

In the RViz2 window showing the map, click on the "2D Pose Estimate" button, then click on the robot's current location in the map, and hold and drag in the direction the robot is currently facing. Release to set the robot's orientation.

\#this definitely needs a gif or short video
{% endstep %}

{% step %}

### Set a Navigation Goal

In RViz2, click the "Nav2 Goal" button, then click a position on the map where you want the robot to navigate to. Make sure the position is inside the free area on the map.

Stretch should now start driving to the location you selected autonomously! Try sending Stretch to different points in the room, then interact with it by moving objects around or standing in front of the robot to force it to react to these new obstacles and find alternative routes.
{% endstep %}
{% endstepper %}

<details>

<summary><strong>Troubleshooting and Debugging</strong></summary>

1. **Isolate node errors:** If navigation fails at startup, try adding `use_composition:=false` to the navigation launch command. This starts nodes outside a shared container, which makes errors easier to spot.
2. **Remote operation:** If you use a remote desktop session, you may need to plug in a dummy HDMI adapter so the display server starts correctly. One of these shipped in the accessory box along with your robot
3. **Navigation does not start after setting the pose:** If too much time passes before you set the robot's initial pose, the navigation stack may need to be reset. A common symptom is that the local costmap appears in RViz2, but the global costmap does not.

   To recover:

   1. In the Navigation panel in RViz2, click **Startup**.
   2. Click **Reset**.
   3. Set the robot's initial pose again.

   Once the global costmap appears, the robot is ready to accept navigation goals.

</details>

#### Learn More <a href="#troubleshooting-and-debugging" id="troubleshooting-and-debugging"></a>

To learn more information about ROS 2 Nav2 software, check out the official [Nav2 Getting Started guide](https://docs.nav2.org/getting_started/index.html) to learn the concepts in simulation. For more advanced topics and tuning guides for navigation on Stretch, you can also explore the tutorials in [Navigation University](/stretch4_docs/working-with-stretch/nav_u). In the next section, we'll learn how to launch the Web Teleoperation demo to drive Stretch with a user-friendly interface from a remote PC or mobile phone.


# Demo - Web Teleoperation

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

A common use case for Stretch is to operate the robot remotely, either on a local network or across the Internet. Hello Robot's Web Teleop software makes this possible, allowing an operator to control the robot while streaming the view through any of the robot's cameras. In the following demo, you will learn how to launch and use the Web Teleop demo that comes with Stretch.

### Launching the Demo

On the robot, Web Teleop can be launched in two ways.

{% stepper %}
{% step %}

### Command Line

Open a Terminal window and type the following to navigate to the correct folder:

```
colcon_cd stretch4_web_teleop
```

Then to launch the demo:

```
./launch_interface.sh
```

{% endstep %}

{% step %}

### Stretch Tray

Click the Hello Robot icon at the top right of the screen, then select "Launch Web Teleop" from the dropdown menu. A Terminal window will open and launch the demo.
{% endstep %}
{% endstepper %}

When the demo launches, the URL for connecting to the robot will be displayed at the bottom of the window (example: <https://192.0.2.1/operator>). Record this for use in the next step.

### Connecting over a local network

On a mobile phone or PC that is connected to the same Wi-Fi network as Stretch, open a web browser (Firefox recommended for best compatibility) and type in the URL as it appeared in the robot's Terminal window. This will connect you to the robot.

{% hint style="info" %}
You might receive an 'invalid certificate' error that asks you if you want to proceed with the connection - this is expected, and it is OK to proceed.
{% endhint %}

After a few seconds, you should see the Web Teleop interface display in your browser:<br>

\#Insert web teleop default image

{% hint style="info" %}
Add hint about homing the robot if unhomed?
{% endhint %}

Using this interface, you can directly control all of the robot's joints, switch between camera views, change speed settings, .

\#Labeled diagram of interface options

\#Insert video screengrab of \~ 1 minute of web teleop

### Advanced capabilities - autonomous navigation

{% hint style="info" icon="book-open" %}
Documentation Coming Soon
{% endhint %}

### Advanced capabilities - click-to-pregrasp

{% hint style="info" icon="book-open" %}
Documentation Coming Soon
{% endhint %}

### Connecting remotely over the Internet

It is possible to use Web Teleop entirely remotely over an internet connection, using a service like ngrok for secure tunneling. Instructions and tips can be found in the [stretch4\_web\_teleop](https://docs.hello-robot.com/stretch-4-web-teleop/) repository.


# Writing Code for Stretch

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

This tutorial introduces the two primary ways to develop software with Stretch SE4 — Python and ROS 2 — and walks you through writing your first Python programs using the Stretch 4 Body API. By the end, you will be able to command every joint on the robot, read sensor status, control the omnibase holonomically, and use the client/server architecture.

***

### Background

Stretch 4 supports two approaches to software development:

**Python (Stretch 4 Body)** is the low-level direct interface to the robot hardware. It gives you fine-grained control over every joint and sensor, and is the fastest way to get started. The `stretch4_body` package is pre-installed on every Stretch SE4.

**ROS 2 (Robot Operating System 2)** is a robotics middleware framework providing a collection of tools, libraries, and conventions for building robot applications. Stretch SE4 ships with ROS 2 Jazzy and a full driver stack. It is well suited for navigation, SLAM, MoveIt, and multi-node architectures.

You can learn more about when to use each approach in the Developing with Stretch guide. This tutorial focuses on the Python API. ROS 2 examples are covered in the Demos section.

***

### Software Architecture

All application code on Stretch SE4 uses the `RobotClient` class to communicate with a background `stretch_body_server` process running a 100 Hz control loop on the NUC.

The 100 Hz control loop follows this sequence every tick:

1. Pulls status from all hardware devices
2. Updates safety sentries (watchdogs)
3. Ingests commands from the Robot Client
4. Runs active controllers and behaviors
5. Computes safe motion limits
6. Pushes safe commands to motor controllers

Your code connects via `RobotClient`, which queues commands that the server loop ingests on the next tick. This means **motion is asynchronous by default** — your code keeps running while the robot moves. Use `push_command()` to flush the command queue and `wait_on_motion_finish()` to block until motion is done.

***

### Prerequisites

Before running any code, the robot must be set up and ready.

**1. Make sure the body server is running.**

The body server is launched automatically at startup. Verify it is active:

```bash
stretch_body_server --status
```

If it is not running, start it:

```bash
stretch_body_server --daemon
```

**2. Confirm the system is healthy.**

```bash
stretch_system_check
```

**3. Home the robot (required once per power cycle).**

```bash
stretch_robot_home
```

The robot will beep when homing is complete. Joints will not accept motion commands until they are homed. Once homed, the motors remember their homed state as long as the robot remains powered on.

***

### Your First Program with RobotClient

Open a terminal and launch iPython, an interactive Python console where each line runs immediately:

```bash
ipython3
```

Import and start the client:

```python
from stretch4_body.robot.robot_client import RobotClient

robot = RobotClient()
robot.startup()
```

`startup()` connects to the running body server. If successful, you now have full access to all robot subsystems. It is a good idea to use `if not robot.startup(): raise Exception("Could not connect to the robot")` to check if `startup()` has failed.

Stow the robot to its compact travel pose:

```python
robot.stow()
```

This is a blocking call — it returns only when the robot is fully stowed.

Check if the robot is homed:

```python
is_homed = robot.is_homed()
print("Robot is homed?", is_homed)
```

When you are done with a session, it is recommended to call:

```python
robot.stop()
```

This cleanly disconnects from the server. `stop()` does not stop the robot motion. It stops the client's connection to the server.

#### Using RobotClient as a Context Manager

For scripts, the cleanest pattern is to use `RobotClient` as a context manager. This ensures `startup()` and `stop()` are called for you:

```python
from stretch4_body.robot.robot_client import RobotClient

with RobotClient() as robot:
    if robot is None:
        raise Exception("Failed to connect to the robot server")
        
    robot.stow()
    # ... your code ...
```

***

### Moving Individual Joints

#### Arm

The arm telescopes horizontally, extending from 0 m to 0.52 m (21.6 inches) beyond the base. Position is in meters (0.0 = fully retracted, 0.52 = fully extended).

Move the arm to an absolute position:

```python
robot.arm.move_to(0.25) # in meters
robot.push_command()
```

Move the arm by a relative amount:

```python
robot.arm.move_by(0.05)
robot.push_command()
```

Move the arm back:

```python
robot.arm.move_by(-0.05)
robot.push_command()
```

> Note: if the robot is not homed, `move_by()` and `move_to()` commands will return False and the robot will not move.

#### Lift

The lift moves vertically from 0 m to 1.2 m (47 inches).\
Position is in meters (0.0 = lift down near the base, 1.2 = lift up near the head)

Move the lift to mid height:

```python
robot.lift.move_to(0.6) # 0.6 is in meters, relative to the homed 0 position near the base.
robot.push_command()
```

Move the lift up a little:

```python
robot.lift.move_by(0.1)
robot.push_command()
```

#### Commanding Multiple Joints Simultaneously

You can queue commands to several joints before pushing. They will execute simultaneously:

```python
robot.lift.move_to(0.7)
robot.arm.move_to(0.3)
robot.push_command()
```

{% hint style="info" %}
If you queue two motion commands for the **same joint** before pushing, only the last one executes. Earlier commands are overwritten.
{% endhint %}

#### Velocity and Acceleration Limits

Every motion command accepts optional velocity and acceleration limits (in m/s and m/s² respectively):

```python
robot.arm.move_to(0.2, v_m=0.05, a_m=0.1)
robot.push_command()
```

If not specified, the robot uses its default motion profile.

***

### The Omnibase (Holonomic Base)

Stretch SE4 has a triangular holonomic omnibase — three holonomic closed-loop stepper motors each driving one omnidirectional wheel. Unlike a differential drive, the omnibase can move **forward, sideways, diagonally, and rotate in place**.

Wheel numbering increases counter-clockwise, with wheel 0 to the left of the forward direction. This is consistent with the ROS convention where X+ is forward and Y+ is left.

The base is accessed as `robot.base` or `robot.omnibase`.

#### Translate by a Relative Amount

Move the base 0.3 m forward (X):

```python
robot.base.translate_by(x_m=0.3, y_m=0.0)
robot.push_command()
```

Move sideways to the left (positive Y):

```python
robot.base.translate_by(x_m=0.0, y_m=0.15)
robot.push_command()
```

#### Rotate in Place

Rotate counter-clockwise by 90 degrees (π/2 radians):

```python
import math
robot.base.rotate_by(w_r=math.pi / 2)
robot.push_command()
```

#### Set Continuous Velocity

Set a continuous velocity (useful for teleoperation or reactive control). Units: m/s for linear, rad/s for rotation:

```python
# Drive forward at 0.2 m/s while rotating at 0.1 rad/s
robot.base.set_velocity(vx_m=0.2, vy_m=0.0, w_r=0.1)
robot.push_command()

# Stop
robot.base.set_velocity(vx_m=0.0, vy_m=0.0, w_r=0.0)
robot.push_command()
```

#### Hard Stop

Immediately stop all base motion. Note this may cause jerky motion if the robot is moving at high velocity.

```python
robot.base.hard_stop()
robot.push_command()
```

***

### The Dexterous Wrist and Gripper

Stretch 4 has a 3-DOF dexterous wrist (yaw, pitch, roll) driven by Feetech servo motors over a TTL serial bus, plus a compliant spring-mechanism gripper or a parallel-jaw gripper.

All wrist and gripper joints are accessed through `robot.end_of_arm`. Positions are in **radians** unless noted.

| Joint              | Range                                         |
| ------------------ | --------------------------------------------- |
| `wrist_yaw`        | ±170 deg (340 deg total)                      |
| `wrist_pitch`      | \~100 deg                                     |
| `wrist_roll`       | ±170 deg (340 deg total)                      |
| `stretch_gripper`  | -100 to +100 (percent, where positive = open) |
| `parallel_gripper` | 0 to 116.5 deg                                |

#### Move Wrist Joints

Move wrist yaw to 0.5 rad:

```python
robot.end_of_arm.move_to('wrist_yaw', 0.5)
robot.push_command()
```

Move wrist pitch:

```python
robot.end_of_arm.move_to('wrist_pitch', -0.5)
robot.push_command()
```

Move wrist roll by a relative amount:

```python
robot.end_of_arm.move_by('wrist_roll', 1.0)
robot.push_command()

robot.end_of_arm.move_by('wrist_roll', -1.0)
robot.push_command()
```

#### Gripper

Open the gripper (positive values = open):

```python
robot.end_of_arm.move_to('stretch_gripper', 100)
robot.push_command()
```

Close the gripper (negative values = closed):

```python
robot.end_of_arm.move_to('stretch_gripper', -100)
robot.push_command()
```

Move to neutral (zero):

```python
robot.end_of_arm.move_to('stretch_gripper', 0)
robot.push_command()
```

#### Disable/Enable Torque on a Wrist Joint

Making a joint backdrivable (torque off):

```python
robot.end_of_arm.disable_torque('wrist_yaw')
robot.push_command()
```

Re-enabling:

```python
robot.end_of_arm.enable_torque('wrist_yaw')
robot.push_command()
```

#### Homing the Wrist

If a wrist joint loses its homed state (e.g., after a Feetech motor error and reboot), home it:

```bash
# via cli
stretch_dex_wrist_home --wrist_yaw
```

```python
# via python
robot.routines.routine_wrist_joint_home('wrist_yaw')
```

Or home the entire end of arm at once:

```bash
# via cli
stretch_dex_wrist_home
```

```python
# via python
robot.routines.routine_end_of_arm_home()
```

{% hint style="info" %}
**NOTE**: **After a Feetech error:** If a wrist or gripper motor enters an error state, clear it with `stretch_feetech_reboot` from a terminal, then re-home the wrist with `stretch_dex_wrist_home`.
{% endhint %}

{% hint style="info" %}
**Note:** If you wish to change your gripper or tool, use `stretch_configure_tool` from the command line.
{% endhint %}

***

### Reading Robot Status

#### Print Full Robot Status

Print all subsystem statuses in a human-readable format:

```python
Note:robot.pretty_print()
```

This outputs a lot of data. To read individual subsystem statuses, access `robot.status` directly:

```python
print(robot.status['lift'])
print(robot.status['arm'])
print(robot.status['omnibase'])
print(robot.status['power_periph'])
print(robot.status['end_of_arm'])
```

#### Key Status Fields

**Lift/Arm:**

```python
robot.status['lift']['pos']                     # Current position (m)
robot.status['lift']['vel']                     # Current velocity (m/s)
robot.status['arm']['pos']                      # Current position (m)
robot.status['arm']['motor']['pos_calibrated']  # True if homed
```

**Omnibase:**

```python
robot.status['omnibase']['x']      # X position estimate (m)
robot.status['omnibase']['y']      # Y position estimate (m)
robot.status['omnibase']['theta']  # Heading (rad)
robot.status['omnibase']['x_vel']  # X velocity (m/s)
robot.status['omnibase']['y_vel']  # Y velocity (m/s)
```

**Power Periph (IMU, battery, runstop):**

```python
robot.status['power_periph']['voltage_cpu']       # NUC supply voltage (V)
robot.status['power_periph']['current_cpu']       # NUC current (A)
robot.status['power_periph']['battery_soc']       # Battery state of charge (%)
robot.status['power_periph']['runstop_event']     # True if runstop active
robot.status['power_periph']['imu']['ax']         # IMU accelerometer X (m/s²)
robot.status['power_periph']['imu']['gravity_tilt']  # Gravity tilt angle (rad)
robot.status['power_periph']['over_tilt_type']    # Tilt direction if tilted (e.g. 'Left Tilt')
```

**Wrist Joint (via end\_of\_arm):**

```python
robot.status['end_of_arm']['wrist_yaw']['pos']             # Position (rad)
robot.status['end_of_arm']['wrist_yaw']['effort']          # Effort (%)
robot.status['end_of_arm']['wrist_yaw']['temp']            # Motor temperature
robot.status['end_of_arm']['wrist_yaw']['overtemp_error']  # Overtemp flag
robot.status['end_of_arm']['wrist_yaw']['pos_calibrated']  # True if homed
```

***

### Waiting for Motion to Complete

Motion commands are asynchronous. Use these methods to synchronize:

#### Wait for Specific Subsystems to Finish Moving

```python
robot.arm.move_to(0.4)
robot.push_command()
robot.wait_on_motion_finish(['arm'], timeout=10.0)
print('Arm done moving')
```

Wait for multiple joints at once:

```python
robot.lift.move_to(0.8)
robot.arm.move_to(0.2)
robot.push_command()
robot.wait_on_motion_finish(['lift', 'arm'], timeout=15.0)
```

#### Wait for All Motion to Complete

To wait for the arm, lift, base, and end of arm to all finish moving:

```python
robot.arm.move_to(0.3)
robot.push_command()
robot.wait_command(timeout=10.0)
```

***

### Guarded Contact Sensitivity

Stretch SE4's lift, arm, and omnibase joints have a **Guarded Contact** safety system. This uses current sensing to detect when actuator effort exceeds a configurable threshold. When triggered, the joint halts until a new command is received.

This protects people and objects from excessive force. By default, contacts are set to a moderate sensitivity. You can change this per use case:

```python
# See available modes
print(robot.get_guarded_contact_modes())

# Set a robot-wide mode
robot.set_guarded_contact_sensitivity('high_sensitivity_manipulation')

# Or set per-joint
robot.arm.set_guarded_contact_sensitivity('high_sensitivity_manipulation')
robot.base.set_guarded_contact_sensitivity('default')
```

You can also pass per-move contact thresholds directly:

```python
# Move arm with high contact sensitivity in the positive direction
robot.arm.move_to(0.3, contact_sensitivity_pos=0.9)
robot.push_command()
```

***

### Power Periph: Beeps, Eyes, and Fan

The Power Periph board controls the speaker buzzer, LED eye animations, RGB lightbar, and chassis fans.

#### Trigger a Beep

```python
robot.power_periph.trigger_beep()
robot.push_command()
```

#### Control LED Eyes

Set animations on the left and right eye rings. Animation indices are defined in the firmware (0 = off, see `stretch_eye_animations` for the full set):

```python
# Set left eye animation index 3, right eye animation index 3
robot.power_periph.set_eye_animation(left_idx=3, right_idx=3)
robot.push_command()
```

#### Toggle the Runstop Programmatically

```python
robot.power_periph.trigger_runstop()
robot.push_command()

# Later, clear it
robot.power_periph.clear_runstop()
robot.push_command()
```

***

### Writing a Complete Script

Here is a complete standalone Python script that demonstrates a sequence of moves on Stretch SE4. Save this as `my_first_stretch_script.py`:

```python
#!/usr/bin/env python3
"""
Simple Stretch SE4 demo script.
Run with: python3 my_first_stretch_script.py

Prerequisites:
  - stretch_body_server must be running
  - Robot must be homed
"""

import time
import math
from stretch4_body.robot.robot_client import RobotClient

def main():
    with RobotClient() as robot:
    
        if robot is None:
            raise Exception('Could not connect to the robot server. \
                Please restart it using `stretch_body_server --restart`')

        if not robot.is_homed():
            print('Robot is not homed. Homing now...')
            robot.home()

        print('Stowing robot...')
        robot.stow()

        # --- Lift ---
        print('Raising lift to 0.6 m...')
        robot.lift.move_to(0.6)
        robot.push_command()
        robot.wait_on_motion_finish(['lift'], timeout=15.0)

        # --- Arm ---
        print('Extending arm to 0.3 m...')
        robot.arm.move_to(0.3)
        robot.push_command()
        robot.wait_on_motion_finish(['arm'], timeout=10.0)

        print('Retracting arm to 0.1 m...')
        robot.arm.move_to(0.1)
        robot.push_command()
        robot.wait_on_motion_finish(['arm'], timeout=10.0)

        # --- Simultaneous move ---
        print('Moving lift and arm together...')
        robot.lift.move_to(0.8)
        robot.arm.move_to(0.1)
        robot.push_command()
        robot.wait_on_motion_finish(['lift', 'arm'], timeout=15.0)

        # --- Wrist ---
        print('Moving wrist yaw...')
        robot.end_of_arm.move_to('wrist_yaw', 0.5)
        robot.push_command()

        print('Opening gripper...')
        robot.end_of_arm.move_to('stretch_gripper', 100)
        # comment the line above and uncomment the line below if using a parallel jaw gripper
        # robot.end_of_arm.move_to('parallel_gripper', 100)
        robot.push_command()
        time.sleep(1.0)

        print('Closing gripper...')
        robot.end_of_arm.move_to('stretch_gripper', -50)
        # comment the line above and uncomment the line below if using a parallel jaw gripper
        # bot.end_of_arm.move_to('parallel_gripper', -50)
        robot.push_command()
        time.sleep(1.0)

        # --- Base ---
        print('Translating base forward 0.2 m...')
        robot.base.translate_by(x_m=0.2, y_m=0.0)
        robot.push_command()
        robot.wait_on_motion_finish(['omnibase'], timeout=10.0)

        print('Rotating base 90 degrees...')
        robot.base.rotate_by(w_r=math.pi / 2)
        robot.push_command()
        robot.wait_on_motion_finish(['omnibase'], timeout=10.0)

        # --- Beep to signal completion ---
        robot.power_periph.trigger_beep()
        robot.push_command()

        print('Demo complete. Stowing...')
        robot.stow()

if __name__ == '__main__':
    main()
```

Run it with:

```bash
python3 my_first_stretch_script.py
```

{% hint style="danger" %}
CAUTION: This script will move your robot! Please place Stretch 4 in a 5ft x 5ft space with no obstructions before running this script.
{% endhint %}

***

### ROS 2 with Stretch SE4

Stretch SE4 ships with ROS 2 Jazzy. The ROS 2 driver communicates with `stretch_body_server` via the same `RobotClient` interface, making Python and ROS 2 development fully interoperable.

Key ROS 2 topics published by the driver include:

* `/joint_states` — positions and velocities for all joints
* `/wheel_odom` — odometry from the omnibase
* `/scan_filtered` — point clouds from the Hesai QT128 lidars
* `/color/image_raw` — RGB images from the head cameras

Key ROS 2 services include:

* `/home` — trigger full robot homing
* `/stow` — stow the robot

The Jetson add-on (when present) subscribes to camera topics and returns inference results via Zenoh topic bridging. This enables GPU-accelerated perception (e.g., YOLO, pose estimation) without burdening the NUC.

For full ROS 2 examples, checkout [stretch4\_ros2](https://docs.hello-robot.com/stretch4-ros2-repo/)

***

### Learn More

* [**Robot Overview**](broken://pages/09jDRgcAuXdrN2BcRfuH) — complete reference for all hardware, sensors, and CLI tools on Stretch SE4
* [**Stretch Safety Guide**](https://docs.hello-robot.com/stretch-4-safety-guide/) — essential safety information before operating the robot

***

### Troubleshooting

#### Feetech Motor Errors

Wrist and gripper Feetech motors can enter an error state after over-force or over-temperature events. When this happens:

* The motor becomes limp and backdrivable
* The LED on the motor body blinks red
* Motion commands are ignored

Clear the error without powering down:

```bash
stretch_feetech_reboot
```

After rebooting, re-home the wrist and gripper:

```bash
stretch_dex_wrist_home
stretch_gripper_home
```

You can also check Feetech motor health at any time:

```bash
stretch_feetech_monitor
```

### Next Steps

In the next tutorial, **Demo #1 - Mapping & Navigation**, we will look at two ways that Stretch can navigate within a map.


# Additional Resources

This documentation is currently in beta. Please proceed carefully, and if you run into issues or need additional information, reach out to Hello Robot directly for support.

While we hope the documentation found here gets you successfully started developing with your robot, there are a number of other resources worth noting that provide additional information and ways to interact with Stretch and the larger Hello Robot community.

### :speech\_balloon: Forum <a href="#forum" id="forum"></a>

Stretch has a diverse and vibrant user community, and one of our goals at Hello Robot is to help our customers connect, collaborate, and share with one another. To this end, we have created a [public Hello Robot Forum](https://forum.hello-robot.com/) where you can ask questions, post about your work, request support, or search an archive of resolved issues. Our engineers regularly read and respond to threads here.

### :computer: GitHub <a href="#github" id="github"></a>

Nearly all the code we write for Stretch is open-source and freely available on our [Hello Robot GitHub](https://github.com/hello-robot). Feel free to browse through the repos or dig deeper into the code. For information on how to contribute to Stretch software, see the \[Contribution Guide].

### :people\_holding\_hands: Community Updates <a href="#community-updates" id="community-updates"></a>

We love to help spread the work about the great work our community is constantly publishing! Every month, we publish a \[Stretch Community Update] with some highlights of the recent research done by Stretch customers on our website, mailing list and social media. When you have work that is ready to make public, \[let us know] and we'll make sure it's seen by our community!

### :wrench: Get Some Support

If you've run into a hardware problem, need urgent assistance, or just can't find the answer to your questions and want to chat directly with a Hello Robot engineer, please feel free to contact us directly at support \[at] hello-robot.com. We love talking to Stretch users, and your feedback helps us improve the product for all our users.


# General Use

NA


# Keeping Stretch Charged

Stretch includes a removable battery (LiFePO4 25.6V 20AH) that provides 6-8 hours of runtime given a light level of activity and CPU load. It takes about 3-4 hours to fully charge a battery from a low state of charge.

### Monitoring State of Charge (SoC)

The State of Charge (SoC) indicates the remaining energy available in a battery relative to its maximum capacity, expressed as a percentage from 0% to 100%. It serves as a fuel gauge for Stretch, letting you know how much operating time you have left before needing a recharge.

#### LED Indicators

The head includes 8 RGB LEDs. In addition to indicating the battery SoC, they will slowly strobe if the robot is plugged into its a power source, and flash if the runstop is enabled.

<table><thead><tr><th width="154.046875">SoC (%)</th><th align="center">LEDs</th></tr></thead><tbody><tr><td>89 – 100</td><td align="center">⚪⚪⚪⚪⚪⚪⚪⚪</td></tr><tr><td>76 – 88</td><td align="center">○⚪⚪⚪⚪⚪⚪⚪</td></tr><tr><td>63 – 75</td><td align="center">○○⚪⚪⚪⚪⚪⚪</td></tr><tr><td>51 – 62</td><td align="center">○○○⚪⚪⚪⚪⚪</td></tr><tr><td>39 – 50</td><td align="center">○○○○⚪⚪⚪⚪</td></tr><tr><td>26 – 38</td><td align="center">○○○○○⚪⚪⚪</td></tr><tr><td>21 – 25</td><td align="center">○○○○○○⚪⚪</td></tr><tr><td>11 – 20</td><td align="center">○○○○○○🟡🟡</td></tr><tr><td>0 – 10</td><td align="center">○○○○○○○🔴</td></tr></tbody></table>

#### Command Line Tool

You can check the battery status with:

```
~$stretch_battery_check
For use with S T R E T C H (R) from Hello Robot Inc.
---------------------------------------------------------------------

######## Stretch Battery Information ##########
Battery Remaining Capacity: 61%
Battery Voltage: 26.99 V
Battery Health Percentage: 100%
Battery Current Supplied: 2.30 A
######## Adapter Information ##########
Adapter voltage: present
Adapter fault: not present
Adapter (physical switch): connected
######## Charger Information ##########
Battery Charger: Charging
Battery Charger Current: 4.36 A

```

#### System Tray

The Stretch has a System Tray in the Ubuntu Desktop. You can quickly check the SoC using the tray drop-down.

<figure><img src="/files/zlXIi2CQ1ZEIBcc0teIp" alt=""><figcaption></figcaption></figure>

### Plugging In

It is recommended to leave the robot plugged in and charging when not in use, or when the battery is below approximately 30% SoC.

When the robot is first plugged it will beep and the LED Battery Indicators will slowly strobe on-and-off.

<figure><img src="/files/4oZFuovCKZ2IYKR4rzWM" alt=""><figcaption></figcaption></figure>

#### Power Adapter

<figure><img src="/files/n7x42pfrNol5wkOXFfMi" alt="" width="375"><figcaption></figcaption></figure>

The provided power adapter provides 36V / 8A through a 5.1 mm barrel jack connector.

If you do not hear the beep when plugging in, check that the adapter cords are correctly plugged in.

#### Docking Station

The docking station should be placed up-against a wall and plugged in. Keep the station free of obstacles, cords, rugs, and other items that may impeded docking.

When the robot drives (or is pushed) onto the dock it will engage the contacts of the docking station. When docking, listen for the beep indicating a successful dock.

<figure><img src="/files/CKJSwabPrA2OjcHAV8QZ" alt="" width="375"><figcaption></figcaption></figure>

### Best Practices

#### Battery Swap

The battery can be easily removed to to swamp in charged battery, or to enable easier transport of the robot. Detailed instructions can be found in the [Hardware Guide](#battery-swap)

#### Battery Health

To get the longest life out of your robot’s LiFePO4 battery, avoid draining the battery completely to 0%; instead, recharge it when it drops to below 30%. When charging, use only Hello Robot approved charger.

If the robot will be stored or left unused for an extended period, do not leave the battery fully empty or completely full. Instead, charge or discharge it to about 50% to 60% capacity. Check the battery every 3 to 6 months during long-term storage and give it a top-off charge to prevent it from slipping into a deep-discharge state, which can permanently reduce its capacity and lifespan.

#### Battery Safety

Although LiFePO4 chemistry is inherently safe, stable, and highly resistant to catching fire or exploding, basic precautions are still necessary. The battery's heavy steel enclosure provides excellent physical protection, but never attempt to open, drill into, or modify the box, as damaging the internal cells can still release toxic gasses or cause a short circuit. If the enclosure ever becomes excessively hot, emits an odor, or appears swollen, immediately power down the robot and move it to a safe, isolated area.

#### Battery Shipping

LiFePO4 batteries are classified as Class 9 Dangerous Goods — they must be shipped in compliance with strict transportation regulations (such as IATA, DOT, or IMDG guidelines). Before packaging, discharge the battery to between 20% and 30% of its total capacity to minimize potential energy during transit. Wrap the external connectors securely with electrical tape and pack into a robust, UN-approved outer container with ample non-conductive cushioning to keep it from shifting. Finally, ensure the exterior of the package is clearly labeled with the required Lithium Battery handling marks and documentation before handing it over to a certified carrier.


# Unboxing Stretch

## Step 1

<div align="center"><img src="/files/vh9bZ5qa1FuB4VzSOHap" alt="base" width="400"></div>

## Step 2

<div align="center"><img src="/files/sZ9vvjmFpvOG0kCrrdMh" alt="base" width="400"></div>

## Step 3

<div align="center"><img src="/files/Pj9vt9FNtMeiZgqiYcjG" alt="base" width="400"></div>

## Step 4

<div align="center"><img src="/files/120Esdr6KPF9s210Uvk0" alt="base" width="400"></div>

## Step 5

<div align="center"><img src="/files/4Zvcb413TDNZUqVDIYOv" alt="base" width="400"></div>

## Step 6

<div align="center"><img src="/files/xOQmo63z89aZPB6Cp5TB" alt="base" width="400"></div>

## Step 7

<div align="center"><img src="/files/bhUzb5uQORFzBkk28vz3" alt="base" width="400"></div>

## Step 8

<div align="center"><img src="/files/HApr3nzheszC025GgzT3" alt="base" width="400"></div>


# Connecting to Stretch

Stretch can be accessed in two ways:

* Tethered Setup: Directly connect a monitor, keyboard, and mouse to the robot
* Untethered Setup: Access the robot remotely from another computer

### Tethered Setup <a href="#tethered_setup" id="tethered_setup"></a>

Stretch 4 includes an onboard computer located within the robot’s trunk, commonly referred to as the Intel NUC. This system runs Ubuntu 24.04 and serves as the primary environment for development, testing, and visualization.

A tethered setup allows you to interact directly with the robot by connecting peripherals such as a monitor, keyboard, and mouse to the onboard computer.

#### Hardware Access and Ports <a href="#hardware_access_and_ports" id="hardware_access_and_ports"></a>

<figure><img src="/files/pLDpTyG3t2x6pKJmRMT8" alt="" width="800"><figcaption></figcaption></figure>

The main connectivity panel is located in the trunk, alongside the power controls. It provides:

* 3 × USB-A 2.0 ports
* HDMI port
* Ethernet port
* Power On/Sleep/Off button
* Charging port

Additional USB ports are distributed across the robot and are internally connected to the same onboard computer:

* Wrist: USB-A 2.0
* Head: USB-A 3.2 and USB-C 3.2

These ports allow you to connect peripherals and sensors at convenient locations depending on your setup.

#### Setting Up a Wired Connection <a href="#setting_up_a_wired_connection" id="setting_up_a_wired_connection"></a>

To begin using Stretch in tethered mode:

* Connect a monitor to the HDMI port in the trunk
* Connect a USB keyboard and mouse (wired or via wireless dongle)
* Power on the robot using the trunk’s On/Off button

After booting, the onboard computer will display the Ubuntu desktop environment on the connected monitor.

The default user login credentials came in the box with the robot. By default, the robot is not configured to ask for your password on boot, but may ask for it later if the NUC goes to sleep.

### Untethered Setup <a href="#untethered_setup" id="untethered_setup"></a>

> Once your robot is on the network, you can access it remotely from your computer. This is the preferred way to work with Stretch for most users. The options below range from full desktop access to lightweight development workflows.

Remote Access Options:

1. [RustDesk](#rustdesk) (Required a Dummy HDMI Dongle, Ubuntu GUI access)
2. [RDP](#rdp-with-remote-login-) (Does not require a Dummy HDMI Dongle, Ubuntu GUI access)
3. [SSH / VScode Remote](#ssh--vscode-remote-) (Does not require a Dummy Dongle, no Ubuntu GUI access)

#### Security Considerations

An untethered setup usually requires connecting your robot to the local LAN or internet, and setting up software and credentials for remote access. This means that anyone with access to the network can access the Ubuntu Operating System and your robot remotely.

**It is important to follow your institution or organization's security best practices for setting up remote access and Ubuntu User Account Credentials.**

It is highly recommended to create a new Ubuntu User Account when using remote access. This maximizes security and helps users share a robot. Instructions for adding a new user can be found here: <https://docs.hello-robot.com/stretch4\\_install/docs/add\\_new\\_user#manually-add-a-new-user>

It is also recommended, but optional, to disable auto-login. Go to Settings -> Users -> hello-robot (or your default user) -> Disable auto-login. While having the default user logged in does not get in the way of other users connecting using RDP, it is taking up memory running in the background. Also, with auto-login enabled, the keychain is locked, which limits remote-access to many features.

**Tunneling services (such as Tailscale)**

If you want to access your robot from a non-LAN network, it is recommended to set up a secure tunnel or use a service such as Tailscale, which connects your devices and the robot without additional networking configuration. You can download and log-in to Tailscale on both the robot and your devices by following their documentation: <https://tailscale.com/docs/how-to/quickstart>

#### RustDesk <a href="#rustdesk" id="rustdesk"></a>

RustDesk is a third party software that enables you to access the robot's Ubuntu desktop remotely. It provides a graphical interface, making it ideal for working within the Ubuntu environment, doing development on the robot, and using tools such as Rerun and RViz without additional network configuration.

If you plan to access Stretch through RustDesk without a physical monitor connected, you will need to purchase a dummy HDMI dongle to plug into the HDMI port.

> Note: The Dummy HDMI dongle tricks Ubuntu's Gnome Desktop Manager into making the display visible. Without it, you would see a blank screen when you connect via RustDesk or native Remote Desktop.

You do not need the dongle during RustDesk installation. For setup, first connect the robot to your host computer with a standard HDMI cable. The dummy HDMI dongle is only required afterward, once RustDesk is installed.

According to RustDesk's documentation, it is recommended to host your own Relay server and [avoid using the public servers for sensitive work](https://github.com/rustdesk/rustdesk/wiki/Login-required-for-public-server#rustdesk-is-designed-to-be-self-hosted-this-public-server-is-provided-for-demonstration-and-testing-only-please-do-not-use-it-for-production-or-sensitive-work).

**Install Steps**

1. [Download and install RustDesk](https://rustdesk.com/) on your host machine.
2. Make sure an HDMI cable is connected between your host computer and the robot. Open RustDesk on the robot, either from the applications menu or by running:

   ```
   rustdesk
   ```

<figure><img src="/files/oXNjXYakRJZFb3EgkzVL" alt="" width="800"><figcaption></figcaption></figure>

3. Note the `ID` shown in RustDesk, you will need it later.
4. Set a `Permanent password`:
   1. Click the edit icon:

      <figure><img src="/files/7ZTwHH99PrYqMXRcuyHY" alt="" width="800"><figcaption></figcaption></figure>
   2. Click `Security` on the left-hand side, then select `Unlock security settings`:

      <figure><img src="/files/jWa0LCESS6X1c3caP6R0" alt=""><figcaption></figcaption></figure>
   3. Click `Set permanent password` and set a strong password. It is strongly recommended to also set up Two-Factor Authentication from the same menu. **It is important to follow your institution or organization's security best practices for setting up remote access and Ubuntu User Account Credentials.**:

      <figure><img src="/files/EL4sDWzYDifhueQHeJXM" alt="" width="800"><figcaption></figcaption></figure>
5. Close RustDesk on the robot. Disconnect the HDMI cable, connect the dummy HDMI dongle to the robot, and open RustDesk on your host machine. Enter the robot’s `ID` and permanent password. You now have a remote connection to the robot using RustDesk:

   <figure><img src="/files/hMdkUIe1xPnzfVv1kHry" alt="" width="800"><figcaption></figcaption></figure>

#### RDP with Remote Login <a href="#rdp" id="rdp"></a>

Ubuntu 24.04 ships with a built-in Remote Login feature that allows you to access your Ubuntu User Account remotely using Remote Desktop Protocol (RDP). Multiple users can log into the robot at the same time for development, if necessary.

The official Ubuntu docs for setting up Remote Login are here: <https://help.ubuntu.com/stable/ubuntu-help/remote-login.html.ro>

You do not need a Dummy Dongle to use Remote Login. Note that Remote Login is different from Remote Desktop, which shares the same menu in Ubuntu's Settings. Remote Desktop does require a Dummy Dongle and requires you to remain logged in. Remote Login allows you to access the Ubuntu login screen, and then you can log in to any user account on the system.

**Robot Setup**

The following instructions will walk you through setting up Remote Login.

Please follow these steps while you are on the hello-robot or default user account.

1. Log in to your robot and go to Settings -> System -> Remote Desktop

<img src="https://github.com/user-attachments/assets/e49c1c02-54d2-41e7-943d-05206043d081" alt="image" width="800">

3. Skip the Desktop Sharing tab, and click the Remote Login tab.

   A. Click Unlock and type in your password. B. Toggle to Enable Remote Login C. At the bottom of the window, enter a username and strong password combination to allow you to remotely connect to this robot. It is a good idea to NOT use the password for your User Account here, because if the remote login password is compromised, you still have a layer of access security by having a different User Account password. **It is important to follow your institution or organization's security best practices for setting up remote access and Ubuntu User Account Credentials.**:

   <img src="https://github.com/user-attachments/assets/5309f515-99c1-4337-ae12-1cdbe45c6b0e" alt="image" width="800">
4. Retreive the ip of this robot using `hostname -I` or `tailscale ip` if you are using Tailscale.

**Client Setup on Linux**

1. Run `sudo apt install remmina`
2. Open Remmina and click the + icon, enter your robot's ip, username and password you configured in the previous step. Click Save and Connect to test your connection

<img src="https://github.com/user-attachments/assets/0c230de2-e6e6-461a-bb7a-0db7fe4df171" alt="image" width="800">

3. Click the Toggle Dynamic Resolution button on the sidebar to make the RDP window use your monitor’s resolution.

<img src="https://github.com/user-attachments/assets/d5414074-3536-471d-ae56-d5d1a1dfbef7" alt="image" width="800">

4. Login to your User Account to start using your robot.

**Remmina Audio Passthrough**

On the client, right click the Remmina connection -> Edit -> go to the Advanced tab -> Audio output mode. Choose Remote to play audio through the robot’s speakers. Local to play it using the client speakers. Then click Save.

<img src="https://github.com/user-attachments/assets/087d28b1-47a1-4b45-b6b0-a06e602732d9" alt="image" width="800">

**Client Setup on MacOS**

1. Get the Windows App, offered by Microsoft Corporation, from the [Apple Store](https://apps.apple.com/us/app/windows-app/id1295203466)
2. Create a new Computer connection and change the PC name to the IP Address of the robot. You can add a Friendly Name to help identify the robot. Under Credentials, you can click to “Add Credentials” and save your password so you do not have to enter it every time you connect.

> Note: if you encounter a blank screen error on MacOS, export your connection and change the line that says `use redirection server name:i:1`, and re-import your configuration, as suggested in: <https://askubuntu.com/a/1528263>

#### SSH + VSCode Remote <a href="#ssh__vs_code" id="ssh__vs_code"></a>

This method is focused on development and no remote GUI access. It is likely the easiest method to set up.

**Setting up VSCode Remote**

1. [Download and install Visual Studio Code](https://code.visualstudio.com/download) on your Host machine.
2. Inside VS Code:
   * Go to `Extensions` (left sidebar or `Ctrl+Shift+X`).
   * Search for `Remote - SSH` (by Microsoft).
   * Click `Install`.
3. Open SSH Configuration:
   * Press: `Ctrl + Shift + P`.
   * Type: `Remote-SSH: Open SSH Configuration File`.
   * Select: `~/.ssh/config`.
4. Add Your Stretch Robot:
   * Add a new entry like this:

     ```
     Host stretch
         HostName <ROBOT_IP>
         User hello-robot
     ```
   * `stretch` is the host alias used to initiate the connection. You may rename it if desired.
   * Replace `<ROBOT_IP>` with your robot's IP address. To find it, run the following on the robot (via a tethered terminal or RustDesk):

     ```bash
     ifconfig -a
     ```

     Look for the `wlan0` (or similar wireless) interface — the `inet` field is the robot's Wi-Fi IP address, which is typically in the `10.1.10.xxx` range.
   * The login user should remain `hello-robot`.
   * Save the file.
5. First Connection
   * You can start the connection in two ways:
     * Command Palette:
       * Press: `Ctrl + Shift + P`.
       * Type: `Remote-SSH: Connect to Host`.
     * UI shortcut:
       * Click the bottom-left corner of VS Code
       * Select `Open a Remote Window`
       * Then choose `Connect to Host…`
   * Select `stretch` or whatever host alias name you have set.
   * On first connection, you may see a prompt like: `"stretch" has fingerprint "…". Are you sure you want to continue?`. Select `Continue` to proceed.
   * Enter the password that came in the box with the robot.
   * Once connected, VS Code installs a small server on the robot automatically. After that, you will notice a new VS Code window, you are fully inside Stretch robot.


# Networking Stretch

Stretch 4 features an internal Ethernet network connecting the onboard Intel NUC, the NVIDIA Jetson Orin module (GPU expansion), and the twin Hesai Lidars. This page details the layout, static IP addresses, and configuration of this network.

## Internal Network Architecture

The robot’s onboard computer (Intel NUC), the Jetson Orin module, and the left and right Lidars communicate over a private, wired Ethernet subnet (`192.168.1.x/24`). A physical Ethernet switch inside the robot chassis bridges these devices.

### IP Configuration Table

All internal devices on the private Ethernet network are configured with static IP addresses:

| Device                     | IP Address      | Subnet Mask     | Description                                         |
| -------------------------- | --------------- | --------------- | --------------------------------------------------- |
| **NUC (Lidar interface)**  | `192.168.1.2`   | `255.255.255.0` | IP used by the NUC to receive data from both Lidars |
| **NUC (Jetson interface)** | `192.168.1.100` | `255.255.255.0` | IP used by the NUC to communicate with the Jetson   |
| **NVIDIA Jetson Orin**     | `192.168.1.101` | `255.255.255.0` | GPU expansion module (SSH user: `jetson1`)          |
| **Right Lidar (Hesai)**    | `192.168.1.201` | `255.255.255.0` | Right-side Hesai Lidar                              |
| **Left Lidar (Hesai)**     | `192.168.1.202` | `255.255.255.0` | Left-side Hesai Lidar                               |

> \[!IMPORTANT] The Intel NUC utilizes a **single network connection profile** configured with **two static IP addresses** (`192.168.1.2` and `192.168.1.100`) on its physical Ethernet interface. This allows a single physical port to communicate with both the Lidars and the Jetson Orin simultaneously.

> \[!WARNING] If you need to connect the robot NUC to the internet via a wired Ethernet cable, you must create and switch to a separate network profile (e.g., DHCP). Note that when this internet profile is active, **the internal Lidars and Jetson Orin will not be accessible**.
>
> To maintain connectivity to both the internet and the robot's internal devices simultaneously, it is recommended to connect the NUC to the internet using its built-in Wi-Fi interface.

<div align="center"><figure><img src="/files/0qt8Vcmx9CNzYxuwv2O2" alt="" width="300"><figcaption></figcaption></figure></div>

### Connecting an External Device via Ethernet

You can attach an external device (e.g., a development laptop or an additional sensor) to the robot's Ethernet port in the base and have it communicate on the same `192.168.1.x` subnet as the Jetson and Lidars.

#### On the External Device

1. Connect an Ethernet cable between your external device and the Ethernet port on the robot's trunk.
2. On the external device, configure a **static IP address** on the `192.168.1.x/24` subnet. Choose an address that does not conflict with existing devices (e.g., `192.168.1.50`):

   | Setting     | Value                                  |
   | ----------- | -------------------------------------- |
   | IP Address  | `192.168.1.50` (or any unused address) |
   | Subnet Mask | `255.255.255.0`                        |
   | Gateway     | Leave blank                            |

#### On the NUC

3. You need to add a static IP address to the NUC's existing wired connection profile so the NUC can communicate with your external device on the same subnet. First, identify the name of the active wired profile by running:

   ```bash
   nmcli connection show
   ```

   The active wired profile is typically named `Jetson and Hesai` or `Wired connection 1`. Note this name — you will use it in the commands below (referred to as `<your-wired-profile-name>`).

   Next, add a new static IP to that profile and restart the connection:

   ```bash
   # Add a third static IP to the existing wired profile
   sudo nmcli connection modify "<your-wired-profile-name>" +ipv4.addresses "192.168.1.51/24"

   # Restart the connection to apply changes
   sudo nmcli connection down "<your-wired-profile-name>"
   sudo nmcli connection up "<your-wired-profile-name>"
   ```

   Replace `192.168.1.51` with an appropriate IP for the NUC to use when communicating with your external device. This IP must be different from the device's IP (`192.168.1.50`) and must not conflict with any existing address in the [IP Configuration Table](#ip-configuration-table) above.

> \[!TIP] You can also add the static IP address using the graphical interface instead of the command line. See [Using the Network Manager GUI](#using-the-network-manager-gui) for instructions on opening the wired profile settings — simply add a new row in the **Addresses** table with your chosen IP and netmask.

#### Verify Connectivity

4. From the NUC, ping your external device:

   ```bash
   ping 192.168.1.50
   ```
5. From the external device, ping the NUC:

   ```bash
   ping 192.168.1.51
   ```

> \[!NOTE] This setup connects the external device to the robot's **internal** Ethernet switch, meaning it will also be able to reach the Jetson (`192.168.1.101`) and the Lidars (`192.168.1.201`, `192.168.1.202`) directly, provided its subnet mask is `255.255.255.0`.

> \[!WARNING] Adding addresses to the wired profile will persist across reboots. If you later remove the external device, you may want to clean up the extra address:
>
> ```bash
> sudo nmcli connection modify "<your-wired-profile-name>" -ipv4.addresses "192.168.1.51/24"
> sudo nmcli connection down "<your-wired-profile-name>"
> sudo nmcli connection up "<your-wired-profile-name>"
> ```

## External Network Communication

For step-by-step instructions on establishing remote connections to the robot NUC (via SSH, Remote Desktop/RDP, or RustDesk), please refer to the [Connecting to Stretch Guide](/stretch4_docs/working-with-stretch/general_use/connecting-to-stretch).

When networking Stretch with external systems, keep the following architectural points in mind:

### 1. Wi-Fi Connectivity

The recommended way to provide internet access to the robot NUC while maintaining connectivity to its internal components is via its built-in Wi-Fi interface. Connecting the NUC to a local office/lab network allows external developer PCs on the same Wi-Fi subnet to ping and access the NUC's local IP address.

For stable development, it is highly recommended to configure your Wi-Fi router to assign a **static DHCP reservation** to the NUC's Wi-Fi MAC address so it retains the same IP across reboots.

To check the robot's current IP address on the Wi-Fi interface, run the following on the NUC:

```bash
ifconfig -a
```

Look for the `wlan0` (or similar wireless) interface — the `inet` field shows the robot's Wi-Fi IP address, which is typically in the `10.1.10.xxx` range.

### 2. ROS 2 Network Communication (Zenoh & DDS)

Stretch 4 uses ROS 2 Jazzy, which defaults to the Zenoh middleware (`rmw_zenoh_cpp`) for node communication. This makes sharing ROS 2 topics over the network extremely efficient:

#### **Zenoh Bridging**:&#x20;

To connect to a Zenoh router running on the robot from a different computer, you should:

1. Stop the running Zenoh daemon: `systemctl --user stop zenoh.service`
2. Run Zenoh on the robot exposed to the network using `ZENOH_CONFIG_OVERRIDE=listen/endpoints=["tcp/0.0.0.0:7447"] ros2 run rmw_zenoh_cpp rmw_zenohd`&#x20;

> Note: You can modify Zenoh to start on boot with the listen endpoint exposed by adding `Environment=ZENOH_CONFIG_OVERRIDE=listen/endpoints=["tcp/0.0.0.0:7447"]` to `~/.config/systemd/user/zenoh.service` on your robot.

3. Lastly, on the client you can run:

```
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
# Replace ROBOT_IP with your Stretch's IP address
ROBOT_IP=192.168.X.XXX
export ZENOH_CONFIG_OVERRIDE='mode="client";connect/endpoints=["tcp/$ROBOT_IP:7447"]'
```

#### **DDS Subnets**

If configuring the robot to use traditional DDS middleware (like CycloneDDS or FastDDS), ensure your developer machine and the NUC are on the same subnet (or VPN network) and share the exact same `ROS_DOMAIN_ID`.

### 3. Web-Based Interfaces

Many ROS and Python SDK applications on Stretch run local web servers to expose web consoles, camera stream feeds, or teleoperation web tools:

* **Exposing Services**: You can run web interfaces (e.g. Foxglove Studio, ROS Bridge, or custom Flask/React servers) on the NUC.
* **Connecting**: You can access these interfaces from any remote device by entering `http://<ROBOT_WIFI_OR_TAILSCALE_IP>:<PORT>` in your browser.

***

## Network Utilities and Commands

The following command-line tools can be used **on the NUC** (either via a tethered monitor or an SSH session) to verify connectivity and inspect the internal network configuration:

### Checking Device Connectivity

You can test the connectivity to the internal components from the NUC using standard `ping` commands:

```bash
# Ping the Jetson Orin
ping 192.168.1.101

# Ping the Right Lidar
ping 192.168.1.201

# Ping the Left Lidar
ping 192.168.1.202
```

### Remote Access to the Jetson Orin

From the NUC's terminal, you can log in to the Jetson module via SSH:

```bash
ssh jetson1@192.168.1.101
```

> \[!NOTE] During robot manufacturing and calibration, the networking utilities automatically configure passwordless key-based SSH access from the NUC to the Jetson by generating a standard SSH keypair (`~/.ssh/id_rsa`) on the NUC (if one does not exist) and copying it to the Jetson using `ssh-copy-id`. Consequently, you should be able to log in to the Jetson from the NUC directly without entering a password. If you are unable to log in to the Jetson from the NUC, please contact us at <support@hello-robot.com>

### Checking Network Profiles (nmcli & GUI)

To verify the host network configuration on the NUC, you can check active network connections via the command line or the graphical settings interface:

#### Using the Command Line (nmcli)

Run the following command in a terminal:

```bash
nmcli connection show
```

The active Ethernet profile should show manual configuration. You can inspect the IP addresses assigned to the wired profile with:

```bash
nmcli connection show "Wired connection 1" | grep ipv4.addresses
```

Expected output (addresses may appear on a single line):

```
ipv4.addresses:    192.168.1.2/24, 192.168.1.100/24
```

If both addresses are listed, the NUC's internal network profile is correctly configured.

#### Using the Network Manager GUI

If you have a monitor connected to the NUC (or are connected via RustDesk/RDP):

1. Open the **Settings** application from the desktop.
2. Select **Network** from the left sidebar.
3. Under the **Wired** section, click the **Gear (Settings)** icon next to the active wired connection profile.
4. Navigate to the **IPv4** tab.
5. Verify that:
   * The **IPv4 Method** is set to **Manual**.
   * Both IP addresses (`192.168.1.2` and `192.168.1.100`) are listed in the **Addresses** table, each with a netmask of `255.255.255.0`.

<figure><img src="/files/aBowm7Zf7VY27hePm9fd" alt=""><figcaption></figcaption></figure>

If the configuration matches the above, the NUC's internal network is correctly set up and all internal devices (Jetson and Lidars) should be reachable.


# Packing and Transporting

<figure><img src="/files/n9BMzFEG1MXE05HvX9Jx" alt=""><figcaption></figcaption></figure>




---

[Next Page](/llms-full.txt/1)

