Krrish/coord pr#350
Conversation
…ion/raw/fix(not my topic, it was already publishing to that)
…d vizualizes it,currently works for rayfronts
…added non-world prims to save in flattened manner
There was a problem hiding this comment.
Pull request overview
This PR introduces a multi-robot GCS workflow centered around Foxglove: custom panels (waypoints/polygons/tasks), an action relay that bridges task actions across DDS domains, a GCS-side visualizer for unified multi-robot rendering (including a sim overhead “ground”), plus a new coordination/gossip layer for broadcasting peer state and typed payloads.
Changes:
- Adds Foxglove extensions + persisted waypoint/polygon editors, and wires them into the GCS container startup.
- Adds cross-domain action relaying (GCS domain 0 ↔ robot domain N) and expands GCS visualization / DDS allowlists for multi-robot use.
- Adds a new coordination/gossip layer (msgs + bringup + docs) and extends the random-walk planner to support polygon-bounded exploration.
Reviewed changes
Copilot reviewed 76 out of 95 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| simulation/isaac-sim/utils/scene_prep.py | Adds orthographic overhead camera publisher + USD root-prim helpers + collector/texture fixups |
| simulation/isaac-sim/launch_scripts/two_drone_scene_import.py | Removes legacy two-drone import script |
| simulation/isaac-sim/launch_scripts/gps_utils.py | Adds per-drone GPS-origin helpers for multi-drone scenes |
| simulation/isaac-sim/launch_scripts/example_multi_drone_scene_import.py | New reference multi-drone + scene import + overhead camera wiring |
| simulation/isaac-sim/launch_scripts/barebones_pegasus_launch.py | Adds PLAY_SIM_ON_START gating |
| simulation/isaac-sim/docker/docker-compose.yaml | Removes commented HITL service stub |
| robot/ros_ws/src/global/planners/random_walk/src/random_walk_node.cpp | Adds polygon bounds propagation + “approach polygon” pre-flight logic |
| robot/ros_ws/src/global/planners/random_walk/src/random_walk_logic.cpp | Adds point-in-polygon bounds checks + nearest-inside helper |
| robot/ros_ws/src/global/planners/random_walk/launch/random_walk_launch.xml | Updates launch remaps/paths for namespaced usage |
| robot/ros_ws/src/global/planners/random_walk/include/random_walk_node.hpp | Adds storage for pending bounds + mutex |
| robot/ros_ws/src/global/planners/random_walk/include/random_walk_logic.hpp | Adds polygon-bound API + makes mutex mutable for const methods |
| robot/ros_ws/src/global/planners/random_walk/config/random_walk_config.yaml | Adds new config keys and topics for random-walk node |
| robot/ros_ws/src/global/global_bringup/launch/global.launch.xml | Adjusts remaps/naming notes for namespaced random-walk planner |
| robot/ros_ws/src/autonomy_bringup/onboard_all/config/domain_bridge.yaml | Adds additional sim visualization topics to bridge |
| robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml | Expands allowlist for cameras, trajectory vis, global plan; notes about gossip router separation |
| robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml | Adds coordination/gossip bringup include + args |
| robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py | Scopes LD_LIBRARY_PATH for ddsrouter execution |
| mkdocs.yml | Adds nav entries for new GCS/sim/coordination docs |
| gcs/saves/.gitkeep | Adds persistent save directory placeholder |
| gcs/ros_ws/src/gcs_visualizer/setup.py | New GCS visualizer ROS 2 Python package setup |
| gcs/ros_ws/src/gcs_visualizer/setup.cfg | New package install script config |
| gcs/ros_ws/src/gcs_visualizer/resource/gcs_visualizer | New ament resource marker |
| gcs/ros_ws/src/gcs_visualizer/package.xml | New package manifest for GCS visualization nodes |
| gcs/ros_ws/src/gcs_visualizer/launch/gcs_visualizer.launch.xml | Launches GCS visualizer + payload + waypoint/polygon collectors |
| gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/waypoint_collector_node.py | Implements waypoint editor backend + save persistence + marker/path publishing |
| gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/polygon_collector_node.py | Implements polygon editor backend + save persistence + marker publishing |
| gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/payload_visualizer_node.py | Republishes gossip payloads per-robot/per-payload for Foxglove |
| gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/gcs_utils.py | Adds shared GCS utilities (PC2→markers, transforms) |
| gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/init.py | Package init |
| gcs/ros_ws/src/action_relay/setup.py | New action relay ROS 2 Python package setup |
| gcs/ros_ws/src/action_relay/setup.cfg | New package install script config |
| gcs/ros_ws/src/action_relay/resource/action_relay | New ament resource marker |
| gcs/ros_ws/src/action_relay/package.xml | New package manifest for action relay |
| gcs/ros_ws/src/action_relay/launch/action_relay.launch.py | Launches one relay instance per robot based on NUM_ROBOTS |
| gcs/ros_ws/src/action_relay/action_relay/init.py | Package init |
| gcs/foxglove_extensions/waypoint-editor/package.json | New Foxglove Waypoint Editor extension metadata |
| gcs/foxglove_extensions/waypoint-editor/dist/extension.js | Bundled Waypoint Editor panel implementation |
| gcs/foxglove_extensions/robot-commands/package.json | New/updated Robot Tasks extension metadata |
| gcs/foxglove_extensions/render_layout.py | Generates NUM_ROBOTS-matched Foxglove layout JSON |
| gcs/foxglove_extensions/polygon-editor/package.json | New Foxglove Polygon Editor extension metadata |
| gcs/foxglove_extensions/polygon-editor/dist/extension.js | Bundled Polygon Editor panel implementation |
| gcs/foxglove_extensions/install.sh | Installs unpacked Foxglove extensions into Foxglove Studio extensions dir |
| gcs/docker/gcs-base-docker-compose.yaml | Runs extension install + layout render; mounts saves + extensions; exports NUM_ROBOTS |
| docs/simulation/isaac_sim/spawning_drones.md | Documents multi-drone spawning + GPS origins pattern |
| docs/simulation/isaac_sim/overhead_camera.md | Documents overhead camera producer and Foxglove ground rendering |
| docs/robot/autonomy/coordination/payloads.md | Documents config-driven gossip payloads + GCS visualization handlers |
| docs/robot/autonomy/coordination/index.md | Documents coordination layer architecture and usage |
| docs/gcs/waypoints_and_geofences.md | Documents waypoint/polygon editor workflow + persistence |
| docs/gcs/foxglove.md | Documents GCS visualizer topics, discovery, and layout behavior |
| common/ros_packages/robot_descriptions/launch/robot_state_publisher.launch.py | Adjusts URDF lookup logic/package reference text |
| common/ros_packages/robot_descriptions/CMakeLists.txt | Minor trailing whitespace change |
| common/ros_packages/msgs/task_msgs/action/SemanticSearchTask.action | Updates semantic-search goal fields and defaults (adds background_queries) |
| common/ros_packages/gui/rviz/rviz_tasks_panel/src/tasks_panel.cpp | Updates RViz task panel fields for semantic search changes |
| common/ros_packages/desktop_bringup/params/domain_bridge.yaml | Minor formatting change |
| common/ros_packages/desktop_bringup/package.xml | Adds deps for new GCS-side components |
| common/ros_packages/desktop_bringup/launch/gcs.launch.xml | Launches Foxglove + bridge + gossip bridge + gcs_visualizer + action_relay |
| common/ros_packages/coordination/README.md | Adds coordination layer README |
| common/ros_packages/coordination/coordination_msgs/package.xml | New coordination_msgs package manifest |
| common/ros_packages/coordination/coordination_msgs/msg/PeerProfilePayload.msg | New payload wire-format message |
| common/ros_packages/coordination/coordination_msgs/msg/PeerProfile.msg | New peer profile wire-format message |
| common/ros_packages/coordination/coordination_msgs/CMakeLists.txt | Builds/generates coordination messages |
| common/ros_packages/coordination/coordination_bringup/setup.py | New coordination_bringup Python package setup |
| common/ros_packages/coordination/coordination_bringup/scripts/peer_registry_monitor.py | Adds CLI monitor for peer registry |
| common/ros_packages/coordination/coordination_bringup/scripts/gossip_node | Adds gossip_node entry script |
| common/ros_packages/coordination/coordination_bringup/resource/coordination_bringup | New ament resource marker |
| common/ros_packages/coordination/coordination_bringup/package.xml | New bringup package manifest |
| common/ros_packages/coordination/coordination_bringup/launch/gossip.launch.xml | Launches per-robot gossip router + gossip node |
| common/ros_packages/coordination/coordination_bringup/launch/gcs_gossip_bridge.launch.py | Launches GCS-side gossip DDS router |
| common/ros_packages/coordination/coordination_bringup/coordination_bringup/peer_profile.py | Adds PeerProfile helper API for payload serialization |
| common/ros_packages/coordination/coordination_bringup/coordination_bringup/gossip_node.py | Implements gossip publishing, payload attachment, registry maintenance |
| common/ros_packages/coordination/coordination_bringup/coordination_bringup/frame_utils.py | Shared GPS/ENU + message transform helpers |
| common/ros_packages/coordination/coordination_bringup/coordination_bringup/init.py | Package init |
| common/ros_packages/coordination/coordination_bringup/config/gossip_payloads.yaml | Adds config-driven payload declaration file |
| common/ros_packages/coordination/coordination_bringup/config/gossip_dds_router.yaml | Adds per-robot gossip DDS router config |
| common/ros_packages/coordination/coordination_bringup/config/gcs_gossip_dds_router.yaml | Adds GCS-side gossip DDS router config |
| AGENTS.md | Adds new skills to agent index |
| .gitmodules | Minor trailing whitespace change |
| .gitignore | Ignores local scenes + persisted GCS saves |
| .env | Bumps version and changes defaults (NUM_ROBOTS, sim script) |
| .agents/skills/visualize-in-foxglove/SKILL.md | Adds skill doc for Foxglove visualization workflow |
| .agents/skills/attach-gossip-payload/SKILL.md | Adds skill doc for adding gossip payloads end-to-end |
Comments suppressed due to low confidence (1)
robot/ros_ws/src/global/planners/random_walk/include/random_walk_node.hpp:40
- This header now includes twice (once newly added and once already present). Please drop the duplicate include to keep the header tidy and avoid redundant work for compilers/unity builds.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Reject points outside the goal's search_bounds polygon (XY footprint | ||
| // in robot-local map). Empty polygon → no-op. | ||
| if (!point_in_search_bounds(std::get<0>(point), std::get<1>(point))) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| std::lock_guard<std::mutex> lock(this->mutex); |
| sub_map_topic: "vdb_map_visualization" | ||
| sub_odometry_topic: "odometry" | ||
| sub_robot_tf_topic: "/tf" | ||
| srv_random_walk_toggle_topic: "~/global_plan_toggle" | ||
|
|
| <depend>rclpy</depend> | ||
| <depend>sensor_msgs</depend> | ||
| <depend>nav_msgs</depend> | ||
| <depend>visualization_msgs</depend> | ||
| <depend>geometry_msgs</depend> | ||
| <depend>builtin_interfaces</depend> | ||
| <depend>tf2_ros</depend> |
| # Use relative path within autonomy_bringup package | ||
| urdf_file = PathJoinSubstitution([ | ||
| FindPackageShare('robot_bringup'), | ||
| FindPackageShare('autonomy_bringup'), | ||
| 'urdf', | ||
| urdf_file_path | ||
| ]) |
| # Current GPS position from interface/mavros/global_position/raw/fix | ||
| # gps_fix.header.stamp carries the NavSatFix timestamp |
| collector = Collector( | ||
| usd_path=source_usd_url, | ||
| collect_dir=output_dir, | ||
| usd_only=False, # include textures, MDLs, etc. | ||
| flat_collection=False, # preserve source folder hierarchy | ||
| flat_collection=True, # preserve source folder hierarchy | ||
| skip_existing=False, |
| # ================= Common =================== | ||
| AUTOLAUNCH="true" # If false, the docker-compose will just spawn idle docker containers with no launch command. | ||
| NUM_ROBOTS="1" # Number of robot containers to launch. | ||
| NUM_ROBOTS="2" # Number of robot containers to launch. | ||
| RECORD_BAGS="false" # "true" or "false" | ||
|
|
||
| # ============== ISAAC SIM ===================== | ||
| ISAAC_SIM_GUI="/isaac-sim/AirStack/simulation/isaac-sim/assets/scenes/simple_pegasus.scene.usd" | ||
| # Set to "true" to launch Isaac Sim using a standalone Python script instead of USD file | ||
| ISAAC_SIM_USE_STANDALONE="true" # "true" or "false" | ||
| # Script name (must be in /AirStack/simulation/isaac-sim/launch_scripts/) | ||
| ISAAC_SIM_SCRIPT_NAME="example_one_px4_pegasus_launch_script.py" | ||
| ISAAC_SIM_SCRIPT_NAME="example_multi_drone_scene_import.py" #"example_one_px4_pegasus_launch_script.py" | ||
| PLAY_SIM_ON_START="false" | ||
|
|
||
| # =============================================== | ||
|
|
||
| # ================= MS-AIRSIM ===================== | ||
| # Do not set if you want airstack to fetch a simple blocks world. | ||
| # MS_AIRSIM_ENV_DIR=./simulation/ms-airsim/assets/scenes | ||
| # MS_AIRSIM_BINARY_PATH="/ms-airsim-env/Blocks/LinuxNoEditor/Blocks.sh" | ||
| # ================================================= | ||
| # =================================================airst | ||
|
|
| og.Controller.Keys.SET_VALUES: [ | ||
| (("inputs:domain_id", nodes["context"]), int(domain_id)), | ||
| (("inputs:cameraPrim", nodes["create_rp"]), camera_prim_path), | ||
| (("inputs:width", nodes["create_rp"]), res), | ||
| (("inputs:height", nodes["create_rp"]), res), | ||
| (("inputs:type", nodes["rgb"]), "rgb"), | ||
| (("inputs:value", nodes["frame"]), str(frame_id)), | ||
| (("inputs:value", nodes["topic"]), str(topic)), | ||
| (("inputs:value", nodes["spec_value"]), float(coverage_m)), | ||
| (("inputs:value", nodes["spec_topic"]), str(spec_topic)), | ||
| (("inputs:messageName", nodes["spec_pub"]), "Float32"), | ||
| (("inputs:messagePackage", nodes["spec_pub"]), "std_msgs"), | ||
| (("inputs:messageSubfolder", nodes["spec_pub"]), "msg"), | ||
| ], |
| period = 1.0 / max(publish_rate, 0.01) | ||
| self._publish_timer = self.create_timer(period, self._publish_tick, clock=ROSClock()) | ||
| self._drain_timer = self.create_timer(0.2, self._drain_peer_inbox, clock=ROSClock()) |
| tests_require=['pytest'], | ||
| entry_points={ | ||
| 'console_scripts': [ | ||
| 'gossip_node = coordination_bringup.gossip_node:main', |
| import math | ||
| import copy | ||
| import struct | ||
|
|
||
| from coordination_bringup.frame_utils import ( | ||
| gps_to_enu as _gps_to_enu_abs, | ||
| heading_to_quat, | ||
| rotate_vector, | ||
| transform_marker_array, | ||
| transform_point_cloud2 as _transform_pc2, | ||
| ) |
| ### Path B — Translate and republish via robot_marker_node | ||
|
|
||
| **File:** `gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/robot_marker_node.py` | ||
|
|
||
| This node auto-discovers robot topics, applies a GPS boot offset to convert from the | ||
| robot's local odom frame to ENU (map frame), and republishes everything as a single | ||
| `/gcs/robot_markers` MarkerArray. |
| ### 4a — Read the payload type from `gossip_payloads.yaml` | ||
|
|
||
| Open `robot/ros_ws/src/coordination/coordination_bringup/config/gossip_payloads.yaml` | ||
| and note the `type:` field for your new entry. This determines how to deserialize it. | ||
|
|
| def main(): | ||
| ap = argparse.ArgumentParser() | ||
| ap.add_argument('--input', default=os.environ.get( | ||
| 'LAYOUT_TEMPLATE', | ||
| '/root/AirStack/gcs/foxglove_extensions/airstack_default.json')) | ||
| ap.add_argument('--output', default=os.environ.get( | ||
| 'LAYOUT_OUTPUT', | ||
| '/root/AirStack/gcs/foxglove_extensions/airstack_default.json')) | ||
| ap.add_argument('--num-robots', type=int, |
| }, true}, | ||
| {"Semantic Search", "tasks/semantic_search", { | ||
| {"query", "string", 0, 0, 0}, | ||
| {"background_queries", "string", 0, 0, 0}, |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
# Conflicts: # simulation/isaac-sim/launch_scripts/two_drone_scene_import.py
Advance to 8e01d013 (main's pointer) which contains spawn_rtx_lidar.py, required by example_one_px4_pegasus_launch_script.py and the multi script after the rtx-lidar update merged from main. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- gossip_node: swap startup log + outgoing-stamp clock to STEADY_TIME so the dedup-by-stamp invariant survives /clock pauses; subscribe to /global_position/global to match foxglove_visualizer and action_relay - gossip_node docstring: drop the false "waypoint triggers immediate publish" claim - coordination README: rename peer_registry node block to the actual per-robot registry topic; "wall-clock" -> "steady" - package.xml: add missing exec/depend rules - coordination_bringup -> autonomy_bringup - autonomy_bringup -> coordination_bringup - desktop_bringup -> coordination_bringup, gcs_visualizer - gcs_visualizer -> std_msgs, coordination_msgs, coordination_bringup - task_msgs: replace TODO license with BSD-3-Clause - gcs.launch.xml: comment had `--no-sandbox` (`--` is illegal inside an XML comment and crashed the ROS launch parser) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… override - payload_visualizer_node: remove unused PointCloud2 / transform_point_cloud2 imports (F401), collapse Marker/MarkerArray - action_relay launch: ROBOT_RELAY_MAP env override for non-default robot_name -> domain mappings (default behavior unchanged) - desktop_bringup robot.rviz: drop BehaviorTreePanel entry pointing at /behavior/behavior_tree_graphviz (publisher package was removed) - autonomy_bringup domain_bridge: bridge /global_position/global to match the dds_router and the rest of the stack (was /raw/fix) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- render_layout: regex now strips every trailing _r<n> (was: only the last one), fixes _r1_r1_r1... stacking on repeated runs - render_layout: atomic write via tmp + os.replace so a partial json.dump doesn't corrupt the layout file - airstack_default.json: re-render with fixed stripper to commit a clean source template (no stacked _r1 suffixes) - install.sh -> install.py: file is Python, shebang is python3 - install.py: slugify publisher into the on-disk extension dir name so "AirLab CMU" doesn't produce a directory with a space - drop robot-commands/robot-commands.foxe (duplicate; canonical is at foxglove_extensions/robot-commands.foxe) and the .foxe.bak Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0c3d4a2 to
3f49b90
Compare
andrewjong
left a comment
There was a problem hiding this comment.
Tested locally, confirmed working. Went back and forth with Krrish.
What does this pull request do?
gcs/foxglove_extensions/):robot-commands(per-robot task tabs with multi-feedback feed),waypoint-editor,polygon-editor. Auto-installed into Foxglove on container start viainstall.sh.gcs/ros_ws/src/action_relay/): bridges Foxglove JSON-on-String goals to per-robot action servers across DDS domains; cancel-aware with 10 s timeout fallback.gcs/ros_ws/src/gcs_visualizer/): multi-robot markers in one 3D namespace, per-robot/gcs/<robot>/locationrepublisher withmapframe, save markers for waypoints/polygons,sim_groundTRIANGLE_LISTfrom the sim overhead camera.common/ros_packages/coordination/): gossip node +PeerProfile+ configurable typed payloads (gossip_payloads.yaml).simulation/isaac-sim/utils/scene_prep.py):add_orthographic_camera+add_overhead_camera_publisher(publishes/sim/overhead/{image,spec});robot/ros_ws/src/global/planners/random_walk/): point-in-polygon search bounds, auto-navigate to nearest inside point pre-flight.gcs/saves/mounted into the GCS container.Which issue number does this address?
Video can be found here
Videos + photos in the updated docs
How did you implement it?
relay_node.py): a single Python process spins up actx0(GCS, domain 0) executor and onectxN(per-robot, domain N) executor; reads JSON goal/cancel from/<robot>/tasks/<task>String topics, builds the typed action goal,forwards to the robot-domain action client, and re-publishes feedback/result as JSON.
NUM_ROBOTSenv spawns the right set of relays.install.sh, which copies each unpacked extension into~/.foxglove-studio/extensions/<publisher>.<name>-<version>/at container start (mounted read-only fromgcs/foxglove_extensions/)./<robot>/odometry_conversion/odometrytopics; per-robot color from a fixed palette; world→map static TF published once on the GCS side.OVERHEAD_ALTITUDE_Mover (0,0); OmniGraphROS2CameraHelperpublishes a JPEG to/sim/overhead/imageand aFloat32coverage spec to/sim/overhead/spec. GCS side downsamples to aMarker.TRIANGLE_LISTgrid (overhead_grid_per_m, capped atoverhead_max_grid_resolution) so it stays cheap in Foxglove's 3D panel.random_walk_logic.cpp::point_in_search_bounds; nearest-inside-point pre-flight dispatched via the navigate action client.Testing
How do you run the tests?
peer_profileterminal viewer.gossip_payloads.yamland a VDB map payload to confirm the gossip layer handles arbitrary user-declared types end-to-end.What do the tests do?
Cover the GCS-side critical paths added by this PR: action relay (goal / feedback / result / cancel), polygon + waypoint editor + saves persistence, multi-robot visualizer, top-down sim map publisher, and the gossip payload pipeline.
What are the expected results of the tests?
gcs/savesmount.sim_groundrenders without dropping frames in Foxglove 3D across all tested scenes.PeerProfilereaches every other robot with all declared payloads attached, regardless of payload type.Reproducing tests
.env:NUM_ROBOTS=4
ISAAC_SIM_SCRIPT_NAME="example_multi_drone_scene_import.py"
Bring up the stack:
The Foxglove window should auto-connect to ws://localhost:8765 and open the AirStack default layout from the CMU org.
Fallback: open the dynamically-generated layout manually
Use this if Foxglove doesn't auto-connect or you want to use the locally-rendered, NUM_ROBOTS-matched layout instead of the cloud one.
- Inside the GCS container: /root/AirStack/gcs/foxglove_extensions/airstack_default.json
- On the host: gcs/foxglove_extensions/airstack_default.json
- ws://localhost:8765 if Foxglove is running inside the GCS container
- ws://localhost:8766 if Foxglove is running on the host
Did you update the docs (and under what path)?
New pages:
docs/gcs/foxglove.md— GCS Foxglove visualization (multi-robot markers, payload republishing, sim ground)docs/gcs/waypoints_and_geofences.md— click-to-place Waypoint and Polygon editor workflow + saves persistencedocs/simulation/isaac_sim/spawning_drones.md— multi-drone scene authoring withgps_utils.set_gps_originsdocs/simulation/isaac_sim/overhead_camera.md— 2D world map in Foxglove (real-world satellite tiles + simulated overheadcamera)
Updated:
docs/robot/autonomy/coordination/index.md— gossip protocol overview (already wired in nav)docs/robot/autonomy/coordination/payloads.md— how to add a custom payload + Foxglove handlermkdocs.yml— nav entries for the four new pages, underGround Control Station → FoxgloveandSimulation → Isaac SimSkills (
.agents/skills/):attach-gossip-payload/— agent workflow for adding a payload end-to-endvisualize-in-foxglove/— agent workflow for adding a topic to the GCS visualizer