Skip to main content

Stretch4 Nav Webapp

Standalone web UI for Stretch 4 mapping, map editing (occupancy / keepout / speed zones / named rooms, plus crop & rotate), and navigation. The app runs on the robot, you can open it from any browser on the same network.


Requirements

Need Check it with
ROS 2 Jazzy with stretch_nav2 built ros2 pkg list | grep stretch_nav2
rosbridge_suite ros2 pkg list | grep rosbridge
Python 3.10+ python3 --version

If rosbridge_suite is missing:

sudo apt install ros-jazzy-rosbridge-server

Install

pip3 install --user hello-robot-stretch4-nav-webapp

The web UI ships prebuilt inside the package.

Use --user, not a virtualenv. The app needs rclpy — which comes from the robot's ROS install, never from pip. A --user install lives in the robot's system Python and can see it.

Run it

stretch_nav_webapp

Then open http://<robot-ip>:8080 from any browser on the same network (hostname -I on the robot gives you the IP).

That single command starts the API, serves the UI, and launches rosbridge. Everything else — mapping, navigation, the camera — is started from buttons in the UI.

Updating later

pip3 install --user --upgrade hello-robot-stretch4-nav-webapp
# then restart stretch_nav_webapp

To run the code from a git checkout instead, see Developing.


CLI flags

stretch_nav_webapp                                   # normal: UI on :8080, rosbridge on :9090
stretch_nav_webapp --port 9000                       # serve the UI on a different port
stretch_nav_webapp --rosbridge-port 9091             # move rosbridge
stretch_nav_webapp --maps-dir ~/my_maps              # use a different maps folder
stretch_nav_webapp --config ~/my_config.yaml         # use a different config file entirely
stretch_nav_webapp --no-rosbridge                    # you already run rosbridge yourself
stretch_nav_webapp --disconnected                    # touch nothing on the robot: map editing only
stretch_nav_webapp --host 127.0.0.1                  # local-only, no network access

Flags always win over the config file.

Robot connected / disconnected

The Robot connected switch in the header decides whether the app may run anything on the robot. Connected (the default), it runs its own rosbridge and the mapping / navigation stacks, each with its own stretch_driver. That collides with other apps driving the robot. Disconnected, the app runs nothing on the robot and only Edit Map works, so you can clean up a map while someone else uses the robot.

--disconnected starts the app that way. The switch only works between runs: stop mapping, navigation first. Connecting takes the robot over: it stops any rosbridge on the rosbridge port.


Configuration

One YAML file holds everything worth editing. Every setting below is safe to change; the app re-reads it on restart.

Ports and paths

ui_port: 8080          # where the web UI is served
rosbridge_port: 9090   # the WebSocket the browser subscribes to ROS through
maps_dir: null         # null = $HELLO_FLEET_PATH/maps, else ~/stretch_user/maps
host: "0.0.0.0"        # 0.0.0.0 = reachable from other machines; 127.0.0.1 = robot only
  • Change ui_port if something else already owns 8080. The browser URL changes with it; nothing else does.
  • Change maps_dir to point at a different map library, e.g. maps_dir: "/home/hello-robot/shared_maps". The folder is created if missing.
  • Leave host at 0.0.0.0 unless you specifically want the UI unreachable from other machines.

Launch commands

These are the exact ros2 launch commands the mode buttons run. The {braces} are filled in by the app before it runs them.

launches:
  mapping: "ros2 launch stretch_nav2 offline_mapping.launch.py use_rviz:={use_rviz}"
  navigation: "ros2 launch stretch_nav2 navigation_mppi.launch.py map:={map_yaml} use_rviz:={use_rviz}"
  navigation_filters: "ros2 launch stretch_nav2 navigation_mppi_keepout.launch.py map:={map_yaml} keepout_mask:={keepout_yaml} speed_mask:={speed_yaml} enable_keepout:={enable_keepout} enable_speed:={enable_speed} use_rviz:={use_rviz}"
  rosbridge: "ros2 run rosbridge_server rosbridge_websocket --ros-args -p port:={rosbridge_port} ..."

use_rviz: "false"      # set "true" to also open RViz alongside the web UI

Using your own config file

The default config lives inside the installed package. Copy it out, edit the copy, and point the app at it — that way an upgrade never overwrites your changes:

cp "$(python3 -c 'import stretch4_nav_webapp.app as a; print(a.DEFAULT_CONFIG_PATH)')" ~/my_config.yaml
# edit ~/my_config.yaml
stretch_nav_webapp --config ~/my_config.yaml

Modes

Button What it starts What you see
Mapping stretch_nav2 offline_mapping.launch.py Live map building as you drive with the gamepad
Edit Map Nothing on the robot — file editing only Paint tools for occupancy / keepout / speed / semantic / binary filter, plus crop & rotate
Navigation Nav2 MPPI (+ keepout/speed filters if enabled) Map, robot, lidar, costmaps, planned path, footprint

Only one mode runs at a time — starting one stops the previous one.

Both robot viewers are top-down and mirror the stock Stretch RViz displays. The Layers checkboxes hide overlays without stopping the data.


Where maps live

Under $HELLO_FLEET_PATH/maps (fallback ~/stretch_user/maps), one folder per map:

maps/<map_name>/
  map.pgm + map.yaml            The occupancy map itself
  keepout.pgm + keepout.yaml    No-go zones (black = blocked)
  speed.pgm + speed.yaml        Slow zones (darker = slower)
  semantic.png + semantic.yaml  Which room each cell belongs to
  semantic.json                 The room names and colours
  binary_filter_mask.pgm + .yaml   Areas for a binary filter you launch yourself
  locations.json                Saved places you can send the robot to
  docks.yaml                    The charging dock 
  .before_transform/            One undo step for crop & rotate

Docking

Navigation also starts stretch_nav2's docking_server.py and undocking_server.py (config key docking.launches; an empty list turns them off). In the Navigation panel, Charging dock:

  • + Mark dock / Move dock: click on the map where the dock stands and drag out from the wall, the way the dock faces. The dock model is drawn there.
  • Dock sends a DockRobot goal for the marked dock. The docking server drives there through Nav2, finds the dock and lines up on its own.
  • Undock sends an UndockRobot goal. The server checks there is room beside the dock before it slides off.
  • Cancel goal or Stop docking cancels either one.

Find dock lets the robot find the dock itself. Park it about 1 m from the dock, facing it, set its pose on the map, then press Start. The app starts the head camera's center socket, ArUco detection and stretch_nav2's discover_dock beside navigation. The robot doesn't move. When discover_dock confirms a dock (lidar shape plus the dock's tags), it's drawn on the map for you to save or discard.

A map with no dock of its own picks up the one discover_dock saved for a map of the same name (maps/docks/<map_name>_docks.yaml).


Keepout, speed & rooms

Edit Map paints four kinds of zone onto a saved map:

  • Keepout — the robot will not enter, at all.
  • Speed — the robot slows down; darker paint = slower.
  • Rooms — you name an area ("Kitchen") and paint where it is.
  • Binary filter — areas for a binary filter you launch yourself. Drawn here, never launched here.

Switch keepout and speed on when you start Navigation. That's when the app picks the navigation_filters launch instead of the plain one. See the Nav2 keepout tutorial for what Nav2 does with them.

Binary filter (draw here, launch yourself)

There is a fourth mask, Binary filter, and it is deliberately different: the app only draws it. Navigation from the app has no binary switch and never starts a filter for it, because a binary filter is only half a feature, the other half is the node you write that reacts to /binary_state (turn something on, play a sound, stop the arm). What should happen in a marked area is yours to define.

What the app writes:

maps/<map_name>/binary_filter_mask.pgm     255 everywhere, 153 where you painted
maps/<map_name>/binary_filter_mask.yaml    mode: scale, free_thresh 0.0, occupied_thresh 1.0

Point your own filter_mask_server at that yaml. With the usual base: 0.0, multiplier: 1.0, flip_threshold: 10.0, any pixel darker than about 229 flips the state, 153 is the grey used in the app.

It is cropped and rotated along with every other layer, so it stays registered against the map.

What the speed percentage means

The slider is the share of full speed the robot keeps: 80% means it drives at 80% of its normal speed there. That holds as long as the robot's speed filter is configured base: 100.0, multiplier: -1.0 (in stretch_nav2's nav2_filter_servers.yaml).

The slider stops at 5%, not 0%. Nav2 reads a speed limit of exactly zero as "no limit", so a pure-black speed zone would make the robot drive at full speed through it. To forbid an area outright, paint it on the keepout layer instead.


Crop & rotate

Edit Map → Crop & rotate (available on the Occupancy layer) reshapes the whole map. It is not a paint tool:

  • Keep inside trims the map to the selection; Remove inside clears the selection back to blank (unknown / no-restriction) and keeps the map size.
  • Rotation previews live on the canvas; nothing is written until Apply.
  • Every layer — occupancy, keepout, speed, rooms, binary — is transformed together, so the masks stay registered against the map.
  • Saved locations are moved to match, so a location on the kitchen door is still on the kitchen door afterwards.
  • One undo step is kept, in .before_transform/.

Developing

For changing the code rather than just running it. We're using Node v24 and we recommend verifying you have the same version (node --version), because here you build the UI yourself instead of getting it prebuilt from PyPI.

git clone https://github.com/hello-robot/stretch4_nav_webapp
cd stretch4_nav_webapp

pip3 install --user -e .          # editable: runs the clone in place

cd frontend
npm install
npm run build                     # writes stretch4_nav_webapp/static
cd ..

stretch_nav_webapp

The UI is served from stretch4_nav_webapp/static, not from frontend/src. If you change anything under frontend/src and skip npm run build, the browser keeps showing the old version.

Testing

python3 -m pytest

Releasing

Releases are built with a pinned local Node (v24) so the UI bundle is the same no matter what Node the machine has. One-time setup, from the repo root:

NODE_VER=v24.21.0
mkdir -p .node
curl -fsSL "https://nodejs.org/dist/$NODE_VER/node-$NODE_VER-linux-x64.tar.xz" \
  | tar -xJ --strip-components=1 -C .node
.node/bin/node --version          # v24.21.0

scripts/release.sh puts .node/bin first on PATH when it exists.

Then, for each release:

  1. Bump version in pyproject.toml to today's date (CalVer, YYYY.MM.DD).

  2. Build and check the artifacts:

    scripts/release.sh
    
  3. Upload:

    uv publish --token <pypi-token>
    

Adding a new mode

  1. Create stretch4_nav_webapp/modes/my_mode.py:
from stretch4_nav_webapp.modes.base import Mode, ModeContext, register, shlex_split_launch

@register
class MyMode(Mode):
    id = "my_mode"
    label = "My Mode"
    description = "Does something useful."
    needs_robot = True

    def start(self, ctx: ModeContext, **kwargs) -> dict:
        cmd = shlex_split_launch("ros2 launch my_pkg my.launch.py")
        ctx.process_manager.set_active_mode(self.id, cmd)
        return {"ok": True, "mode": self.id, "command": cmd}
  1. Import it from stretch4_nav_webapp/modes/__init__.py so it registers itself.
  2. Add a page and a button in frontend/src/App.jsx that calls api('/api/modes/my_mode/start', { method: 'POST', body: '{}' }).
  3. Rebuild the frontend and restart stretch_nav_webapp. The mode also shows up in GET /api/modes.

API

The same endpoints the UI buttons use.

Modes

  • GET /api/status — active mode, ports, maps folder, process table
  • POST /api/modes/{mapping|navigation|edit_map}/start — accepts {"map_name": "...", "use_keepout": true, "use_speed": true} and {"skip_readiness": true} to bypass the homed/stowed/dongle guardrail
  • POST /api/modes/stop

Robot

  • GET /api/robot/readiness — homed, stowed, battery, runstop, dongle, in-flight action
  • POST /api/robot/home · POST /api/robot/stow — start the routine, returns immediately
  • POST /api/robot/runstop {"engaged": true|false} — the safety stop
  • POST /api/robot/gamepad/start
  • GET|POST /api/robot/connection {"connected": true|false}: the header switch

Navigation

  • POST /api/navigation/goal {"x":…, "y":…, "yaw":…}
  • POST /api/navigation/cancel
  • POST /api/navigation/initial_pose — tell the robot where it is
  • POST /api/navigation/dock · POST /api/navigation/undock: start a dock / undock, returns immediately
  • GET /api/navigation/dock/status: what the dock / undock is doing
  • POST /api/navigation/dock/discover/start · .../stop · GET /api/navigation/dock/discover: Find dock

Maps

  • POST /api/mapping/save {"name": "..."}
  • GET /api/maps
  • GET|POST /api/maps/{name}/layer/{occupancy|keepout|speed|semantic|binary}
  • GET|PUT /api/maps/{name}/locations
  • GET|PUT|DELETE /api/maps/{name}/dock: {"x":…, "y":…, "yaw":…}, the dock's docking_station_link pose in the map frame
  • GET|PUT /api/maps/{name}/semantic — room names/colours
  • GET|POST /api/maps/{name}/transform · POST /api/maps/{name}/transform/undo

Camera

  • GET /api/camera/status · POST /api/camera/start · POST /api/camera/stop

Troubleshooting

Symptom Cause
UI loads, robot never moves Runstop engaged — the banner over the map has a Release button
Mode starts but the map stays empty Give it ~30 s. If the Driver/Lidar chips go red, stop and restart the mode
Robot shown in the wrong place Use Set pose to tell it where it actually is
Your UI change didn't appear Working from source and npm run build wasn't run
Address already in use Something else owns 8080 — use --port or change ui_port
Camera panel stuck on "Waiting for video" Normal for 20–30 s. Longer means the driver failed — check /tmp/stretch4_nav_webapp_camera.log

Every process the app starts writes its output to /tmp/:

/tmp/stretch4_nav_webapp_mode_navigation.log
/tmp/stretch4_nav_webapp_mode_mapping.log
/tmp/stretch4_nav_webapp_rosbridge.log
/tmp/stretch4_nav_webapp_camera.log
/tmp/stretch4_nav_webapp_gamepad.log
/tmp/stretch4_nav_webapp_body_server.log

That's the first place to look when something won't start — it holds the real ros2 launch output, errors included.

Metadata

Release files for hello-robot-stretch4-nav-webapp 2026.10.20

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hello-robot-stretch4-nav-webapp 2026.10.20
File Size Uploaded Provenance
hello_robot_stretch4_nav_webapp-2026.10.20.tar.gz 448.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for hello-robot-stretch4-nav-webapp 2026.10.20
File Interpreter ABI Platform
hello_robot_stretch4_nav_webapp-2026.10.20-py3-none-any.whl Python 3 none any Details

Total release size: 811.8 kB

Release files / hello_robot_stretch4_nav_webapp-2026.10.20.tar.gz

Download URL hello_robot_stretch4_nav_webapp-2026.10.20.tar.gz
Size 448.7 kB
Tags Source
SHA-256 checksum
How to use checksums
55012ae10aaa6762dea81d7e9a951003cc6a2a046e78e4de8247f085c8684016
BLAKE2b-256 checksum
How to use checksums
eb45a07857fb4493bd0b0c1d893ae587df8252e0662e1ee286571236087a7c4d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / hello_robot_stretch4_nav_webapp-2026.10.20-py3-none-any.whl

Download URL hello_robot_stretch4_nav_webapp-2026.10.20-py3-none-any.whl
Size 363.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
91766e8076de9c4161c3c61e3d6f8804af8e7966cfeed09fa43df97630b5a8b4
BLAKE2b-256 checksum
How to use checksums
9f2075e46f3bd477911561f5db34ea5c99ebcc838781c5c117a5f386b1739f5f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for hello-robot-stretch4-nav-webapp 2026.10.20

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

2026.10.20 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page