diff --git a/.gitignore b/.gitignore
index 28b0c9f..5f94900 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,4 +75,5 @@ bld/
# NUNIT
*.VisualState.xml
-TestResult.xml
\ No newline at end of file
+TestResult.xml__pycache__/
+*.pyc
diff --git a/BuildInstructions.md b/BuildInstructions.md
index 556c930..31c0765 100644
--- a/BuildInstructions.md
+++ b/BuildInstructions.md
@@ -10,13 +10,16 @@ cd ~/waybionic_ws
```
## Build and Launch
-- Run these commands in your workspace:
+- Run these commands from the root of your workspace (`~/waybionic_ws`) to install dependencies and build the foundation:
```
source /opt/ros/jazzy/setup.bash
-rosdep install --from-paths . --ignore-src -r -y
-colcon build --packages-select waybionic_description waybionic_bringup
+rosdep install --from-paths src --ignore-src -r -y
+colcon build
source install/setup.bash
-ros2 launch waybionic_bringup display.launch.py
+```
+- Launch ground station using:
+```
+ros2 launch waybionic_bringup ground_station.launch.py
```
- **RViz** and **Joint State Publisher GUI** (separate small window) will pop up after the last command
- RViz opens pre-configured with `base_link` fixed frame, `RobotModel`, and `TF` displays already loaded
diff --git a/scripts/check_waybionic_ws.sh b/scripts/check_waybionic_ws.sh
index da07b4b..4a2ea32 100755
--- a/scripts/check_waybionic_ws.sh
+++ b/scripts/check_waybionic_ws.sh
@@ -16,7 +16,7 @@ echo "2. Checking dependencies with rosdep..."
rosdep install --from-paths src --ignore-src -r -y
echo "3. Building Waybionic packages..."
-colcon build --packages-select waybionic_description waybionic_bringup
+colcon build
echo "4. Sourcing workspace..."
source install/setup.bash
diff --git a/waybionic_bringup/CMakeLists.txt b/waybionic_bringup/CMakeLists.txt
index 962f006..fba831a 100644
--- a/waybionic_bringup/CMakeLists.txt
+++ b/waybionic_bringup/CMakeLists.txt
@@ -13,14 +13,11 @@ find_package(ament_cmake REQUIRED)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
- # the following line skips the linter which checks for copyrights
- # comment the line when a copyright and license is added to all source files
+ find_package(launch_testing_ament_cmake REQUIRED)
set(ament_cmake_copyright_FOUND TRUE)
- # the following line skips cpplint (only works in a git repo)
- # comment the line when this package is in a git repo and when
- # a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
+ add_launch_test(test/test_ground_station_launch.py)
endif()
install(DIRECTORY launch
diff --git a/waybionic_bringup/launch/ground_station.launch.py b/waybionic_bringup/launch/ground_station.launch.py
new file mode 100644
index 0000000..2183b8b
--- /dev/null
+++ b/waybionic_bringup/launch/ground_station.launch.py
@@ -0,0 +1,112 @@
+import os
+
+from ament_index_python.packages import get_package_share_directory
+
+from launch import LaunchDescription
+from launch.actions import DeclareLaunchArgument, OpaqueFunction
+from launch.conditions import IfCondition
+from launch.substitutions import Command, LaunchConfiguration
+from launch_ros.actions import Node
+
+
+def check_files_exist(context, *args, **kwargs):
+ model_path = LaunchConfiguration('model').perform(context)
+ rviz_path = LaunchConfiguration('rvizconfig').perform(context)
+ if not os.path.exists(model_path):
+ raise FileNotFoundError(f'Model file not found: {model_path}')
+ if not os.path.exists(rviz_path):
+ raise FileNotFoundError(f'RViz config file not found: {rviz_path}')
+ return []
+
+
+def generate_launch_description():
+ waybionic_desc_dir = get_package_share_directory('waybionic_description')
+ waybionic_bringup_dir = get_package_share_directory('waybionic_bringup')
+
+ default_model_path = os.path.join(
+ waybionic_desc_dir, 'urdf', 'waybionic_placeholder.urdf')
+ default_rviz_config_path = os.path.join(
+ waybionic_bringup_dir, 'rviz', 'waybionic_unified.rviz')
+
+ # --- Declare Launch Arguments ---
+ model_arg = DeclareLaunchArgument(
+ 'model', default_value=default_model_path,
+ description='Absolute path to robot urdf')
+
+ use_mock_diag_arg = DeclareLaunchArgument(
+ 'use_mock_diagnostics', default_value='true',
+ description='Panel mode: true for internal mock, false for live topics')
+
+ diag_topic_arg = DeclareLaunchArgument(
+ 'diagnostics_topic', default_value='/diagnostics',
+ description='Diagnostics topic name')
+
+ start_temp_pub_arg = DeclareLaunchArgument(
+ 'start_temporary_diagnostics_publisher', default_value='false',
+ description='Start the optional temporary publisher for live demo')
+
+ use_jsp_gui_arg = DeclareLaunchArgument(
+ 'use_joint_state_publisher_gui', default_value='true',
+ description='Launch joint state publisher GUI')
+
+ use_sim_time_arg = DeclareLaunchArgument(
+ 'use_sim_time', default_value='false',
+ description='Use simulation time')
+
+ launch_rviz_arg = DeclareLaunchArgument(
+ 'launch_rviz', default_value='true',
+ description='Launch RViz (set false for headless validation)')
+
+ rviz_config_arg = DeclareLaunchArgument(
+ 'rvizconfig', default_value=default_rviz_config_path,
+ description='Absolute path to rviz config file')
+
+ file_check = OpaqueFunction(function=check_files_exist)
+
+ # --- Nodes ---
+ robot_description_content = {
+ 'robot_description': Command(['xacro ', LaunchConfiguration('model')])
+ }
+
+ rsp_node = Node(
+ package='robot_state_publisher', executable='robot_state_publisher',
+ parameters=[
+ robot_description_content,
+ {'use_sim_time': LaunchConfiguration('use_sim_time')}
+ ]
+ )
+
+ jsp_gui_node = Node(
+ package='joint_state_publisher_gui', executable='joint_state_publisher_gui',
+ condition=IfCondition(LaunchConfiguration('use_joint_state_publisher_gui'))
+ )
+
+ # Pass the correct arguments to the temporary publisher
+ temp_diag_pub_node = Node(
+ package='waybionic_rviz_plugins', executable='temporary_diagnostics_publisher.py',
+ name='temp_diag_pub',
+ condition=IfCondition(LaunchConfiguration('start_temporary_diagnostics_publisher')),
+ parameters=[
+ {'mode': 'normal'},
+ {'topic': LaunchConfiguration('diagnostics_topic')},
+ {'publish_rate_hz': 10.0}
+ ]
+ )
+
+ # Pass the mock toggles into the RViz node parameters
+ rviz_node = Node(
+ package='rviz2', executable='rviz2', name='rviz2', output='screen',
+ arguments=['-d', LaunchConfiguration('rvizconfig')],
+ condition=IfCondition(LaunchConfiguration('launch_rviz')),
+ parameters=[
+ {'use_sim_time': LaunchConfiguration('use_sim_time')},
+ {'use_mock_diagnostics': LaunchConfiguration('use_mock_diagnostics')},
+ {'diagnostics_topic': LaunchConfiguration('diagnostics_topic')}
+ ]
+ )
+
+ return LaunchDescription([
+ model_arg, use_mock_diag_arg, diag_topic_arg, start_temp_pub_arg,
+ use_jsp_gui_arg, use_sim_time_arg, launch_rviz_arg, rviz_config_arg,
+ file_check, rsp_node, jsp_gui_node, temp_diag_pub_node, rviz_node
+ ])
diff --git a/waybionic_bringup/package.xml b/waybionic_bringup/package.xml
index bb3772f..f262ccb 100644
--- a/waybionic_bringup/package.xml
+++ b/waybionic_bringup/package.xml
@@ -12,6 +12,7 @@
ament_index_python
xacro
waybionic_description
+ waybionic_rviz_plugins
launch
launch_ros
robot_state_publisher
@@ -20,6 +21,7 @@
ament_lint_auto
ament_lint_common
+ launch_testing_ament_cmake
ament_cmake
diff --git a/waybionic_bringup/rviz/waybionic_unified.rviz b/waybionic_bringup/rviz/waybionic_unified.rviz
new file mode 100644
index 0000000..135994e
--- /dev/null
+++ b/waybionic_bringup/rviz/waybionic_unified.rviz
@@ -0,0 +1,199 @@
+Panels:
+ - Class: rviz_common/Displays
+ Help Height: 70
+ Name: Displays
+ Property Tree Widget:
+ Expanded:
+ - /Global Options1
+ - /Status1
+ - /RobotModel1
+ - /RobotModel1/Description Topic1
+ Splitter Ratio: 0.5
+ Tree Height: 70
+ - Class: rviz_common/Selection
+ Name: Selection
+ - Class: rviz_common/Tool Properties
+ Expanded:
+ - /2D Goal Pose1
+ - /Publish Point1
+ Name: Tool Properties
+ Splitter Ratio: 0.5886790156364441
+ - Class: rviz_common/Views
+ Expanded:
+ - /Current View1
+ Name: Views
+ Splitter Ratio: 0.5
+ - Class: rviz_common/Time
+ Experimental: false
+ Name: Time
+ SyncMode: 0
+ SyncSource: ""
+ - Class: waybionic_rviz_plugins/DiagnosticsPanel
+ Diagnostics Topic: /diagnostics
+ Name: DiagnosticsPanel
+ Use Mock Diagnostics: true
+Visualization Manager:
+ Class: ""
+ Displays:
+ - Alpha: 0.5
+ Cell Size: 1
+ Class: rviz_default_plugins/Grid
+ Color: 160; 160; 164
+ Enabled: true
+ Line Style:
+ Line Width: 0.029999999329447746
+ Value: Lines
+ Name: Grid
+ Normal Cell Count: 0
+ Offset:
+ X: 0
+ Y: 0
+ Z: 0
+ Plane: XY
+ Plane Cell Count: 10
+ Reference Frame:
+ Value: true
+ - Alpha: 1
+ Class: rviz_default_plugins/RobotModel
+ Collision Enabled: false
+ Description File: ""
+ Description Source: Topic
+ Description Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /robot_description
+ Enabled: true
+ Links:
+ All Links Enabled: true
+ Expand Joint Details: false
+ Expand Link Details: false
+ Expand Tree: false
+ Link Tree Style: Links in Alphabetic Order
+ arm_link:
+ Alpha: 1
+ Show Axes: false
+ Show Trail: false
+ Value: true
+ base_link:
+ Alpha: 1
+ Show Axes: false
+ Show Trail: false
+ Value: true
+ Mass Properties:
+ Inertia: false
+ Mass: false
+ Name: RobotModel
+ TF Prefix: ""
+ Update Interval: 0
+ Value: true
+ Visual Enabled: true
+ - Class: rviz_default_plugins/TF
+ Enabled: true
+ Filter (blacklist): ""
+ Filter (whitelist): ""
+ Frame Timeout: 15
+ Frames:
+ All Enabled: true
+ arm_link:
+ Value: true
+ base_link:
+ Value: true
+ Marker Scale: 1
+ Name: TF
+ Show Arrows: true
+ Show Axes: true
+ Show Names: false
+ Tree:
+ base_link:
+ arm_link:
+ {}
+ Update Interval: 0
+ Value: true
+ Enabled: true
+ Global Options:
+ Background Color: 48; 48; 48
+ Fixed Frame: base_link
+ Frame Rate: 30
+ Name: root
+ Tools:
+ - Class: rviz_default_plugins/Interact
+ Hide Inactive Objects: true
+ - Class: rviz_default_plugins/MoveCamera
+ - Class: rviz_default_plugins/Select
+ - Class: rviz_default_plugins/FocusCamera
+ - Class: rviz_default_plugins/Measure
+ Line color: 128; 128; 0
+ - Class: rviz_default_plugins/SetInitialPose
+ Covariance x: 0.25
+ Covariance y: 0.25
+ Covariance yaw: 0.06853891909122467
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /initialpose
+ - Class: rviz_default_plugins/SetGoal
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /goal_pose
+ - Class: rviz_default_plugins/PublishPoint
+ Single click: true
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /clicked_point
+ Transformation:
+ Current:
+ Class: rviz_default_plugins/TF
+ Value: true
+ Views:
+ Current:
+ Class: rviz_default_plugins/Orbit
+ Distance: 1.5597326755523682
+ Enable Stereo Rendering:
+ Stereo Eye Separation: 0.05999999865889549
+ Stereo Focal Distance: 1
+ Swap Stereo Eyes: false
+ Value: false
+ Focal Point:
+ X: 0
+ Y: 0
+ Z: 0
+ Focal Shape Fixed Size: true
+ Focal Shape Size: 0.05000000074505806
+ Invert Z Axis: false
+ Name: Current View
+ Near Clip Distance: 0.009999999776482582
+ Pitch: 0.785398006439209
+ Target Frame:
+ Value: Orbit (rviz)
+ Yaw: 0.785398006439209
+ Saved: ~
+Window Geometry:
+ DiagnosticsPanel:
+ collapsed: false
+ Displays:
+ collapsed: false
+ Height: 2055
+ Hide Left Dock: false
+ Hide Right Dock: false
+ QMainWindow State: 000000ff00000000fd0000000400000000000001a400000774fc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b000000c7000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb000000200044006900610067006e006f0073007400690063007300500061006e0065006c0100000108000002330000023400fffffffb000000200044006900610067006e006f0073007400690063007300500061006e0065006c0100000341000002340000023400fffffffb000000200044006900610067006e006f0073007400690063007300500061006e0065006c010000057b000002340000023400ffffff000000010000010f00000774fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003b00000774000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000002ea00000037fc0100000002fb0000000800540069006d00650100000000000002ea0000025300fffffffb0000000800540069006d006501000000000000045000000000000000000000002b0000077500000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
+ Selection:
+ collapsed: false
+ Time:
+ collapsed: false
+ Tool Properties:
+ collapsed: false
+ Views:
+ collapsed: false
+ Width: 746
+ X: 57
+ Y: 4
diff --git a/waybionic_bringup/test/test_ground_station_launch.py b/waybionic_bringup/test/test_ground_station_launch.py
new file mode 100644
index 0000000..cee03d3
--- /dev/null
+++ b/waybionic_bringup/test/test_ground_station_launch.py
@@ -0,0 +1,44 @@
+import os
+import unittest
+
+from ament_index_python.packages import get_package_share_directory
+import launch
+from launch.actions import IncludeLaunchDescription
+from launch.launch_description_sources import PythonLaunchDescriptionSource
+import launch_testing
+import launch_testing.actions
+import pytest
+
+
+@pytest.mark.launch_test
+def generate_test_description():
+ bringup_dir = get_package_share_directory('waybionic_bringup')
+ launch_file = os.path.join(bringup_dir, 'launch', 'ground_station.launch.py')
+
+ ground_station_launch = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(launch_file),
+ launch_arguments={
+ 'launch_rviz': 'false',
+ 'use_joint_state_publisher_gui': 'false',
+ 'start_temporary_diagnostics_publisher': 'true'
+ }.items()
+ )
+
+ return launch.LaunchDescription([
+ ground_station_launch,
+ launch_testing.actions.ReadyToTest()
+ ])
+
+
+class TestGroundStationLaunch(unittest.TestCase):
+
+ def test_nodes_started(self, proc_info, proc_output):
+ proc_info.assertWaitForStartup(process=None, timeout=5)
+ assert len(proc_info.processes()) > 0, 'No processes were started!'
+
+
+@launch_testing.post_shutdown_test()
+class TestProcessOutput(unittest.TestCase):
+
+ def test_exit_codes(self, proc_info):
+ launch_testing.asserts.assertExitCodes(proc_info, allowable_exit_codes=[0, -2])