vizanti repository

Repository Summary

Checkout URI
VCS Type git
VCS Version noetic-devel
Last Updated 2023-09-19
CI status No Continuous Integration
Tags No category tags.
Contributing Help Wanted (0)
Good First Issues (0)
Pull Requests to Review (0)


Name Version
vizanti 0.1.0


Icon Vizanti - Web Visualizer & Mission Planner for ROS

License Build Status

Vizanti is a web-based visualization and control tool developed for more convenient operation of outdoor robots running the Robot Operating System (ROS). The application attempts to replicate RViz's orthographic 2D view as closely as possible with a smartphone friendly interface. The second goal is to allow planning and executing movement and mission commands, i.e. goals and waypoints, with custom buttons and parameter reconfigure.


As a field tool, Vizanti is designed to operate just as well without internet access, and as such the intended way is to host it on a robot, with rosbridge autoconnecting to the host IP.

cd ~/catkin_ws/src
git clone
cd ..
rosdep install --from-paths src --ignore-src

Or if rosdep fails for some reason, these are the main two deps:

sudo apt install ros-noetic-rosbridge-suite python3-flask

Flask and Jinja2 are used for templating, rosbridge is required for socket communication.


roslaunch vizanti server.launch

The web app can be accessed at http://<host_ip>:5000. Client settings are automatically saved in localStorage. The satelite imagery renderer also uses the indexedDB to store tiles for offline use (note that this is IP specific). By default the rosbridge instance also occupies port 5001.

If you're using a mobile device connected to a robot's hotspot that doesn't have internet access, make sure to turn off mobile data. This will prevent Android from sending packets to the wrong gateway.

Note that the client uses the web fetch API to load a fair few things, make sure your browser is at least somewhat up to date or some features may not work.

Feature list

Aside from the required ones, custom widgets can be added to the navbar to customize functionality for a given robot and test setup.

Note: Some icons open setup modals instantly, while others use a single press to trigger actions and use a long press to open the modal.

General Tools & Configuration #### Global Settings Set the background color and the fixed TF frame. Also has a button to reset the camera view to zero and default zoom. #### Grid The adjustable metric grid. Currently renders only in the fixed frame. #### TF Renders TF frames, same options as in RViz for the most part. #### Robot Model Renders a 2D sprite to represent the robot model or any specific TF link. #### Dynamic Reconfigure Adjust configurations of all nodes supporting dynamic reconfigure params. Currently rather slow to load and update, but will make sure parameters are current. It treats ints as floats due to type autodetection problems. #### Bag Recorder Recording specified topics by calling rosbag record via proxy. #### Node Manager See info printouts of a specific node, kill nodes, launch nodes, that sort of thing. #### Add new visualizer/widget Self explanatory.
Mission Planning #### Teleop Joystick Joystick used for publishing Twist messages, can be positioned anywhere on the screen and switched into holonomic mode. #### 2D Pose Estimate Send the /initialpose for navigation startup. Long press to open setup menu. #### 2D Nav Goal Send a /move_base_simple/goal. Long press to open setup menu. #### Waypoint Mission Create missions with multiple waypoints, then send them as a Path message. Single tap to add a point, single tap to remove an existing one, hold and drag to move points. Adding a point on an existing line will add it between those two points. Long press to open setup menu. #### Area Mission Drag to select an area and publish it to a PolygonStamped topic. Since the area is a rectangle, the first polygon vertex will be at the cursor press, and the third vertex will be the press released point. Long press to open setup menu. #### Button A button with customizable text that displays the last message sent on a Bool topic and sends the inverse to toggle it when pressed. Also supports just sending messages to an Empty topic. Long press to open setup menu.
Data Visualization #### Map Display an OccupancyGrid. Also has some experimental map_server controls for saving and loading maps. #### Satellite Tiles Display satelite imagery, by default from OpenStreetMap. Requires a Fix origin with the correct frame in its header. #### Battery Display a BatteryState message. #### Compressed Image Display a CompressedImage message in a movable box anywhere on the screen. Heavily throttled by default. #### Marker Array Visualize a MarkerArray. Currently supported types are ARROW, CUBE, SPHERE, CYLIDER, LINE_STRIP and TEXT_VIEW_FACING. Since each of these widgets adds another canvas layer, it makes more sense to aggregate regular Marker messages into a Marker Array to avoid some of that overhead. #### Path Render a Path message for navigation debugging. #### Range Render a Range message on the main view. Supports grouping multiple messages onto the same topic, as long as the tf frames are different. #### Laser Scan Display a LaserScan message on the main view. Heavily throttled by default. #### Point Cloud Display a PointCloud2 message on the main view. Heavily throttled by default. #### Pose with Covariance (Stamped) Display a PoseWithCovarianceStamped message. The covariance rendering is currently experimental and will likely only display correctly for spherical covariance. #### Pose Array Display a PoseArray message. Throttled to 15 hz. #### Temperature Display a Temperature message. Only as a widget for now, not on the view itself.


Please see for more information.


Contributing to Vizanti

Got an idea on how to improve Vizanti?

Contrbutions are of course welcome, feel free to fork and submit PRs for any fixes or extra widgets you find useful.


Vizanti is split into the python based ROS-side server and the web browser client.


The static content is served using Flask, with Jinja2 as the template engine. ROS related communications go through Rosbridge and Rosapi.


There are also two ROS nodes that provide additional functionality not covered by Rosbridge.




The front end is written in vanilla JS with ES6 modules, while making use of Jinja templating and minimal fixed dependencies in public/js/lib.

Adding a new widget

Upon client load, the server collects all widget templates in public/templates and sends them to the client where they are processed by main.js. As per the existing examples, the suported files are:

  • name_icon.html The mandatory icon element that will be added to the taskbar.

  • name_modal.html A popup window element that is used to select the topic and change any settings.

  • name_view.html A canvas layer for drawing visualizers. Define the Z-order value so that your element is rendered in the right order. Canvases of the same type of widget will be rendered in the order they were added.

  • name_script.js A JS module that can import helper singletons from public/js/modules.

To add the widget to the taskbar, it has to be defined in the add_types_container list in the add_modal.html. The data-topic value is used for creating widgets from a specific topic.

Any required assets should be put into public/assets.


Every widget should have the string "{uniqueID}" embedded in its code, which will be replaced with a unique sequential identifier (e.g. "auto53") at widget instantiation, so all of those scattered elements above can have direct references to each other and a unique tag for saving settings.

As an example:

<div id="{uniqueID}_icon" class="icon noselect">
    <img src="assets/battery_unknown.svg" alt="?" width="50" height="50" onclick="openModal('{uniqueID}_modal')">

Which can then be obtained from the script as follows:

const icon = document.getElementById("{uniqueID}_icon");
icon.src = "something.png"

Helper Singletons

Quaternion.js and Roslib.js are loaded globally. There are also two global functions to streamline opening and closing models, the aptly named openModal(widget_id+"_modal") and closeModal(widget_id+"_modal") found in setup.js. Typically called when an icon is clicked.


let viewModule = await import(`${base_url}/js/modules/view.js`);
let view = viewModule.view;

Handles how the screen is moved and zoomed by the user for final rendering from ROS tf space into screen space. The TF and screen frames are currently not rotated, which simplifies rendering.


const img_width = view.getMapUnitsInPixels(width_meters);
const img_height = view.getMapUnitsInPixels(length_meters);

let pos = view.fixedToScreen({
    x: object.translation.x,
    y: object.translation.y,

ctx.translate(pos.x, pos.y);
ctx.drawImage(img, -img_width/2, -img_height/2, img_width, img_height);

It fires off events when the screen is updated as a rendering callback.

window.addEventListener("view_changed", drawWidget);

TF Transforms

let tfModule = await import(`${base_url}/js/modules/tf.js`);
let tf =;

An implementation of the TF graph that gets grouped TF data at 30 fps from the/vizanti/tf_consolidated topic.


//global rendering frame, as selected in global options

//absoluteTransforms provides all known frames transformed to the global fixed frame for quick rendering
const absolute_frame = tf.absoluteTransforms["base_link"];
absolute_frame.translation // Vector3
absolute_frame.rotation // Quaternion

//getting the radians yaw for top down rendering
let yaw = absolute_frame.rotation.toEuler().h;

//transforms provides the relative parent-child transforms as given by TF
const relative_frame = tf.transforms["base_link"];
relative_frame.parent //"odom", most likely

Like the view, it fires off events when the screen is updated as a rendering callback.

window.addEventListener("tf_changed", drawWidget);


let rosbridgeModule = await import(`${base_url}/js/modules/rosbridge.js`);
let rosbridge = rosbridgeModule.rosbridge;

Wrapper for roslib.js, used for communicating with ROS.


//all running nodes
let nodes = await rosbridge.get_all_nodes();

//all topics
let topics = await rosbridge.get_all_topics();

//all topics of specified type
let specific_topics = await rosbridge.get_topics("sensor_msgs/NavSatFix");

//built in function to get the topic name passed by the widget creator
let topic = getTopic("{uniqueID}");

roslib_topic = new ROSLIB.Topic({
    ros : rosbridge.ros,
    name : topic,
    messageType : 'sensor_msgs/LaserScan',
    throttle_rate: 30, //minium ms between messages, alleviates congestion but does not guarantee that all messages will be received
    compression: "cbor" //compression for higher throughput topics

listener = roslib_topic.subscribe((msg) => {    


let persistentModule = await import(`${base_url}/js/modules/persistent.js`);
let settings = persistentModule.settings;

Provides a localStorage object for saving widget settings.


//if there's saved data, we fetch it upon module load
    const loaded_data  = settings["{uniqueID}"];
    topic = loaded_data.topic;
    something = loaded_data.something;
    checkbox.checked = loaded_data.checkbox;

function saveSettings(){
    settings["{uniqueID}"] = {
        topic: topic,
        something: input.value,
        checkbox: checkbox.checked

//on input change


let utilModule = await import(`${base_url}/js/modules/util.js`);
let imageToDataURL = utilModule.imageToDataURL;

The util class provides utility functions, the only one right now being imageToDataURL for persistent image loading that doesn't trigger new server requests upon changing an Image .src param.

const persistent_image = await imageToDataURL("assets/image.svg");


let StatusModule = await import(`${base_url}/js/modules/status.js`);
let Status = StatusModule.Status;

A helper class that emulates the status indicator from rviz for each widget.

First, add this element to the widget's modal, the current convention is directly below the title text.

<p id="{uniqueID}_status" class="status">Status: Ok.</p>

Then instantiate the class and pass it the icon and status elements:

//by default the status is "Ok" as defined in the html
let status = new Status(

//show an errpr
status.setError("Empty topic.");

//show a warning
status.setWarn("No data received.");

//everything should be working fine


let joystickModule = await import(`${base_url}/js/modules/joystick.js`);
let nipplejs = joystickModule.nipplejs;

Joystick.js is a module wrapper for nipple.js. Example usage in teleop:script.js

Satelite Tiles

let navsatModule = await import(`${base_url}/js/modules/navsat.js`);
let navsat = navsatModule.navsat;

A module for downloading, storing, and loading slippy map tiles, mainly for use in satelite_script.js.


const tileURL = ""

let tileImage = navsat.live_cache[tileURL];

//could be a placeholder if we're still loading, or not downloaded yet, in which case fetch it
if(!tileImage || !tileImage.complete){
    tileImage = placeholder;

//lattitude and longitude to coordinate tile indices
let tile_coords = navsat.coordToTile(longitude, latitude, zoomLevel)

//and the reverse, gets the bottom left corner of the tile
let tile_fix = navsat.tileToCoord(x, y, zoomLevel);

//distance between two points on a sphere
let distance_meters = navsat.haversine(latitude_a, longitude_a, latitude_b, longitude_b);

More general info: