Repository Summary
Checkout URI | https://github.com/MoffKalast/vizanti.git |
VCS Type | git |
VCS Version | ros2 |
Last Updated | 2024-10-12 |
Dev Status | MAINTAINED |
CI status | No Continuous Integration |
Released | UNRELEASED |
Tags | No category tags. |
Contributing |
Help Wanted (0)
Good First Issues (0) Pull Requests to Review (0) |
Packages
Name | Version |
---|---|
vizanti | 0.1.0 |
vizanti_cpp | 0.0.1 |
vizanti_demos | 0.0.2 |
vizanti_msgs | 0.1.0 |
vizanti_server | 0.1.1 |
README
Vizanti - Web Visualizer & Mission Planner for ROS
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.
Installation
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 ~/colcon_ws/src
git clone -b ros2 https://github.com/MoffKalast/vizanti.git
cd ..
rosdep install -i --from-path src/vizanti -y
colcon build
Docker
Alternatively, you can also containerize Vizanti. For that you need to install Docker and build the container:
In case you’re building on a system with a different version of ROS or it’s not installed, replace $ROS_DISTRO with either humble or jazzy.
git clone -b ros2 https://github.com/MoffKalast/vizanti.git
cd vizanti
docker build -f docker/Dockerfile -t vizanti:2.0 . --build-arg ROS_VERSION=$ROS_DISTRO
Run
ros2 launch vizanti_server vizanti_server.launch.py
Or with Docker (set env vars directly if they need to be different):
docker run --rm -it --net=host --name vizanti-ros2 -e ROS_DOMAIN_ID=$ROS_DOMAIN_ID -e RMW_IMPLEMENTATION=$RMW_IMPLEMENTATION -e USE_RWS=false -v /dev/shm:/dev/shm vizanti:2.0
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 and can’t load the page, turn off mobile data. This will prevent the browser from sending packets to the wrong gateway.
Check the wiki for usage and configuration instructions, as well as feature and compatibility info.
Optional - Experimental RWS Backend
With rosbridge being a Tornado python based package and rclpy being overly CPU heavy, this cpp drop-in replacement server should result in a ~5x lower overhead and faster response times. It works with CycloneDDS out of the box, and for FastDDS it requires the rmw_fastrtps_dynamic_cpp
version which includes interface introspection.
cd ~/colcon_ws/src
git clone -b $ROS_DISTRO https://github.com/v-kiniv/rws.git
cd ..
rosdep install -i --from-path src/rws -y
colcon build
If using FastDDS:
sudo apt install ros-$ROS_DISTRO-rmw-fastrtps-dynamic-cpp
export RMW_IMPLEMENTATION=rmw_fastrtps_dynamic_cpp
Then run the RWS launch instead:
ros2 launch vizanti_server vizanti_rws.launch.py
Or with Docker (set env vars directly if they need to be different):
docker run --rm -it --net=host --name vizanti-ros2 -e ROS_DOMAIN_ID=$ROS_DOMAIN_ID -e RMW_IMPLEMENTATION=$RMW_IMPLEMENTATION -e USE_RWS=true -v /dev/shm:/dev/shm vizanti:2.0
Contributing
Please see Contributing.md for more information.
CONTRIBUTING
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.
Overview
Vizanti is split into the python based ROS-side server and the web browser client.
Server
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.
Client
The front end is written in vanilla JS with ES6 modules, while making use of Jinja templating and minimal fixed dependencies in vizanti_server/public/js/lib.
Adding a new widget
Upon client load, the server collects all widget templates in vizanti_server/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 vizanti_server/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 vizanti_server/public/assets. Any SVG icons that need to display a colour change using utilModule.setIconColor need to have specific elements tagged as id=”fillColor” or id=”strokeColor” depending on which part needs coloring.
{uniqueID}
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:
//battery_icon.html
<div id="{uniqueID}_icon" class="icon noselect">
<img src="assets/battery_unknown.svg" alt="?" width="50" height="50" onclick="openModal('{uniqueID}_modal')">
</div>
Which can then be obtained from the script as follows:
//battery_script.html
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.
View
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.
Example:
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 = tfModule.tf;
An implementation of the TF graph that gets grouped TF data at 30 fps from the/vizanti/tf_consolidated
topic.
Example:
//global rendering frame, as selected in global options
tf.fixed_frame
//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);
Rosbridge
let rosbridgeModule = await import(`${base_url}/js/modules/rosbridge.js`);
let rosbridge = rosbridgeModule.rosbridge;
Wrapper for roslib.js, used for communicating with ROS.
Example:
//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) => {
console.log(msg.data)
});
Settings
let persistentModule = await import(`${base_url}/js/modules/persistent.js`);
let settings = persistentModule.settings;
Provides a localStorage object for saving widget settings.
Example:
//if there's saved data, we fetch it upon module load
if(settings.hasOwnProperty("{uniqueID}")){
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
}
settings.save();
}
//on input change
saveSettings();
Util
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");
Status
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(
document.getElementById("{uniqueID}_icon"),
document.getElementById("{uniqueID}_status")
);
//show an errpr
status.setError("Empty topic.");
//show a warning
status.setWarn("No data received.");
//everything should be working fine
status.setOK();
Joysticks
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.
Example:
const tileURL = "https://tile.openstreetmap.org/19/123/254.png"
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;
navsat.enqueue(tileURL);
}
//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:
https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
https://github.com/nobleo/rviz_satellite
Repository Summary
Checkout URI | https://github.com/MoffKalast/vizanti.git |
VCS Type | git |
VCS Version | noetic-devel |
Last Updated | 2024-10-08 |
Dev Status | MAINTAINED |
CI status | No Continuous Integration |
Released | UNRELEASED |
Tags | No category tags. |
Contributing |
Help Wanted (0)
Good First Issues (0) Pull Requests to Review (0) |
Packages
Name | Version |
---|---|
vizanti | 0.1.0 |
README
Vizanti - Web Visualizer & Mission Planner for ROS
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.
Installation
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 https://github.com/MoffKalast/vizanti.git
cd ..
rosdep install -i --from-path src/vizanti -y
catkin_make
Docker
Alternatively, you can also containerize Vizanti. For that you need to install Docker and build the container:
git clone https://github.com/MoffKalast/vizanti.git
cd vizanti
docker build -f docker/Dockerfile -t vizanti:1.0 .
Run
roslaunch vizanti server.launch
Or with Docker:
docker run -dit --net=host --name vizanti-noetic vizanti:1.0
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.
Check the wiki for usage and configuration instructions, as well as feature and compatibility info.
Contributing
Please see Contributing.md for more information.
CONTRIBUTING
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.
Overview
Vizanti is split into the python based ROS-side server and the web browser client.
Server
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.
Client
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. Any SVG icons that need to display a colour change using utilModule.setIconColor need to have specific elements tagged as id=”fillColor” or id=”strokeColor” depending on which part needs coloring.
{uniqueID}
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:
//battery_icon.html
<div id="{uniqueID}_icon" class="icon noselect">
<img src="assets/battery_unknown.svg" alt="?" width="50" height="50" onclick="openModal('{uniqueID}_modal')">
</div>
Which can then be obtained from the script as follows:
//battery_script.html
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.
View
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.
Example:
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 = tfModule.tf;
An implementation of the TF graph that gets grouped TF data at 30 fps from the/vizanti/tf_consolidated
topic.
Example:
//global rendering frame, as selected in global options
tf.fixed_frame
//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);
Rosbridge
let rosbridgeModule = await import(`${base_url}/js/modules/rosbridge.js`);
let rosbridge = rosbridgeModule.rosbridge;
Wrapper for roslib.js, used for communicating with ROS.
Example:
//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) => {
console.log(msg.data)
});
Settings
let persistentModule = await import(`${base_url}/js/modules/persistent.js`);
let settings = persistentModule.settings;
Provides a localStorage object for saving widget settings.
Example:
//if there's saved data, we fetch it upon module load
if(settings.hasOwnProperty("{uniqueID}")){
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
}
settings.save();
}
//on input change
saveSettings();
Util
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");
Status
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(
document.getElementById("{uniqueID}_icon"),
document.getElementById("{uniqueID}_status")
);
//show an errpr
status.setError("Empty topic.");
//show a warning
status.setWarn("No data received.");
//everything should be working fine
status.setOK();
Joysticks
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.
Example:
const tileURL = "https://tile.openstreetmap.org/19/123/254.png"
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;
navsat.enqueue(tileURL);
}
//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:
https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
https://github.com/nobleo/rviz_satellite