- See RobotX GUI for the first version of the GUI.
Quick Setup
README.md
Installation
- Install dependencies from the root of the workspace
rosdep install --from-paths src --ignore-src -r -y
- Build this package
colcon build --packages-select greenhorn_gui
Running the GUI
- Launch the rosbridge (websocket) server and Flask server/node
source install/setup.bash ros2 launch greenhorn_gui servers.launch.py
- Launch light buoy sim in another terminal
source install/setup.bash ros2 launch greenhorn_gui light_buoy_sim_launch.py
- Open GUI in web browser using a local server (ex: Live Server on VS Code or
python3 -m http.server 5500
)http://localhost:5500/src/greenhorn/greenhorn_gui/gui/
- Run Flask demo page, shows a table of waypoints
http://localhost:5000/
Virtual Joystick to /joy topic
- See Joy Package
Virtual joystick publish to /joy topic
- The code converts the joystick position into a normalized linear (
x
) and angular (y
) velocity based on the joystickβs angle and distance from the center, scaling it to the maximum set values (max_linear
andmax_angular
).- The
axes
array represents the joystickβs linear (forward-backward) and angular (left-right) velocities to be sent in the ROSJoy
message.manager.on('move', function (event, data) { max_linear = 1.0; // Max linear velocity (m/s) max_angular = 1.0; // Max angular velocity (rad/s) max_distance = 75.0; // Max joystick distance (pixels) const x = Math.sin(data.angle.radian) * max_linear * data.distance / max_distance; const y = -Math.cos(data.angle.radian) * max_angular * data.distance / max_distance; // const x = data.position.x - 547/2; // const y = data.position.y - 305/2; const axes = [x, y]; console.log("x: " + x + " y: " + y); console.log(data); const msg = new ROSLIB.Message({ axes: axes, buttons: [0, 0, 0] }); joy_publisher.publish(msg); }); manager.on('end', function () { // When joystick stops moving, send a zeroed Joy message const msg = new ROSLIB.Message({ axes: [0, 0], buttons: [0, 0, 0] }); joy_publisher.publish(msg); }); }
Test joystick with turtlesim
- remap
/turtle1/cmd_vel
topic
ros2 run turtlesim turtlesim_node --ros-args --remap /turtle1/cmd_vel:=/cmd_vel
- check if remap was successful
ros2 topic info /turtle1/cmd_vel
- publish sample message to cmd_vel
ros2 topic pub /cmd_vel geometry_msgs/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.0}}"
-
Control turtle with joystick
-
Update
CMakeLists.txt
install(
DIRECTORY config
DESTINATION share/${PROJECT_NAME}/
)
Emergency Stop Publisher

- Enable emergency stop via GUI
- Pressing the βEmergency Stopβ button sends a βkilledβ message to
/control_mode
topic
- Pressing the βEmergency Stopβ button sends a βkilledβ message to
const eStopButton = document.getElementById('eStopButton');
const e_stop_publisher = new ROSLIB.Topic({
ros: ros,
name: "/control_mode",
messageType: "std_msgs/String"
});
function sendEStop() {
const message = new ROSLIB.Message({
data: "killed"
});
e_stop_publisher.publish(message);
console.log("Sent kill command")
}
eStopButton.addEventListener('click', sendEStop);
index.js
index.js
// --------WebSocket Connection to ROS Setup and Status------------- var ros = new ROSLIB.Ros({ url: 'ws://localhost:9090' }); ros.on('connection', function() { statusLabel.className = "deploy-status open"; console.log('Connected to websocket server.'); }); ros.on('error', function(error){ document.getElementById("status").innerHTML = "Error connecting to ROS"; statusLabel.className = 'status-error'; console.log('Error connecting to websocket server.'); }) ros.on('close', function() { statusLabel.className = 'deploy-status dead'; console.log('Closed connection to websocket server.'); }); // --------Light Tower Sequence Subscriber for Scan the Code------------- const statusLabel = document.getElementById('status'); const buoy_div = document.getElementById('buoy_div'); const buoy_label = document.getElementById('buoy_label'); const toggle_button = document.getElementById('toggleBuoy'); const light1 = document.getElementById('box1'); const label1 = document.getElementById('label1'); const light2 = document.getElementById('box2'); const label2 = document.getElementById('label2'); const light3 = document.getElementById('box3'); const label3 = document.getElementById('label3'); var listener = new ROSLIB.Topic({ ros: ros, name: "/light_sequence", messageType: "std_msgs/String", }); var box_colors = ['black', 'black', 'black']; listener.subscribe(function (message){ var incomingLight = message.data; // console.log("Received message: " + incomingLight); var box_color = "box-" + incomingLight; if (!buoy_div.classList.contains('hidden') ){ buoy_div.classList = box_color; } if (incomingLight !== "black") { if (box_colors[0] === "black") { box_colors[0] = incomingLight; } else if (box_colors[1] === "black") { box_colors [1] = incomingLight; } else if (box_colors[2] === "black") { box_colors[2] = incomingLight; } } else { // Reset all boxes to black after the sequence box_colors = ['black', 'black', 'black'] } var labels = [label1, label2, label3]; var lights = [light1, light2, light3]; for (let i = 0; i < 3; i++) { var color = box_colors[i]; if (color !== "black") { labels[i].textContent = color.toUpperCase(); } else { labels[i].textContent = " "; } lights[i].className = "box-" + color; } }); // --------Buoy Simulator Visibility Toggle------------- function toggleBuoy(){ // console.log(buoy_div.textContent); if (buoy_div.classList.contains('hidden')){ buoy_div.classList.remove('hidden'); // buoy_label.classList.remove('hidden'); toggle_button.textContent = "Hide"; } else { buoy_div.classList.add('hidden'); // buoy_label.classList.add('hidden'); toggle_button.textContent = "Show"; } } toggle_button.addEventListener('click', toggleBuoy); // --------Map Setup and Waypoint Management------------- const map_div = document.getElementById('map'); const map_label = document.getElementById('map_label'); const map_coords_label = document.getElementById('map_coords_label'); // 0) demo 1) OSM-based map style const map_styles = ['https://demotiles.maplibre.org/style.json', 'https://tiles.stadiamaps.com/styles/alidade_smooth.json'] var placeName = "Nathan Benderson Park, FL" var map_url = "https://www.google.com/maps/place/Nathan+Benderson+Park/@27.3742443,-82.4500873,15z/data=!4m6!3m5!1s0x88c338bd14033973:0xba4efc2b7130fac1!8m2!3d27.3742443!4d-82.4500873!16s%2Fg%2F11f11nr15f?entry=ttu&g_ep=EgoyMDI0MDkyNC4wIKXMDSoASAFQAw%3D%3D"; var coordinates = [-82.4500873, 27.3742443]; const map = new maplibregl.Map({ container: 'map', style: map_styles[1], // center: [0, 0], // Longitude, Latitude center: coordinates, zoom: 14 }); map.on('click', (event) => { const lngLat = event.lngLat; console.log('New waypoint:', lngLat); // Add waypoint new maplibregl.Marker() .setLngLat(lngLat) .addTo(map); // Send waypoint to Flask server const waypoint = { lng: lngLat.lng, lat: lngLat.lat }; fetch('http://localhost:5000/waypoints', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(waypoint) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => console.log('Success:', data)) .catch(error => console.error('Error:', error)); }); // Update map labels map_coords_label.textContent = placeName + " (" + `${coordinates[0]}, ${coordinates[1]}` + ")"; document.getElementById('map_link').href = map_url; // --------Virtual Joystick Integration with ROS------------- // // Command velocity publisher // cmd_vel_publisher = new ROSLIB.Topic({ // ros : ros, // name : "/cmd_vel", // messageType : 'geometry_msgs/Twist' // }); // move = function (linear, angular) { // var twist = new ROSLIB.Message({ // linear: { // x: linear, // y: 0, // z: 0 // }, // angular: { // x: 0, // y: 0, // z: angular // } // }); // cmd_vel_publisher.publish(twist); // } // Create virtual joystick object and handle events createJoystick = function () { var options = { zone: document.getElementById('zone_joystick'), threshold: 0.1, position: { left: '50%' }, mode: 'static', size: 150, color: '#000000', }; manager = nipplejs.create(options); manager.on('start', function (event, data) { timer = setInterval(function () { // move(linear_speed, angular_speed); // console.log("Timer running"); }, 25); }); // sensor_msgs/Joy Message // Header header # timestamp in the header is the time the data is received from the joystick // float32[] axes # the axes measurements from a joystick // int32[] buttons # the buttons measurements from a joystick joy_publisher = new ROSLIB.Topic({ ros : ros, name : "/joy", messageType : 'sensor_msgs/Joy' }); manager.on('move', function (event, data) { max_linear = 1.0; // Max linear velocity (m/s) max_angular = 1.0; // Max angular velocity (rad/s) max_distance = 75.0; // Max joystick distance (pixels) const x = Math.sin(data.angle.radian) * max_linear * data.distance / max_distance; const y = -Math.cos(data.angle.radian) * max_angular * data.distance / max_distance; // const x = data.position.x - 547/2; // const y = data.position.y - 305/2; const axes = [x, y]; console.log("x: ", x, "y: ", y); var twist = new ROSLIB.Message({ axes: axes, buttons: [0, 0, 0] }); joy_publisher.publish(twist); }); manager.on('end', function () { if (timer) { clearInterval(timer); } }); } createJoystick();
Robot Vehicle statusLabel
- Test status indicator
ros2 topic pub robot_state std_msgs/Int8 "{data: 3}"
Xbox Controller
Map Layer- Buoys
- Add
gps_to_coords.py
file
greenhorn_gui/
βββ CMakeLists.txt
βββ package.xml
βββ gps_to_coords.py
βββ gui/
β βββ index.html
β βββ index.js
β βββ style.css
βββ launch/
β βββ light_buoy_sim_launch.py
β βββ websocket.launch.py
βββ light_buoy_sim/
β βββ light_buoy_publisher.py
βββ server/
βββ app.py
- Make it executable
chmod +x gps_to_coords.py
- Create
gps_to_coords_launch.py
file inlaunch
folder
gps_to_coords_launch.py
import launch
from launch import LaunchDescription
from launch.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='greenhorn_gui',
executable='gps_to_coords.py', # Use the Python file as executable
name='gps_to_coords_node',
output='screen'
)
])
- Run the launch file
ros2 launch greenhorn_gui gps_to_coords_launch.py