From de50d499bba713e53e85e038ef251fb27cd1b6e1 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Mon, 27 Apr 2026 22:11:19 -0400 Subject: [PATCH 01/16] Turns on warnings as errors for radio bridge package --- radio/ateam_radio_bridge/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/ateam_radio_bridge/CMakeLists.txt b/radio/ateam_radio_bridge/CMakeLists.txt index cdffff551..bc981b4fa 100644 --- a/radio/ateam_radio_bridge/CMakeLists.txt +++ b/radio/ateam_radio_bridge/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.8) project(ateam_radio_bridge) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) + add_compile_options(-Wall -Wextra -Wpedantic -Werror) endif() find_package(ament_cmake REQUIRED) From 5cb97e5b5c48aab76a283c307380b3f0aa891218 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Mon, 27 Apr 2026 22:12:45 -0400 Subject: [PATCH 02/16] Adds connection states, with initial connect timeout --- .../src/radio_bridge_node.cpp | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/radio/ateam_radio_bridge/src/radio_bridge_node.cpp b/radio/ateam_radio_bridge/src/radio_bridge_node.cpp index 5c0508be0..7da95b046 100644 --- a/radio/ateam_radio_bridge/src/radio_bridge_node.cpp +++ b/radio/ateam_radio_bridge/src/radio_bridge_node.cpp @@ -50,12 +50,21 @@ using namespace std::string_literals; namespace ateam_radio_bridge { +enum class ConnectionState +{ + Disconnected, + Connecting, + Connected, + ReadyToClose +}; + class RadioBridgeNode : public rclcpp::Node { public: RadioBridgeNode(const rclcpp::NodeOptions & options) : rclcpp::Node("radio_bridge", options), - timeout_threshold_(declare_parameter("timeout_ms", 250)), + sustain_timeout_threshold_(declare_parameter("sustain_timeout_ms", 250)), + connect_timeout_threshold_(declare_parameter("connect_timeout_ms", 750)), command_timeout_threshold_(declare_parameter("command_timeout_ms", 100)), game_controller_listener_(*this, std::bind_front(&RadioBridgeNode::TeamColorChangeCallback, this)), @@ -68,7 +77,7 @@ class RadioBridgeNode : public rclcpp::Node { std::fill(shutdown_requested_.begin(), shutdown_requested_.end(), false); std::fill(reboot_requested_.begin(), reboot_requested_.end(), false); - std::fill(goodbye_received_.begin(), goodbye_received_.end(), false); + std::fill(connection_states_.begin(), connection_states_.end(), ConnectionState::Disconnected); declare_parameters("controls_enabled", { {"body_vel", true}, @@ -122,14 +131,18 @@ class RadioBridgeNode : public rclcpp::Node } private: - const std::chrono::milliseconds timeout_threshold_; + // Incoming timeouts + const std::chrono::milliseconds sustain_timeout_threshold_; + const std::chrono::milliseconds connect_timeout_threshold_; + + // Outgoing timeouts const std::chrono::milliseconds command_timeout_threshold_; + std::mutex mutex_; std::array motion_commands_; std::array motion_command_timestamps_; std::array shutdown_requested_; std::array reboot_requested_; - std::array goodbye_received_; ateam_common::GameControllerListener game_controller_listener_; std::array::SharedPtr, 16> motion_command_subscriptions_; @@ -143,7 +156,8 @@ class RadioBridgeNode : public rclcpp::Node FirmwareParameterServer firmware_parameter_server_; rclcpp::Service::SharedPtr power_request_service_; std::array, 16> connections_; - std::array last_heartbeat_timestamp_; + std::array last_heartbeat_timestamps_; + std::array connection_states_; rclcpp::TimerBase::SharedPtr connection_check_timer_; rclcpp::TimerBase::SharedPtr command_send_timer_; @@ -173,6 +187,7 @@ class RadioBridgeNode : public rclcpp::Node { std::lock_guard lock(mutex_); connections_.at(connection_index).swap(connection); + connection_states_[connection_index] = ConnectionState::Disconnected; } if(!connection) { // Connection already closed @@ -206,22 +221,22 @@ class RadioBridgeNode : public rclcpp::Node connection_publishers_[i]->publish(connection_message); shutdown_requested_[i] = false; reboot_requested_[i] = false; - goodbye_received_[i] = false; + connection_states_[i] = ConnectionState::Disconnected; continue; } - const auto & last_heartbeat_time = last_heartbeat_timestamp_[i]; + const auto & last_heartbeat_time = last_heartbeat_timestamps_[i]; const auto time_since_heartbeat = std::chrono::steady_clock::now() - last_heartbeat_time; - if (time_since_heartbeat > timeout_threshold_) { + const auto effective_timeout = connection_states_[i] == ConnectionState::Connected ? + sustain_timeout_threshold_ : connect_timeout_threshold_; + if (time_since_heartbeat > effective_timeout) { RCLCPP_WARN(get_logger(), "Connection to robot %ld timed out.", i); // release lock early so CloseConnection can grab it lock.unlock(); CloseConnection(i); } - if(goodbye_received_[i]) { - RCLCPP_INFO(get_logger(), "Received goodbye from robot %ld.", i); + if(connection_states_[i] == ConnectionState::ReadyToClose) { // release lock early so CloseConnection can grab it lock.unlock(); - // don't send goodbye because we already received one from the robot CloseConnection(i, false); } // lock released by destructor @@ -340,7 +355,8 @@ class RadioBridgeNode : public rclcpp::Node sender_address.c_str(), sender_port); motion_command_timestamps_[robot_id] = {}; - last_heartbeat_timestamp_[robot_id] = std::chrono::steady_clock::now(); + last_heartbeat_timestamps_[robot_id] = std::chrono::steady_clock::now(); + connection_states_[robot_id] = ConnectionState::Connecting; connections_[hello_data.robot_id] = std::make_unique( sender_address, sender_port, std::bind( @@ -376,11 +392,12 @@ class RadioBridgeNode : public rclcpp::Node switch (packet.command_code) { case CC_GOODBYE: // close connection. No need to send our own goodbye - goodbye_received_[robot_id] = true; + RCLCPP_INFO(get_logger(), "Received goodbye from robot %d.", robot_id); + connection_states_[robot_id] = ConnectionState::ReadyToClose; break; case CC_TELEMETRY: { - last_heartbeat_timestamp_[robot_id] = std::chrono::steady_clock::now(); + OnHeartbeatQualifiedMessageReceived(robot_id); const auto data_var = ExtractData(packet, error); if (!error.empty()) { RCLCPP_WARN(get_logger(), "Ignoring basic telemetry message from robot %d. %s", robot_id, error.c_str()); @@ -424,7 +441,7 @@ class RadioBridgeNode : public rclcpp::Node break; } case CC_KEEPALIVE: - last_heartbeat_timestamp_[robot_id] = std::chrono::steady_clock::now(); + OnHeartbeatQualifiedMessageReceived(robot_id); break; default: RCLCPP_WARN( @@ -484,6 +501,13 @@ class RadioBridgeNode : public rclcpp::Node response->success = true; } + void OnHeartbeatQualifiedMessageReceived(int robot_id) { + last_heartbeat_timestamps_[robot_id] = std::chrono::steady_clock::now(); + if(connection_states_[robot_id] == ConnectionState::Connecting) { + connection_states_[robot_id] = ConnectionState::Connected; + } + } + }; } // namespace ateam_radio_bridge From 7282673388c66c1fc22b0b44541af2b28f8fa014 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Mon, 27 Apr 2026 23:01:31 -0400 Subject: [PATCH 03/16] Adds exit code assertion to launch tests --- .../test/launch_tests/bridge_command_test.py | 5 +++++ .../test/launch_tests/bridge_discovery_test.py | 5 +++++ .../test/launch_tests/bridge_feedback_test.py | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/radio/ateam_radio_bridge/test/launch_tests/bridge_command_test.py b/radio/ateam_radio_bridge/test/launch_tests/bridge_command_test.py index 3b98a39f5..a98bacc40 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/bridge_command_test.py +++ b/radio/ateam_radio_bridge/test/launch_tests/bridge_command_test.py @@ -91,3 +91,8 @@ def test_commands(self): if abs(vel_x_linear - 2.0) < 0.1: # Pass the test return + +@launch_testing.post_shutdown_test() +class TestProcessExit(unittest.TestCase): + def test_exit_codes(self, proc_info): + launch_testing.asserts.assertExitCodes(proc_info) diff --git a/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py b/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py index e5e509b6b..499efb633 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py +++ b/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py @@ -79,3 +79,8 @@ def test_discoveryResponse(self): "Hello response should not hold \ port 0", ) + +@launch_testing.post_shutdown_test() +class TestProcessExit(unittest.TestCase): + def test_exit_codes(self, proc_info): + launch_testing.asserts.assertExitCodes(proc_info) diff --git a/radio/ateam_radio_bridge/test/launch_tests/bridge_feedback_test.py b/radio/ateam_radio_bridge/test/launch_tests/bridge_feedback_test.py index 976ab52af..7bcf142e3 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/bridge_feedback_test.py +++ b/radio/ateam_radio_bridge/test/launch_tests/bridge_feedback_test.py @@ -77,3 +77,8 @@ def test_feedback(self): # Just checks a few fields to make sure it's a reasonably valid message self.assertEqual(message.sequence_number, 1) self.assertAlmostEqual(message.battery_percent, 100) + +@launch_testing.post_shutdown_test() +class TestProcessExit(unittest.TestCase): + def test_exit_codes(self, proc_info): + launch_testing.asserts.assertExitCodes(proc_info) From 6759376adab379348ffb1ccb19f547575c327b0e Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Mon, 27 Apr 2026 23:01:49 -0400 Subject: [PATCH 04/16] Adds goodbye launch test --- .../test/launch_tests/CMakeLists.txt | 4 ++ .../test/launch_tests/bridge_goodbye_test.py | 69 +++++++++++++++++++ .../test/launch_tests/mock_robot.py | 23 +++++++ 3 files changed, 96 insertions(+) create mode 100644 radio/ateam_radio_bridge/test/launch_tests/bridge_goodbye_test.py diff --git a/radio/ateam_radio_bridge/test/launch_tests/CMakeLists.txt b/radio/ateam_radio_bridge/test/launch_tests/CMakeLists.txt index 74dd619f5..e4028ff68 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/CMakeLists.txt +++ b/radio/ateam_radio_bridge/test/launch_tests/CMakeLists.txt @@ -11,6 +11,10 @@ add_launch_test(bridge_command_test.py APPEND_ENV PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR} TIMEOUT 10 ) +add_launch_test(bridge_goodbye_test.py + APPEND_ENV PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR} + TIMEOUT 10 +) install(PROGRAMS mock_robot.py diff --git a/radio/ateam_radio_bridge/test/launch_tests/bridge_goodbye_test.py b/radio/ateam_radio_bridge/test/launch_tests/bridge_goodbye_test.py new file mode 100644 index 000000000..d2ba56b4b --- /dev/null +++ b/radio/ateam_radio_bridge/test/launch_tests/bridge_goodbye_test.py @@ -0,0 +1,69 @@ +"""Tests the bridge's ability to handle goodbye packets from the robot.""" + +import time +import unittest + +import launch +from launch.actions import TimerAction + +import launch_ros.actions + +import launch_testing + +from mock_robot import MockRobot + +import pytest + + +discovery_address = "224.4.20.70" +discovery_port = 42069 + + +@pytest.mark.rostest +def generate_test_description(): + return ( + launch.LaunchDescription( + [ + launch_ros.actions.Node( + package="ateam_radio_bridge", + executable="radio_bridge_node", + parameters=[ + {"discovery_address": discovery_address}, + {"default_team_color": "yellow"}, + {"net_interface_address": ""}, + ], + ), + TimerAction(period=0.1, actions=[launch_testing.actions.ReadyToTest()]), + ] + ), + locals(), + ) + + +class TestRadioBridgeNode(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.robot = MockRobot( + discovery_address=discovery_address, discovery_port=discovery_port + ) + cls.robot.startAsync() + + @classmethod + def tearDownClass(cls): + cls.robot.stopAsync() + + def test_feedback(self): + connect_timeout = time.time() + 1 + while not self.robot.isConnected() and time.time() < connect_timeout: + time.sleep(0.1) + self.assertTrue(self.robot.isConnected()) + + # Let connection run for a bit + time.sleep(0.1) + + self.robot.sendGoodbyeAndShutdown() + +@launch_testing.post_shutdown_test() +class TestProcessExit(unittest.TestCase): + def test_exit_codes(self, proc_info): + launch_testing.asserts.assertExitCodes(proc_info) diff --git a/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py b/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py index 2a462d525..1c30882d3 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py +++ b/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py @@ -3,6 +3,7 @@ import socket import struct +import time from threading import Thread @@ -18,6 +19,7 @@ def __init__( self._last_cmd_packet = None self._robot_id = robot_id self._color = color + self._goodbye_requested = False self._async_running = False self._async_thread = None self._bridge_endpoint = ('', 0) @@ -31,12 +33,22 @@ def isConnected(self) -> bool: def getLastCmdMessage(self) -> bytes: return self._last_cmd_packet + def sendGoodbyeAndShutdown(self) -> None: + if not self._connected: + return + self._goodbye_requested = True + time.sleep(0.1) + self.stopAsync() + def run(self, run_async=False): while True: if run_async and not self._async_running: return if not self._connected: self._runDiscovery() + elif self._goodbye_requested: + self._sendGoodbyePacket() + self._goodbye_requested = False else: self._runConnected() @@ -96,6 +108,17 @@ def _sendTelemetryPacket(self): 100, # Kicker Charge Percent ) self._socket.sendto(packet, self._bridge_endpoint) + + def _sendGoodbyePacket(self): + packet = struct.pack( + 'IHHBH', + 0, # CRC + 0, # Version Major + 1, # Version Minor + 3, # CC Goodbye, + 0, # Data Length + ) + self._socket.sendto(packet, self._bridge_endpoint) def _runConnected(self): try: From a42af944415b5f4003cf0007eda4733c4ae80bec Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Wed, 29 Apr 2026 00:25:52 -0400 Subject: [PATCH 05/16] Updates software-comms submodule --- radio/ateam_radio_msgs/software-communication | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/ateam_radio_msgs/software-communication b/radio/ateam_radio_msgs/software-communication index 0fc77eee7..b969e32db 160000 --- a/radio/ateam_radio_msgs/software-communication +++ b/radio/ateam_radio_msgs/software-communication @@ -1 +1 @@ -Subproject commit 0fc77eee7b20b4113e452816be8dd7bc42609f21 +Subproject commit b969e32db321430eb058c72444e161ec3ae7e084 From 431d08e5fadd9312042711ad3a4ca50fe444d59b Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Wed, 29 Apr 2026 00:59:21 -0400 Subject: [PATCH 06/16] Imports build changes needed to support new packet language features. --- radio/ateam_radio_msgs/CMakeLists.txt | 22 +- .../cmake/generate_msgs.cmake | 6 + .../scripts/generate_conversion_code.py | 191 +++++++++++++++--- .../ateam_radio_msgs/scripts/generate_msgs.py | 89 +++++++- 4 files changed, 261 insertions(+), 47 deletions(-) diff --git a/radio/ateam_radio_msgs/CMakeLists.txt b/radio/ateam_radio_msgs/CMakeLists.txt index d93bc9f4b..1e31c1905 100644 --- a/radio/ateam_radio_msgs/CMakeLists.txt +++ b/radio/ateam_radio_msgs/CMakeLists.txt @@ -17,10 +17,20 @@ set(GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/ateam_generated) set(RADIO_PACKETS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/software-communication/ateam-common-packets/include) set(RADIO_STRUCTS_TO_GENERATE + GlobalPositionCommand + GlobalVelocityCommand + LocalVelocityCommand + GlobalAccelerationCommand + LocalAccelerationCommand BasicTelemetry ExtendedTelemetry + BodyControlTelemetry + BodyControlExtendedTelemetry KickerTelemetry MotorTelemetry + CcmTelemetry + CcmVelocityTelemetry + CcmCurrentTelemetry PowerTelemetry BatteryInfo ) @@ -42,12 +52,7 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CONFIG_DEPENDS} set(MSG_TARGET ${PROJECT_NAME}) rosidl_generate_interfaces(${MSG_TARGET} - ${GENERATED_DIR}:msg/BasicTelemetry.msg - ${GENERATED_DIR}:msg/ExtendedTelemetry.msg - ${GENERATED_DIR}:msg/KickerTelemetry.msg - ${GENERATED_DIR}:msg/MotorTelemetry.msg - ${GENERATED_DIR}:msg/PowerTelemetry.msg - ${GENERATED_DIR}:msg/BatteryInfo.msg + ${GENERATED_MSG_TUPLES} msg/ConnectionStatus.msg @@ -82,9 +87,8 @@ install( DIRECTORY ${GENERATED_DIR}/include/ DESTINATION include/ateam_radio_msgs/ateam_radio_msgs ) -file(GLOB SYMLINKED_INCLUDES include/ateam_radio_msgs/packets/*) install( - FILES ${SYMLINKED_INCLUDES} + DIRECTORY ${RADIO_PACKETS_INCLUDE_DIR} DESTINATION include/ateam_radio_msgs/ateam_radio_msgs/packets ) install( @@ -98,7 +102,7 @@ install( if(BUILD_TESTING) file(GLOB_RECURSE submodule_files software-communication/*) - file(GLOB symlinked_headers include/ateam_radio_msgs/packets/*) + file(GLOB_RECURSE symlinked_headers include/ateam_radio_msgs/packets/*) set(_linter_excludes software-communication ${submodule_files} diff --git a/radio/ateam_radio_msgs/cmake/generate_msgs.cmake b/radio/ateam_radio_msgs/cmake/generate_msgs.cmake index 294bf44f8..2036da417 100644 --- a/radio/ateam_radio_msgs/cmake/generate_msgs.cmake +++ b/radio/ateam_radio_msgs/cmake/generate_msgs.cmake @@ -44,6 +44,12 @@ function(generate_msgs) list(APPEND generated_msgs_files "${arg_DESTINATION}/${struct}.msg") endforeach() + set(GENERATED_MSG_TUPLES "") + foreach(_msg ${generated_msgs_files}) + string(REPLACE "${arg_DESTINATION}/" "" _rel "${_msg}") + list(APPEND GENERATED_MSG_TUPLES "${arg_DESTINATION}:${_rel}") + endforeach() + execute_process( COMMAND python3 ${_generate_msgs_script} ${arg_DESTINATION} ${arg_SOURCE} ${arg_STRUCTS} RESULT_VARIABLE result diff --git a/radio/ateam_radio_msgs/scripts/generate_conversion_code.py b/radio/ateam_radio_msgs/scripts/generate_conversion_code.py index a1c907826..def5cf419 100644 --- a/radio/ateam_radio_msgs/scripts/generate_conversion_code.py +++ b/radio/ateam_radio_msgs/scripts/generate_conversion_code.py @@ -28,15 +28,69 @@ import clang.cindex +def _is_elaborated_union(field_node): + """Return True if the field's type is a typedef'd union.""" + if field_node.type.kind != clang.cindex.TypeKind.ELABORATED: + return False + # get_declaration() on a typedef'd union returns TYPEDEF_DECL, not UNION_DECL. + # The canonical type strips the typedef layer and gives us the underlying record. + canon = field_node.type.get_canonical() + return ( + canon.kind == clang.cindex.TypeKind.RECORD + and canon.get_declaration().kind == clang.cindex.CursorKind.UNION_DECL + ) + + +def collect_union_member_types(file_path, struct_names): + """Return a list of struct type names used as union members inside struct_names types.""" + index = clang.cindex.Index.create() + tu = index.parse(file_path) + extras = [] + struct_like = (clang.cindex.CursorKind.STRUCT_DECL, clang.cindex.CursorKind.UNION_DECL) + for node in tu.cursor.get_children(): + if node.location.file is None or node.location.file.name != file_path: + continue + if node.kind not in struct_like or node.spelling not in struct_names: + continue + for field in node.get_children(): + if field.kind != clang.cindex.CursorKind.FIELD_DECL: + continue + if not _is_elaborated_union(field): + continue + for member in field.type.get_canonical().get_declaration().get_children(): + if member.kind == clang.cindex.CursorKind.FIELD_DECL: + extras.append(member.type.spelling) + return extras + + def generate_conversion_code(output_directory, header_file, struct_names): """Generate conversion functions.""" + include_root = pathlib.Path(header_file).parent index = clang.cindex.Index.create() - translation_unit = index.parse(header_file) - generate_header_file(output_directory, translation_unit, struct_names) - generate_implementation_file(output_directory, translation_unit, struct_names) + header_tus = [ + (h, index.parse(str(h))) for h in sorted(include_root.rglob('*.h')) + ] + # Expand struct_names with types used as union members so Convert() functions are + # generated for them — they aren't in the original list but each needs its own + # converter because ROS2 unions are expanded into separate named fields. + all_struct_names = list(struct_names) + for header_path, _ in header_tus: + for extra in collect_union_member_types(str(header_path), struct_names): + if extra not in all_struct_names: + all_struct_names.append(extra) + generate_header_file(output_directory, header_tus, all_struct_names, include_root) + generate_implementation_file(output_directory, header_tus, all_struct_names) + + +def _nodes_in_file(translation_unit, header_path): + """Yield only top-level nodes whose definition is in header_path.""" + path_str = str(header_path) + for node in translation_unit.cursor.get_children(): + if node.location.file and node.location.file.name == path_str: + yield node -def generate_header_file(output_directory, translation_unit, struct_names): +def generate_header_file(output_directory, header_tus, struct_names, include_root): """Generate header file for conversion functions.""" header_text = ( '// Auto-generated conversion functions for ROS2 messages\n' @@ -46,20 +100,24 @@ def generate_header_file(output_directory, translation_unit, struct_names): for struct_name in struct_names: header_text += '#include \n' - for node in translation_unit.cursor.get_children(): - if node.kind == clang.cindex.CursorKind.STRUCT_DECL: - msg_name = node.spelling - if msg_name not in struct_names: + _struct_like = ( + clang.cindex.CursorKind.STRUCT_DECL, + clang.cindex.CursorKind.UNION_DECL, + ) + for header_path, translation_unit in header_tus: + for node in _nodes_in_file(translation_unit, header_path): + if node.kind not in _struct_like: + continue + if node.spelling not in struct_names: continue - declaration_file = pathlib.Path( - node.get_definition().location.file.name - ).name - header_text += f'#include \n' + relative_path = header_path.relative_to(include_root) + header_text += f'#include \n' header_text += 'namespace ateam_radio_msgs {\n' - for node in translation_unit.cursor.get_children(): - if node.kind == clang.cindex.CursorKind.STRUCT_DECL: - msg_name = node.spelling - if msg_name not in struct_names: + for header_path, translation_unit in header_tus: + for node in _nodes_in_file(translation_unit, header_path): + if node.kind not in _struct_like: + continue + if node.spelling not in struct_names: continue header_text += generate_conversion_function_declaration(node) + '\n' header_text += '} // namespace ateam_radio_msgs\n\n' @@ -70,21 +128,29 @@ def generate_header_file(output_directory, translation_unit, struct_names): f.write(header_text) -def generate_implementation_file(output_directory, translation_unit, struct_names): +def generate_implementation_file(output_directory, header_tus, struct_names): """Generate implementation file for conversion functions.""" enums = [] impl_text = '#include "conversion.hpp"\n\n' 'namespace ateam_radio_msgs {\n\n' - for node in translation_unit.cursor.get_children(): - match node.kind: - case clang.cindex.CursorKind.ENUM_DECL: - enums.append(collect_enum_details(node)) - case clang.cindex.CursorKind.STRUCT_DECL: - msg_name = node.spelling - if msg_name not in struct_names: + for header_path, translation_unit in header_tus: + for node in translation_unit.cursor.get_children(): + match node.kind: + case clang.cindex.CursorKind.ENUM_DECL: + # Accumulate enums with a file filter so each enum is collected + # exactly once (each header's TU re-exposes enums from its includes). + if node.location.file and node.location.file.name == str(header_path): + enums.append(collect_enum_details(node)) + case clang.cindex.CursorKind.STRUCT_DECL | clang.cindex.CursorKind.UNION_DECL: + if node.location.file is None \ + or node.location.file.name != str(header_path): + continue + if node.spelling not in struct_names: + continue + impl_text += generate_conversion_function_implementation( + node, enums, struct_names + ) + case _: continue - impl_text += generate_conversion_function_implementation(node, enums) - case _: - continue impl_text += '} // namespace ateam_radio_bridge\n' os.makedirs(f'{output_directory}/src', exist_ok=True) file_path = f'{output_directory}/src/conversion.cpp' @@ -99,7 +165,7 @@ def generate_conversion_function_declaration(struct_node): return f'{return_type} Convert(const {struct_node.spelling} & {param_name});' -def generate_conversion_function_implementation(struct_node, enums): +def generate_conversion_function_implementation(struct_node, enums, struct_names): """Generate the implementation of a conversion function for the given struct.""" param_name = camel_case_to_snake_case(struct_node.spelling) return_type = f'ateam_radio_msgs::msg::{struct_node.spelling}' @@ -107,25 +173,84 @@ def generate_conversion_function_implementation(struct_node, enums): f'{return_type} Convert(const {struct_node.spelling} & {param_name}) {{\n' ) impl_text += f' {return_type} msg;\n' + # Track the most recently seen enum field; by convention the enum selector + # immediately precedes its union field in the struct declaration, so this is + # the right field to switch on when we hit a union. + last_enum_field = None for field in struct_node.get_children(): if field.kind != clang.cindex.CursorKind.FIELD_DECL: continue if field.spelling.startswith('_'): continue - impl_text += generate_field_copy_line(field, param_name, enums) + if field.type.spelling in [e['type_name'] for e in enums]: + last_enum_field = field + impl_text += generate_field_copy_line( + field, param_name, enums, struct_names, last_enum_field + ) impl_text += ' return msg;\n' impl_text += '}\n' return impl_text -def generate_field_copy_line(field_node, param_name, enums): +def generate_union_switch_copy_lines(field_node, param_name, struct_names, selector_field, enums): + """Generate a switch on selector_field to convert only the active union member.""" + field_name = field_node.spelling + union_decl = field_node.type.get_canonical().get_declaration() + members = [ + m for m in union_decl.get_children() + if m.kind == clang.cindex.CursorKind.FIELD_DECL + and m.type.spelling in struct_names + ] + enum_details = next(e for e in enums if e['type_name'] == selector_field.type.spelling) + # Convention: enum value 0 means "none/off" (no active union member). + # Remaining values sorted ascending map positionally to union members in + # declaration order, so BCM_GLOBAL_POSITION(1)->global_pos, BCM_GLOBAL_VELOCITY(2)->global_vel, etc. + non_zero_cases = sorted( + [(name, val) for name, val in enum_details['values'] if val != 0], + key=lambda x: x[1], + ) + result = f' switch ({param_name}.{selector_field.spelling}) {{\n' + for (case_name, _), member in zip(non_zero_cases, members): + msg_field = f'{field_name}_{member.spelling}' + result += f' case {case_name}:\n' + result += ( + f' msg.{msg_field} = ' + f'Convert({param_name}.{field_name}.{member.spelling});\n' + ) + result += ' break;\n' + result += ' default:\n' + result += ' break;\n' + result += ' }\n' + return result + + +def generate_field_copy_line(field_node, param_name, enums, struct_names, selector_field=None): """Generate a line of code to copy a field from the struct to the message.""" field_name = field_node.spelling if field_node.type.kind == clang.cindex.TypeKind.CONSTANTARRAY: return f' std::ranges::copy({param_name}.{field_node.spelling}, ' \ f'std::back_inserter(msg.{field_name}));\n' elif field_node.type.kind == clang.cindex.TypeKind.ELABORATED: - if field_node.type.spelling in [ + if _is_elaborated_union(field_node): + if selector_field is not None: + return generate_union_switch_copy_lines( + field_node, param_name, struct_names, selector_field, enums + ) + # No selector enum found — fall back to converting all members unconditionally. + union_decl = field_node.type.get_canonical().get_declaration() + result = '' + for member in union_decl.get_children(): + if member.kind != clang.cindex.CursorKind.FIELD_DECL: + continue + if member.type.spelling not in struct_names: + continue + msg_field = f'{field_name}_{member.spelling}' + result += ( + f' msg.{msg_field} = ' + f'Convert({param_name}.{field_name}.{member.spelling});\n' + ) + return result + elif field_node.type.spelling in [ 'uint8_t', 'uint16_t', 'uint32_t', @@ -138,10 +263,12 @@ def generate_field_copy_line(field_node, param_name, enums): return f' msg.{field_name} = {param_name}.{field_node.spelling};\n' elif field_node.type.spelling in [e['type_name'] for e in enums]: return f' msg.{field_name} = {param_name}.{field_node.spelling};\n' - else: + elif field_node.type.spelling in struct_names: return ( f' msg.{field_name} = Convert({param_name}.{field_node.spelling});\n' ) + else: + return '' else: return f' msg.{field_name} = {param_name}.{field_node.spelling};\n' diff --git a/radio/ateam_radio_msgs/scripts/generate_msgs.py b/radio/ateam_radio_msgs/scripts/generate_msgs.py index 2ad51cb0b..af61f8c16 100644 --- a/radio/ateam_radio_msgs/scripts/generate_msgs.py +++ b/radio/ateam_radio_msgs/scripts/generate_msgs.py @@ -26,6 +26,41 @@ import clang.cindex +def _is_elaborated_union(field_node): + """Return True if the field's type is a typedef'd union.""" + if field_node.type.kind != clang.cindex.TypeKind.ELABORATED: + return False + # get_declaration() on a typedef'd union returns TYPEDEF_DECL, not UNION_DECL. + # The canonical type strips the typedef layer and gives us the underlying record. + canon = field_node.type.get_canonical() + return ( + canon.kind == clang.cindex.TypeKind.RECORD + and canon.get_declaration().kind == clang.cindex.CursorKind.UNION_DECL + ) + + +def collect_union_member_types(file_path, struct_names): + """Return a list of struct type names used as union members inside struct_names types.""" + index = clang.cindex.Index.create() + tu = index.parse(file_path) + extras = [] + struct_like = (clang.cindex.CursorKind.STRUCT_DECL, clang.cindex.CursorKind.UNION_DECL) + for node in tu.cursor.get_children(): + if node.location.file is None or node.location.file.name != file_path: + continue + if node.kind not in struct_like or node.spelling not in struct_names: + continue + for field in node.get_children(): + if field.kind != clang.cindex.CursorKind.FIELD_DECL: + continue + if not _is_elaborated_union(field): + continue + for member in field.type.get_canonical().get_declaration().get_children(): + if member.kind == clang.cindex.CursorKind.FIELD_DECL: + extras.append(member.type.spelling) + return extras + + def generate_msgs_for_file(output_dir, file_path, struct_names): """Generate ROS2 message definitions from a C header file.""" index = clang.cindex.Index.create() @@ -36,21 +71,26 @@ def generate_msgs_for_file(output_dir, file_path, struct_names): for node in translation_unit.cursor.get_children(): match node.kind: case clang.cindex.CursorKind.ENUM_DECL: + # Intentionally no file filter: enums defined in included headers (e.g. + # BodyControlMode in basic_control.h) must be visible when processing files + # that include them, so enum field types resolve correctly. enums.append(collect_enum_details(node)) - case clang.cindex.CursorKind.STRUCT_DECL: + case clang.cindex.CursorKind.STRUCT_DECL | clang.cindex.CursorKind.UNION_DECL: + if node.location.file is None or node.location.file.name != file_path: + continue msg_name = node.spelling if msg_name not in struct_names: continue declaration_file = pathlib.Path( node.get_definition().location.file.name ).name - msg = generate_msg_for_struct(node, enums) + msg = generate_msg_for_struct(node, enums, struct_names) write_msg_to_file(output_dir, msg, declaration_file) case _: continue -def generate_msg_for_struct(struct_ast_node, enums): +def generate_msg_for_struct(struct_ast_node, enums, struct_names): """Generate a ROS2 message definition for a single struct AST node.""" type_name = struct_ast_node.spelling declarations = [] @@ -59,7 +99,7 @@ def generate_msg_for_struct(struct_ast_node, enums): continue if field.spelling.startswith('_'): continue - add_field_declarations(declarations, field, enums) + add_field_declarations(declarations, field, enums, struct_names) return {'type_name': type_name, 'declarations': declarations} @@ -75,19 +115,39 @@ def write_msg_to_file(output_dir, msg, input_file_path): f.write(f'{declaration}\n') -def add_field_declarations(declarations, field_node, enums): +def add_field_declarations(declarations, field_node, enums, struct_names): """Add declarations for a field node to the list of declarations.""" if field_node.is_bitfield(): add_bitfield_declaration(declarations, field_node) elif field_node.type.kind == clang.cindex.TypeKind.CONSTANTARRAY: add_array_declaration(declarations, field_node) + elif _is_elaborated_union(field_node): + add_union_field_declarations(declarations, field_node, struct_names) elif field_node.type.spelling in [e['type_name'] for e in enums]: add_enum_declaration(declarations, field_node, enums) else: ros2_type = get_ros2_basic_type(field_node.type) + # Skip nested struct types not in struct_names: rosidl would fail to resolve + # them, and they are intentionally excluded from generation (e.g. RadioHeader). + if ros2_type.startswith('ateam_radio_msgs/') \ + and ros2_type.removeprefix('ateam_radio_msgs/') not in struct_names: + return declarations.append(f'{ros2_type} {field_node.spelling}') +def add_union_field_declarations(declarations, field_node, struct_names): + """Expand a union field into one declaration per union member.""" + field_prefix = field_node.spelling + union_decl = field_node.type.get_canonical().get_declaration() + for member in union_decl.get_children(): + if member.kind != clang.cindex.CursorKind.FIELD_DECL: + continue + member_type = member.type.spelling + if member_type not in struct_names: + continue + declarations.append(f'ateam_radio_msgs/{member_type} {field_prefix}_{member.spelling}') + + def add_bitfield_declaration(declarations, field_node): """Add a declaration for a bitfield field node.""" type_name = '' @@ -118,6 +178,8 @@ def add_array_declaration(declarations, field_node): def add_enum_declaration(declarations, field_node, enums): """Add a declaration for an enum field node.""" enum_details = [e for e in enums if e['type_name'] == field_node.type.spelling][0] + # ROS2 messages don't support enum types, so we use the underlying integer type and + # inline the named constants as message-level constants below the field. declarations.append(f'{enum_details["underlying_type"]} {field_node.spelling}') for value_name, value in enum_details['values']: declarations.append( @@ -192,4 +254,19 @@ def get_ros2_basic_type(field_type): ' [ ...]' ) sys.exit(1) - generate_msgs_for_file(sys.argv[1], sys.argv[2], sys.argv[3:]) + output_dir = sys.argv[1] + source = pathlib.Path(sys.argv[2]) + struct_names = list(sys.argv[3:]) + include_dir = source.parent + headers = sorted(include_dir.rglob('*.h')) + # Two-pass: first discover the struct types used as union members (e.g. + # ExtendedGlobalPositionTelemetry inside BodyControlSkillExtendedTelemetry). + # These aren't in the original struct_names list but need their own .msg files + # because ROS2 doesn't support unions — we expand each union into separate fields. + all_struct_names = list(struct_names) + for header in headers: + for extra in collect_union_member_types(str(header), struct_names): + if extra not in all_struct_names: + all_struct_names.append(extra) + for header in headers: + generate_msgs_for_file(output_dir, str(header), all_struct_names) From b552e5a341d62ce5b0569410c0f5de0f15305da9 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Wed, 29 Apr 2026 01:26:50 -0400 Subject: [PATCH 07/16] First pass at generating packet version header. --- radio/ateam_radio_msgs/CMakeLists.txt | 3 ++ .../cmake/generate_version_header.cmake | 52 +++++++++++++++++++ .../ateam_radio_msgs/templates/version.hpp.in | 31 +++++++++++ 3 files changed, 86 insertions(+) create mode 100644 radio/ateam_radio_msgs/cmake/generate_version_header.cmake create mode 100644 radio/ateam_radio_msgs/templates/version.hpp.in diff --git a/radio/ateam_radio_msgs/CMakeLists.txt b/radio/ateam_radio_msgs/CMakeLists.txt index 1e31c1905..024428dae 100644 --- a/radio/ateam_radio_msgs/CMakeLists.txt +++ b/radio/ateam_radio_msgs/CMakeLists.txt @@ -3,6 +3,7 @@ project(ateam_radio_msgs) include(cmake/generate_msgs.cmake) include(cmake/generate_conversion_code.cmake) +include(cmake/generate_version_header.cmake) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) @@ -47,6 +48,8 @@ generate_conversion_code( STRUCTS ${RADIO_STRUCTS_TO_GENERATE} ) +generate_version_header() + file(GLOB CONFIG_DEPENDS CONFIGURE_DEPENDS "${RADIO_PACKETS_INCLUDE_DIR}/*.*") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CONFIG_DEPENDS}) diff --git a/radio/ateam_radio_msgs/cmake/generate_version_header.cmake b/radio/ateam_radio_msgs/cmake/generate_version_header.cmake new file mode 100644 index 000000000..a0d2b0775 --- /dev/null +++ b/radio/ateam_radio_msgs/cmake/generate_version_header.cmake @@ -0,0 +1,52 @@ +# Copyright 2026 A Team +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +function(generate_version_header) + set(SOFT_COMMS_SUBMODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/software-communication") + + find_package(Git REQUIRED) + + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY ${SOFT_COMMS_SUBMODULE_DIR} + OUTPUT_VARIABLE SOFT_COMMS_SUBMODULE_SHA + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + + string(SUBSTRING "${SOFT_COMMS_SUBMODULE_SHA}" 0 8 SOFT_COMMS_SUBMODULE_SHA_SHORT) + + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain + WORKING_DIRECTORY ${SOFT_COMMS_SUBMODULE_DIR} + OUTPUT_VARIABLE SOFT_COMMS_SUBMODULE_STATUS + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if(SOFT_COMMS_SUBMODULE_STATUS) + set(SOFT_COMMS_SUBMODULE_DIRTY true) + else() + set(SOFT_COMMS_SUBMODULE_DIRTY false) + endif() + + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/templates/version.hpp.in "${CMAKE_CURRENT_BINARY_DIR}/ateam_generated/include/version.hpp" @ONLY) + +endfunction() diff --git a/radio/ateam_radio_msgs/templates/version.hpp.in b/radio/ateam_radio_msgs/templates/version.hpp.in new file mode 100644 index 000000000..1c13011b2 --- /dev/null +++ b/radio/ateam_radio_msgs/templates/version.hpp.in @@ -0,0 +1,31 @@ +// Copyright 2026 A Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#ifndef ATEAM_RADIO_MSGS__VERSION_HPP_ +#define ATEAM_RADIO_MSGS__VERSION_HPP_ + +namespace ateam_radio_msgs +{ + constexpr const char * COMMS_HASH = "@SOFT_COMMS_SUBMODULE_SHA_SHORT@"; + + constexpr bool COMMS_DIRTY = @SOFT_COMMS_SUBMODULE_DIRTY@; +} + +#endif // ATEAM_RADIO_MSGS__VERSION_HPP_ From fd8c86867d17c897466bed66fe8a19dea110511f Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Fri, 1 May 2026 23:12:15 -0400 Subject: [PATCH 08/16] Replaces string hash with numeric hash. --- radio/ateam_radio_msgs/cmake/generate_version_header.cmake | 2 ++ radio/ateam_radio_msgs/templates/version.hpp.in | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/radio/ateam_radio_msgs/cmake/generate_version_header.cmake b/radio/ateam_radio_msgs/cmake/generate_version_header.cmake index a0d2b0775..04f039160 100644 --- a/radio/ateam_radio_msgs/cmake/generate_version_header.cmake +++ b/radio/ateam_radio_msgs/cmake/generate_version_header.cmake @@ -33,6 +33,8 @@ function(generate_version_header) ) string(SUBSTRING "${SOFT_COMMS_SUBMODULE_SHA}" 0 8 SOFT_COMMS_SUBMODULE_SHA_SHORT) + set(_hex_string "0x${SOFT_COMMS_SUBMODULE_SHA_SHORT}") + math(EXPR SOFT_COMMS_SUBMODULE_SHA_SHORT_NUM "${_hex_string} + 0") execute_process( COMMAND ${GIT_EXECUTABLE} status --porcelain diff --git a/radio/ateam_radio_msgs/templates/version.hpp.in b/radio/ateam_radio_msgs/templates/version.hpp.in index 1c13011b2..7e5044570 100644 --- a/radio/ateam_radio_msgs/templates/version.hpp.in +++ b/radio/ateam_radio_msgs/templates/version.hpp.in @@ -23,7 +23,7 @@ namespace ateam_radio_msgs { - constexpr const char * COMMS_HASH = "@SOFT_COMMS_SUBMODULE_SHA_SHORT@"; + constexpr uint32_t COMMS_HASH = @SOFT_COMMS_SUBMODULE_SHA_SHORT_NUM@; constexpr bool COMMS_DIRTY = @SOFT_COMMS_SUBMODULE_DIRTY@; } From f2cb8e193075cbfd087d5d62cadf30bb24c74153 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Fri, 1 May 2026 23:17:36 -0400 Subject: [PATCH 09/16] Fixes packet header install step --- radio/ateam_radio_msgs/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/ateam_radio_msgs/CMakeLists.txt b/radio/ateam_radio_msgs/CMakeLists.txt index 024428dae..ff39fff23 100644 --- a/radio/ateam_radio_msgs/CMakeLists.txt +++ b/radio/ateam_radio_msgs/CMakeLists.txt @@ -91,7 +91,7 @@ install( DESTINATION include/ateam_radio_msgs/ateam_radio_msgs ) install( - DIRECTORY ${RADIO_PACKETS_INCLUDE_DIR} + DIRECTORY ${RADIO_PACKETS_INCLUDE_DIR}/ DESTINATION include/ateam_radio_msgs/ateam_radio_msgs/packets ) install( From 5b53ec840df15abf378c69dfe4e743ffeffbf83d Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Sat, 2 May 2026 01:07:31 -0400 Subject: [PATCH 10/16] Fixes radio message generation --- radio/ateam_radio_msgs/CMakeLists.txt | 11 +++++++++-- .../ateam_radio_msgs/cmake/generate_msgs.cmake | 17 ++++------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/radio/ateam_radio_msgs/CMakeLists.txt b/radio/ateam_radio_msgs/CMakeLists.txt index ff39fff23..9737d4d3e 100644 --- a/radio/ateam_radio_msgs/CMakeLists.txt +++ b/radio/ateam_radio_msgs/CMakeLists.txt @@ -38,7 +38,7 @@ set(RADIO_STRUCTS_TO_GENERATE generate_msgs( SOURCE ${RADIO_PACKETS_INCLUDE_DIR}/radio.h - DESTINATION ${GENERATED_DIR}/msg + DESTINATION ${GENERATED_DIR} STRUCTS ${RADIO_STRUCTS_TO_GENERATE} ) @@ -53,9 +53,16 @@ generate_version_header() file(GLOB CONFIG_DEPENDS CONFIGURE_DEPENDS "${RADIO_PACKETS_INCLUDE_DIR}/*.*") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CONFIG_DEPENDS}) +file(GLOB _raw_generated_msgs "${GENERATED_DIR}/msg/*.msg") +set(_generated_msgs "") +foreach(_msg ${_raw_generated_msgs}) + string(REPLACE "${GENERATED_DIR}/" "" _rel "${_msg}") + list(APPEND _generated_msgs "${GENERATED_DIR}:${_rel}") +endforeach() + set(MSG_TARGET ${PROJECT_NAME}) rosidl_generate_interfaces(${MSG_TARGET} - ${GENERATED_MSG_TUPLES} + ${_generated_msgs} msg/ConnectionStatus.msg diff --git a/radio/ateam_radio_msgs/cmake/generate_msgs.cmake b/radio/ateam_radio_msgs/cmake/generate_msgs.cmake index 2036da417..67aa2a0d7 100644 --- a/radio/ateam_radio_msgs/cmake/generate_msgs.cmake +++ b/radio/ateam_radio_msgs/cmake/generate_msgs.cmake @@ -32,26 +32,17 @@ function(generate_msgs) message(FATAL_ERROR "At least one struct must be specified.") endif() - file(MAKE_DIRECTORY "${arg_DESTINATION}") + set(output_msg_dir "${arg_DESTINATION}/msg") + + file(MAKE_DIRECTORY "${output_msg_dir}") set(_generate_msgs_script "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_msgs.py") if(NOT EXISTS "${_generate_msgs_script}") message(FATAL_ERROR "Script ${_generate_msgs_script} does not exist.") endif() - set(${generated_msgs_files} "") - foreach(struct ${arg_STRUCTS}) - list(APPEND generated_msgs_files "${arg_DESTINATION}/${struct}.msg") - endforeach() - - set(GENERATED_MSG_TUPLES "") - foreach(_msg ${generated_msgs_files}) - string(REPLACE "${arg_DESTINATION}/" "" _rel "${_msg}") - list(APPEND GENERATED_MSG_TUPLES "${arg_DESTINATION}:${_rel}") - endforeach() - execute_process( - COMMAND python3 ${_generate_msgs_script} ${arg_DESTINATION} ${arg_SOURCE} ${arg_STRUCTS} + COMMAND python3 ${_generate_msgs_script} ${output_msg_dir} ${arg_SOURCE} ${arg_STRUCTS} RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error From 90d0f4ef88e05b44f6df31d8d642573a745ef508 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Sat, 2 May 2026 05:06:00 -0400 Subject: [PATCH 11/16] Updates radio bridge to new message definitions, including tests --- .../src/firmware_parameter_server.cpp | 4 +- .../src/radio_bridge_node.cpp | 41 +++-- .../src/rnp_packet_helpers.cpp | 152 +++++++++--------- .../src/rnp_packet_helpers.hpp | 19 ++- .../test/launch_tests/bridge_command_test.py | 4 +- .../launch_tests/bridge_discovery_test.py | 16 +- .../test/launch_tests/bridge_feedback_test.py | 2 +- .../test/launch_tests/mock_robot.py | 60 +++---- .../unit_tests/rnp_packet_helpers_tests.cpp | 134 +++++++++------ 9 files changed, 240 insertions(+), 192 deletions(-) diff --git a/radio/ateam_radio_bridge/src/firmware_parameter_server.cpp b/radio/ateam_radio_bridge/src/firmware_parameter_server.cpp index 3de4fafeb..ec164df2d 100644 --- a/radio/ateam_radio_bridge/src/firmware_parameter_server.cpp +++ b/radio/ateam_radio_bridge/src/firmware_parameter_server.cpp @@ -72,7 +72,7 @@ void FirmwareParameterServer::GetFirmwareParameterCallback(const ateam_msgs::srv response_ready_ = false; connections_[robot_id]->send( reinterpret_cast(&command_packet), - GetPacketSize(command_packet.command_code)); + GetPacketSize(command_packet.header.command_code)); if(!response_cv_.wait_for(lock, std::chrono::milliseconds(300), [this]{ return response_ready_.load(); })) { response->success = false; response->reason = "Timed out waiting for reply."; @@ -124,7 +124,7 @@ void FirmwareParameterServer::SetFirmwareParameterCallback(const ateam_msgs::srv response_ready_ = false; connections_[robot_id]->send( reinterpret_cast(&command_packet), - GetPacketSize(command_packet.command_code)); + GetPacketSize(command_packet.header.command_code)); if(!response_cv_.wait_for(lock, std::chrono::milliseconds(100), [this]{ return response_ready_.load(); })) { response->success = false; response->reason = "Timed out waiting for reply."; diff --git a/radio/ateam_radio_bridge/src/radio_bridge_node.cpp b/radio/ateam_radio_bridge/src/radio_bridge_node.cpp index 7da95b046..72f485314 100644 --- a/radio/ateam_radio_bridge/src/radio_bridge_node.cpp +++ b/radio/ateam_radio_bridge/src/radio_bridge_node.cpp @@ -201,7 +201,7 @@ class RadioBridgeNode : public rclcpp::Node const auto packet = CreateEmptyPacket(CC_GOODBYE); connection->send( reinterpret_cast(&packet), - GetPacketSize(packet.command_code)); + GetPacketSize(packet.header.command_code)); // Give some time for the message to actually send before closing the connection std::this_thread::sleep_for(std::chrono::milliseconds(50)); } @@ -263,22 +263,31 @@ class RadioBridgeNode : public rclcpp::Node control_msg.game_state_in_stop = game_controller_listener_.GetGameCommand() == ateam_common::GameCommand::Stop; control_msg.emergency_stop = false; - control_msg.body_vel_controls_enabled = get_parameter("controls_enabled.body_vel").as_bool(); control_msg.wheel_vel_control_enabled = get_parameter("controls_enabled.wheel_vel").as_bool(); control_msg.wheel_torque_control_enabled = get_parameter("controls_enabled.wheel_torque").as_bool(); - control_msg.play_song = 0; - control_msg.vel_x_linear = motion_commands_[id].twist.linear.x; - control_msg.vel_y_linear = motion_commands_[id].twist.linear.y; - control_msg.vel_z_angular = motion_commands_[id].twist.angular.z; - control_msg.dribbler_speed = motion_commands_[id].dribbler_speed; - control_msg.dribbler_multiplier = 55; + control_msg.vision_update = 0; + control_msg.reserved1 = 0; + control_msg.vision_position_update[0] = 0.0f; + control_msg.vision_position_update[1] = 0.0f; + control_msg.vision_position_update[2] = 0.0f; + control_msg.body_control_mode = BCM_LOCAL_VELOCITY; control_msg.kick_request = static_cast(motion_commands_[id].kick_request); + control_msg.play_song = 0; + control_msg.reserved2[0] = 0; control_msg.kick_vel = motion_commands_[id].kick_speed; + control_msg.dribbler_speed = motion_commands_[id].dribbler_speed; + control_msg.cmd.local_vel = { + static_cast(motion_commands_[id].twist.linear.x), + static_cast(motion_commands_[id].twist.linear.y), + static_cast(motion_commands_[id].twist.angular.z), + 0.0, // TODO(barulicm): max_linear_acc + 0.0 // TODO(barulicm): max_angular_acc + }; const auto control_packet = CreatePacket(CC_CONTROL, control_msg); connections_[id]->send( reinterpret_cast(&control_packet), - GetPacketSize(control_packet.command_code)); + GetPacketSize(control_packet.header.command_code)); } } @@ -293,10 +302,10 @@ class RadioBridgeNode : public rclcpp::Node return; } - if (packet.command_code != CC_HELLO_REQ) { + if (packet.header.command_code != CC_HELLO_REQ) { RCLCPP_WARN( get_logger(), "Ignoring discovery packet. Unexpected command code: %d", - packet.command_code); + packet.header.command_code); return; } @@ -330,7 +339,7 @@ class RadioBridgeNode : public rclcpp::Node const auto reply_packet = CreateEmptyPacket(CC_NACK); discovery_receiver_.SendTo( sender_address, sender_port, - reinterpret_cast(&reply_packet), GetPacketSize(reply_packet.command_code)); + reinterpret_cast(&reply_packet), GetPacketSize(reply_packet.header.command_code)); RCLCPP_WARN(get_logger(), "Rejecting discovery packet. Invalid robot ID: %d", robot_id); return; } @@ -344,7 +353,7 @@ class RadioBridgeNode : public rclcpp::Node const auto reply_packet = CreateEmptyPacket(CC_NACK); discovery_receiver_.SendTo( sender_address, sender_port, - reinterpret_cast(&reply_packet), GetPacketSize(reply_packet.command_code)); + reinterpret_cast(&reply_packet), GetPacketSize(reply_packet.header.command_code)); RCLCPP_WARN(get_logger(), "Rejecting discovery packet. Robot ID already connected: %d", robot_id); return; @@ -373,7 +382,7 @@ class RadioBridgeNode : public rclcpp::Node const auto reply_packet = CreatePacket(CC_HELLO_RESP, response); discovery_receiver_.SendTo( sender_address, sender_port, - reinterpret_cast(&reply_packet), GetPacketSize(reply_packet.command_code)); + reinterpret_cast(&reply_packet), GetPacketSize(reply_packet.header.command_code)); } void RobotIncomingPacketCallback( @@ -389,7 +398,7 @@ class RadioBridgeNode : public rclcpp::Node const std::lock_guard lock(mutex_); - switch (packet.command_code) { + switch (packet.header.command_code) { case CC_GOODBYE: // close connection. No need to send our own goodbye RCLCPP_INFO(get_logger(), "Received goodbye from robot %d.", robot_id); @@ -446,7 +455,7 @@ class RadioBridgeNode : public rclcpp::Node default: RCLCPP_WARN( get_logger(), "Ignoring telemetry message from robot %d. Unsupported command code: %d", - robot_id, packet.command_code); + robot_id, packet.header.command_code); return; } } diff --git a/radio/ateam_radio_bridge/src/rnp_packet_helpers.cpp b/radio/ateam_radio_bridge/src/rnp_packet_helpers.cpp index 191538dbc..bb76bc62f 100644 --- a/radio/ateam_radio_bridge/src/rnp_packet_helpers.cpp +++ b/radio/ateam_radio_bridge/src/rnp_packet_helpers.cpp @@ -41,40 +41,41 @@ namespace ateam_radio_bridge */ std::size_t GetPacketSize(const CommandCode & command_code) { + const auto packet_header_size = sizeof(RadioHeader); // For now, packet size only depends on command code switch (command_code) { case CC_ACK: - return kPacketHeaderSize; + return packet_header_size; break; case CC_NACK: - return kPacketHeaderSize; + return packet_header_size; break; case CC_GOODBYE: - return kPacketHeaderSize; + return packet_header_size; break; case CC_KEEPALIVE: - return kPacketHeaderSize; + return packet_header_size; break; case CC_HELLO_REQ: - return kPacketHeaderSize + sizeof(HelloRequest); + return packet_header_size + sizeof(HelloRequest); break; case CC_TELEMETRY: - return kPacketHeaderSize + sizeof(BasicTelemetry); + return packet_header_size + sizeof(BasicTelemetry); break; case CC_CONTROL: - return kPacketHeaderSize + sizeof(BasicControl); + return packet_header_size + sizeof(BasicControl); break; case CC_HELLO_RESP: - return kPacketHeaderSize + sizeof(HelloResponse); + return packet_header_size + sizeof(HelloResponse); break; case CC_CONTROL_DEBUG_TELEMETRY: - return kPacketHeaderSize + sizeof(ExtendedTelemetry); + return packet_header_size + sizeof(ExtendedTelemetry); break; case CC_ROBOT_PARAMETER_COMMAND: - return kPacketHeaderSize + sizeof(ParameterCommand); + return packet_header_size + sizeof(ParameterCommand); break; default: - throw std::invalid_argument("Unrecognized command code."); + throw std::invalid_argument("Unrecognized command code: " + std::to_string(command_code)); } } @@ -89,13 +90,13 @@ std::size_t GetPacketSize(const CommandCode & command_code) */ void SetCRC(RadioPacket & packet) { - const auto crc_size = sizeof(packet.crc32); - const auto packet_size = GetPacketSize(packet.command_code); + const auto crc_size = sizeof(packet.header.crc32); + const auto packet_size = GetPacketSize(packet.header.command_code); boost::crc_32_type crc; crc.process_bytes( reinterpret_cast(reinterpret_cast(&packet) + crc_size), packet_size - crc_size); - packet.crc32 = crc.checksum(); + packet.header.crc32 = crc.checksum(); } /** @@ -109,13 +110,13 @@ void SetCRC(RadioPacket & packet) */ bool HasCorrectCRC(const RadioPacket & packet) { - const auto crc_size = sizeof(packet.crc32); - const auto packet_size = GetPacketSize(packet.command_code); + const auto crc_size = sizeof(packet.header.crc32); + const auto packet_size = GetPacketSize(packet.header.command_code); boost::crc_32_type crc; crc.process_bytes( reinterpret_cast(reinterpret_cast(&packet) + crc_size), packet_size - crc_size); - return packet.crc32 == crc.checksum(); + return packet.header.crc32 == crc.checksum(); } /** @@ -132,11 +133,12 @@ bool HasCorrectCRC(const RadioPacket & packet) RadioPacket CreateEmptyPacket(const CommandCode command_code) { RadioPacket packet{ - 0, - kProtocolVersionMajor, - kProtocolVersionMinor, - command_code, - 0, + RadioHeader{ + 0, // crc32 + command_code, + 0, // reserved + 0 // data length + }, {} }; @@ -160,11 +162,19 @@ RadioPacket CreateEmptyPacket(const CommandCode command_code) */ RadioPacket ParsePacket(const uint8_t * data, const std::size_t data_length, std::string & error) { + const auto packet_header_size = sizeof(RadioHeader); + RadioPacket packet; - std::copy_n(data, kPacketHeaderSize, reinterpret_cast(&packet)); + std::copy_n(data, packet_header_size, reinterpret_cast(&packet)); - const auto packet_size = GetPacketSize(packet.command_code); + std::size_t packet_size = 0; + try { + packet_size = GetPacketSize(packet.header.command_code); + } catch (const std::invalid_argument & e) { + error = "Error getting packet size: " + std::string(e.what()); + return {}; + } if (data_length != packet_size) { error = "Wrong number of bytes. Expected " + std::to_string(packet_size) + " but got " + @@ -172,16 +182,8 @@ RadioPacket ParsePacket(const uint8_t * data, const std::size_t data_length, std return {}; } - if (packet.major_version != kProtocolVersionMajor || - packet.minor_version != kProtocolVersionMinor) - { - // TODO(barulicm) What should our version compatability rules actually be? This assumes they must match. - error = "Protocol versions do not match."; - return {}; - } - std::copy_n( - data + kPacketHeaderSize, packet_size - kPacketHeaderSize, + data + packet_header_size, packet_size - packet_header_size, reinterpret_cast(&packet.data)); // TODO(barulicm) Firmware doesn't implement CRCs yet @@ -210,10 +212,10 @@ PacketDataVariant ExtractData(const RadioPacket & packet, std::string & error) { PacketDataVariant var; - switch (packet.command_code) { + switch (packet.header.command_code) { case CC_HELLO_REQ: { - if (packet.data_length != sizeof(HelloRequest)) { + if (packet.header.data_length != sizeof(HelloRequest)) { error = "Incorrect data length for HelloData type."; break; } @@ -222,7 +224,7 @@ PacketDataVariant ExtractData(const RadioPacket & packet, std::string & error) } case CC_TELEMETRY: { - if (packet.data_length != sizeof(BasicTelemetry)) { + if (packet.header.data_length != sizeof(BasicTelemetry)) { error = "Incorrect data length for BasicTelemetry type."; break; } @@ -231,16 +233,16 @@ PacketDataVariant ExtractData(const RadioPacket & packet, std::string & error) } case CC_CONTROL_DEBUG_TELEMETRY: { - if (packet.data_length != sizeof(ExtendedTelemetry)) { + if (packet.header.data_length != sizeof(ExtendedTelemetry)) { error = "Incorrect data length for ExtendedTelemetry type."; break; } - var = packet.data.control_debug_telemetry; + var = packet.data.extended_telemetry; break; } case CC_ROBOT_PARAMETER_COMMAND: { - if (packet.data_length != sizeof(ParameterCommand)) { + if (packet.header.data_length != sizeof(ParameterCommand)) { error = "Incorrect data length for ParameterCommand type."; break; } @@ -249,7 +251,7 @@ PacketDataVariant ExtractData(const RadioPacket & packet, std::string & error) } case CC_CONTROL: { - if (packet.data_length != sizeof(BasicControl)) { + if (packet.header.data_length != sizeof(BasicControl)) { error = "Incorrect data length for BasicControl type."; break; } @@ -267,28 +269,38 @@ PacketDataVariant ExtractData(const RadioPacket & packet, std::string & error) ParameterDataFormat GetParameterDataFormatForParameter(const ParameterName & parameter) { switch(parameter) { - case VEL_PID_X: - return PID_LIMITED_INTEGRAL_F32; - case VEL_PID_Y: - return PID_LIMITED_INTEGRAL_F32; - case ANGULAR_VEL_PID_Z: - return PID_LIMITED_INTEGRAL_F32; - case VEL_CGKF_ENCODER_NOISE: - return F32; - case VEL_CGKF_GYRO_NOISE: - return F32; - case VEL_CGKF_PROCESS_NOISE: - return F32; - case VEL_CGFK_INITIAL_COVARIANCE: - return F32; - case VEL_CGKF_K_MATRIX: - return MATRIX_F32; - case RC_BODY_VEL_LIMIT: - return VEC3_F32; - case RC_BODY_ACC_LIMIT: - return VEC3_F32; - case RC_WHEEL_ACC_LIMIT: + case KF_PROCESS_STD: + return VEC4_F32; + case KF_MEASUREMENT_STD: return VEC4_F32; + case KF_MAX_STATE: + return VEC4_F32; + case PHYS_WHEEL: + return VEC4_F32; + case PHYS_INERTIA: + return VEC2_F32; + case PHYS_MOTOR_MODEL: + return VEC2_F32; + case PHYS_FRICTION_MODEL: + return VEC4_F32; + case POSE_CONTROL_GAIN: + return VEC2_F32; + case TRAJ_RECOMPUTE_ERROR: + return VEC4_F32; + case TRAJ_MAX: + return VEC4_F32; + case POSE_FB_PIDII_X: + return VEC5_F32; + case POSE_FB_PIDII_Y: + return VEC5_F32; + case POSE_FB_PIDII_THETA: + return VEC5_F32; + case TWIST_FB_PIDII_X: + return VEC5_F32; + case TWIST_FB_PIDII_Y: + return VEC5_F32; + case TWIST_FB_PIDII_THETA: + return VEC5_F32; default: throw std::invalid_argument("GetParameterDataFormatForParameter: Unrecognized parameter name."); } @@ -299,16 +311,14 @@ std::size_t GetDataSizeForParameterFormat(const ParameterDataFormat & format) switch(format) { case F32: return 1; + case VEC2_F32: + return 2; case VEC3_F32: return 3; case VEC4_F32: return 4; - case PID_F32: - return 3; - case PID_LIMITED_INTEGRAL_F32: + case VEC5_F32: return 5; - case MATRIX_F32: - return 25; default: throw std::invalid_argument("GetDataSizeForParameterFormat: Unrecognized parameter data format."); } @@ -320,16 +330,14 @@ float* GetParameterDataForSetFormat(ParameterCommand & command) switch(command.data_format) { case F32: return &command.data.f32; + case VEC2_F32: + return command.data.vec2_f32; case VEC3_F32: return command.data.vec3_f32; case VEC4_F32: return command.data.vec4_f32; - case PID_F32: - return command.data.pid_f32; - case PID_LIMITED_INTEGRAL_F32: - return command.data.pidii_f32; - case MATRIX_F32: - return command.data.matrix_f32; + case VEC5_F32: + return command.data.vec5_f32; default: throw std::invalid_argument("GetParameterDataForSetFormat: Unrecognized parameter data format."); } diff --git a/radio/ateam_radio_bridge/src/rnp_packet_helpers.hpp b/radio/ateam_radio_bridge/src/rnp_packet_helpers.hpp index f773bfa55..d3c061b9d 100644 --- a/radio/ateam_radio_bridge/src/rnp_packet_helpers.hpp +++ b/radio/ateam_radio_bridge/src/rnp_packet_helpers.hpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include #include @@ -39,8 +39,6 @@ namespace ateam_radio_bridge std::size_t GetPacketSize(const CommandCode & packet); -constexpr std::size_t kPacketHeaderSize = 12; - void SetCRC(RadioPacket & packet); bool HasCorrectCRC(const RadioPacket & packet); @@ -48,20 +46,21 @@ bool HasCorrectCRC(const RadioPacket & packet); template void SetDataPayload(RadioPacket & packet, const DataTypeType & payload) { - packet.data_length = sizeof(DataTypeType); + packet.header.data_length = sizeof(DataTypeType); auto payload_ptr = reinterpret_cast(&payload); - std::copy_n(payload_ptr, packet.data_length, reinterpret_cast(&packet.data)); + std::copy_n(payload_ptr, packet.header.data_length, reinterpret_cast(&packet.data)); } template RadioPacket CreatePacket(const CommandCode command_code, const DataTypeType & data) { RadioPacket packet{ - 0, - kProtocolVersionMajor, - kProtocolVersionMinor, - command_code, - 0, + RadioHeader{ + 0, // crc32 will be set later + command_code, + 0, // reserved + 0, // data length + }, {} }; diff --git a/radio/ateam_radio_bridge/test/launch_tests/bridge_command_test.py b/radio/ateam_radio_bridge/test/launch_tests/bridge_command_test.py index a98bacc40..e84ea8997 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/bridge_command_test.py +++ b/radio/ateam_radio_bridge/test/launch_tests/bridge_command_test.py @@ -84,10 +84,10 @@ def test_commands(self): break self.cmd_pub.publish(cmd_msg) last_packet = self.robot.getLastCmdMessage() - if len(last_packet) != 40: + if len(last_packet) != 64: continue # Extract BasicControl.vel_x_linear - vel_x_linear = struct.unpack(" +#include #include @@ -29,32 +30,34 @@ TEST(SetCRC, BasicTest) { RadioPacket packet{ - 0, - 1, - 2, - CC_ACK, - 0, + { + 0, + CC_ACK, + 0, + 0 + }, {} }; ateam_radio_bridge::SetCRC(packet); - EXPECT_EQ(packet.crc32, 1560025497u); + EXPECT_EQ(packet.header.crc32, 2583214201u); } // Do we reject (some) bad CRC values? TEST(HasCorrectCRC, BasicTest) { RadioPacket packet{ - 1560025497, - 1, - 2, - CC_ACK, - 0, + { + 2583214201, + CC_ACK, + 0, + 0 + }, {} }; EXPECT_TRUE(ateam_radio_bridge::HasCorrectCRC(packet)); - packet.crc32 = 12; // arbitrary bad CRC value + packet.header.crc32 = 12; // arbitrary bad CRC value EXPECT_FALSE(ateam_radio_bridge::HasCorrectCRC(packet)); } @@ -63,11 +66,19 @@ TEST(SetDataPayload, HelloRequest) { RadioPacket packet; HelloRequest payload{ - 8, - TC_BLUE + 8, // robot ID + TC_BLUE, + 0, // coms repo dirty + 0, // controls repo dirty + 0, // firmware repo dirty + 0, // bitfield reserved + 0, // reserved + {0, 0, 0, 0}, // coms hash + {0, 0, 0, 0}, // controls hash + {0, 0, 0, 0} // firmware hash }; ateam_radio_bridge::SetDataPayload(packet, payload); - EXPECT_EQ(packet.data_length, 2); + EXPECT_EQ(packet.header.data_length, 16); EXPECT_EQ(packet.data.hello_request.robot_id, 8); EXPECT_EQ(packet.data.hello_request.color, TC_BLUE); } @@ -76,15 +87,21 @@ TEST(SetDataPayload, HelloRequest) TEST(CreatePacket, HelloRequest) { HelloRequest payload{ - 12, - TC_YELLOW + 12, // robot ID + TC_YELLOW, + 0, // coms repo dirty + 0, // controls repo dirty + 0, // firmware repo dirty + 0, // bitfield reserved + 0, // reserved + {0, 0, 0, 0}, // coms hash + {0, 0, 0, 0}, // controls hash + {0, 0, 0, 0} // firmware hash }; RadioPacket packet = ateam_radio_bridge::CreatePacket(CC_HELLO_REQ, payload); - EXPECT_EQ(packet.crc32, 1906667910u); - EXPECT_EQ(packet.major_version, kProtocolVersionMajor); - EXPECT_EQ(packet.minor_version, kProtocolVersionMinor); - EXPECT_EQ(packet.command_code, CC_HELLO_REQ); - EXPECT_EQ(packet.data_length, 2); + EXPECT_EQ(packet.header.crc32, 1293447015u); + EXPECT_EQ(packet.header.command_code, CC_HELLO_REQ); + EXPECT_EQ(packet.header.data_length, 16); EXPECT_EQ(packet.data.hello_request.robot_id, 12); EXPECT_EQ(packet.data.hello_request.color, TC_YELLOW); } @@ -93,54 +110,65 @@ TEST(CreatePacket, HelloRequest) TEST(CreateEmptyPacket, Ack) { RadioPacket packet = ateam_radio_bridge::CreateEmptyPacket(CC_ACK); - EXPECT_EQ(packet.crc32, 381840297u); - EXPECT_EQ(packet.major_version, kProtocolVersionMajor); - EXPECT_EQ(packet.minor_version, kProtocolVersionMinor); - EXPECT_EQ(packet.command_code, CC_ACK); - EXPECT_EQ(packet.data_length, 0); + EXPECT_EQ(packet.header.crc32, 2583214201u); + EXPECT_EQ(packet.header.command_code, CC_ACK); + EXPECT_EQ(packet.header.data_length, 0); } // Are we parsing packets correctly? TEST(ParsePacket, HelloRequest) { - std::array data = { - 0, - 0, - 0, - 0, - kProtocolVersionMajor & 0xFF, - kProtocolVersionMajor >> 8, - kProtocolVersionMinor & 0xFF, - kProtocolVersionMinor >> 8, - CC_HELLO_REQ, - 0, - 2, - 0, - 15, - 1 + std::array data = { + 0, 0, 0, 0, // crc + CC_HELLO_REQ, // command code + 0, // reserved + 16, 0, // data length + 15, // robot ID + 1, // team color + 5, // dirty flags + 0, // reserved + 1, 2, 3, 4, // coms hash + 5, 6, 7, 8, // controls hash + 9, 10, 11, 12 // firmware hash }; std::string error_msg; RadioPacket packet = ateam_radio_bridge::ParsePacket(data.data(), data.size(), error_msg); EXPECT_TRUE(error_msg.empty()) << "Unexpected error message: " << error_msg; - EXPECT_EQ(packet.crc32, 0u); - EXPECT_EQ(packet.major_version, kProtocolVersionMajor); - EXPECT_EQ(packet.minor_version, kProtocolVersionMinor); - EXPECT_EQ(packet.command_code, CC_HELLO_REQ); - EXPECT_EQ(packet.data_length, 2); + EXPECT_EQ(packet.header.crc32, 0u); + EXPECT_EQ(packet.header.command_code, CC_HELLO_REQ); + EXPECT_EQ(packet.header.data_length, 16); EXPECT_EQ(packet.data.hello_request.robot_id, 15); EXPECT_EQ(packet.data.hello_request.color, TC_BLUE); + EXPECT_EQ(packet.data.hello_request.coms_repo_dirty, 1); + EXPECT_EQ(packet.data.hello_request.controls_repo_dirty, 0); + EXPECT_EQ(packet.data.hello_request.firmware_repo_dirty, 1); + EXPECT_THAT(packet.data.hello_request.coms_hash, testing::ElementsAreArray({1, 2, 3, 4})); + EXPECT_THAT(packet.data.hello_request.controls_hash, testing::ElementsAreArray({5, 6, 7, 8})); + EXPECT_THAT(packet.data.hello_request.firmware_hash, testing::ElementsAreArray({9, 10, 11, 12})); } // Are we able to extract data from a packet correctly? TEST(ExtractData, HelloRequest) { RadioPacket packet{ - 0, - 0, - 0, - CC_HELLO_REQ, - 2, - HelloRequest{4, TC_BLUE} + { + 0, + CC_HELLO_REQ, + 0, + 16 + }, + HelloRequest{ + 4, // robot ID + TC_BLUE, + 0, // coms repo dirty + 0, // controls repo dirty + 0, // firmware repo dirty + 0, // bitfield reserved + 0, // reserved + {0, 0, 0, 0}, // coms hash + {0, 0, 0, 0}, // controls hash + {0, 0, 0, 0} // firmware hash + } }; std::string error_msg; ateam_radio_bridge::PacketDataVariant data_var = From 95913271fd370022f6c142e959f661d8ef0d733c Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Sat, 2 May 2026 06:18:19 -0400 Subject: [PATCH 12/16] Adds hash-based compatability check --- .../src/radio_bridge_node.cpp | 16 ++++++++ .../launch_tests/bridge_discovery_test.py | 19 ++++++++- .../test/launch_tests/mock_robot.py | 6 ++- .../test/launch_tests/version_helpers.py | 39 +++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 radio/ateam_radio_bridge/test/launch_tests/version_helpers.py diff --git a/radio/ateam_radio_bridge/src/radio_bridge_node.cpp b/radio/ateam_radio_bridge/src/radio_bridge_node.cpp index 72f485314..d80ac0e1c 100644 --- a/radio/ateam_radio_bridge/src/radio_bridge_node.cpp +++ b/radio/ateam_radio_bridge/src/radio_bridge_node.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -322,6 +323,21 @@ class RadioBridgeNode : public rclcpp::Node HelloRequest hello_data = std::get(data_variant); + const uint32_t incoming_comms_hash = hello_data.coms_hash[0] | (hello_data.coms_hash[1] << 8) | + (hello_data.coms_hash[2] << 16) | (hello_data.coms_hash[3] << 24); + if (incoming_comms_hash != ateam_radio_msgs::COMMS_HASH) { + RCLCPP_WARN(get_logger(), "Ignoring discovery packet. Packet version hash mismatch."); + return; + } + + if (ateam_radio_msgs::COMMS_DIRTY) { + RCLCPP_WARN(get_logger(), "Local packet version is dirty. Compatibility check may be unreliable."); + } + + if (hello_data.coms_repo_dirty) { + RCLCPP_WARN(get_logger(), "Remote robot's packet version is dirty. Compatibility check may be unreliable."); + } + if (!(game_controller_listener_.GetTeamColor() == ateam_common::TeamColor::Blue && hello_data.color == TC_BLUE) && !(game_controller_listener_.GetTeamColor() == ateam_common::TeamColor::Yellow && diff --git a/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py b/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py index 190e6a927..c64de6c43 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py +++ b/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py @@ -11,6 +11,8 @@ import launch_testing.actions +from version_helpers import get_comms_submodule_hash, get_comms_submodule_dirty + discovery_address = "224.4.20.70" discovery_port = 42069 @@ -49,8 +51,23 @@ def tearDownClass(cls): cls.sock.close() def test_discoveryResponse(self): + dirty_flags = 0b00000001 if get_comms_submodule_dirty() else 0 + coms_hash = get_comms_submodule_hash() self.sock.sendto( - struct.pack("IBBHBBBBIII", 653411691, 21, 0, 16, 0, 0, 0, 0, 0, 0, 0), discovery_endpoint + struct.pack( + "IBBH BBBIII", + 653411691, # crc + 21, # cc Hello request + 0, # reserved + 16, # data length + 0, # robot id + 0, # color + dirty_flags, + coms_hash, + 0, # controls hash + 0, # firmware hash + ), + discovery_endpoint, ) data, _ = self.sock.recvfrom(16) global bridge_endpoint diff --git a/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py b/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py index c674cd827..14ed23679 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py +++ b/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py @@ -5,6 +5,7 @@ import struct import time from threading import Thread +from version_helpers import get_comms_submodule_hash, get_comms_submodule_dirty class MockRobot: @@ -65,6 +66,7 @@ def stopAsync(self): self._async_thread = None def _runDiscovery(self): + dirty_flags = 0b00000001 if get_comms_submodule_dirty() else 0 packet = struct.pack('IBBH BBBBIII', 0, # CRC 21, # CC Hello Req @@ -72,9 +74,9 @@ def _runDiscovery(self): 16, # Data Length self._robot_id, # Robot ID 1 if self._color == 'blue' else 0, # Team Color - 0, # dirt flags + dirty_flags, # dirty flags 0, # reserved - 0, # coms hash + get_comms_submodule_hash(), # coms hash 0, # controls hash 0 # firmware hash ) diff --git a/radio/ateam_radio_bridge/test/launch_tests/version_helpers.py b/radio/ateam_radio_bridge/test/launch_tests/version_helpers.py new file mode 100644 index 000000000..dda95a41e --- /dev/null +++ b/radio/ateam_radio_bridge/test/launch_tests/version_helpers.py @@ -0,0 +1,39 @@ +"""Git helper functions for the radio bridge tests.""" + +import clang.cindex +import subprocess +import pathlib + +def get_ateam_radio_msgs_prefix(): + """Get the path prefix for the ateam_radio_msgs package.""" + result = subprocess.run( + ["ros2", "pkg", "prefix", "ateam_radio_msgs"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to get package prefix: {result.stderr}") + return pathlib.Path(result.stdout.strip()) + +def find_version_header(): + """Return the path to the version header file.""" + return get_ateam_radio_msgs_prefix() / "include" / "ateam_radio_msgs" / "ateam_radio_msgs" / "version.hpp" + +def find_constant_value(symbol_name): + """Find the value of a symbol in the version header.""" + index = clang.cindex.Index.create() + tu = index.parse(str(find_version_header()), args=["-Xclang","-ast-dump"]) + tokens = list(tu.cursor.get_tokens()) + for token_index in range(len(tokens)): + if tokens[token_index].spelling == symbol_name and tokens[token_index + 1].spelling == "=": + return tokens[token_index + 2].spelling + raise RuntimeError(f"Failed to find constant {symbol_name} in version.hpp") + +def get_comms_submodule_hash(): + """Get the current commit hash of the software communication submodule.""" + return int(find_constant_value("COMMS_HASH")) + +def get_comms_submodule_dirty(): + """Check if the software communication submodule has uncommitted changes.""" + return bool(find_constant_value("COMMS_DIRTY")) From 983e3a8a68625a66786f9310aeff6558dcc09221 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Sat, 2 May 2026 06:27:31 -0400 Subject: [PATCH 13/16] Removes references to removed fields in motion benchmarks --- .../src/angular_velocity_benchmark.cpp | 5 +++-- .../src/velocity_benchmark.cpp | 15 +++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/motion/ateam_motion_benchmark/src/angular_velocity_benchmark.cpp b/motion/ateam_motion_benchmark/src/angular_velocity_benchmark.cpp index 37bedaf34..1304cadf7 100644 --- a/motion/ateam_motion_benchmark/src/angular_velocity_benchmark.cpp +++ b/motion/ateam_motion_benchmark/src/angular_velocity_benchmark.cpp @@ -355,8 +355,9 @@ class AngularVelocityBenchmarkNode : public rclcpp::Node const auto vision_speed = robot.state->twist_body.angular.z; const auto vision_perp_speed = robot.state->twist_body.linear.y; - const auto firmware_speed = robot.motion_feedback->cgkf_body_velocity_state_estimate[2]; - const auto firmware_perp_speed = robot.motion_feedback->cgkf_body_velocity_state_estimate[0]; + // TODO These values are not in the new packet definitions? + const auto firmware_speed = 0.0; // robot.motion_feedback-> cgkf_body_velocity_state_estimate[2]; + const auto firmware_perp_speed = 0.0; // robot.motion_feedback->cgkf_body_velocity_state_estimate[0]; DataEntry entry{ .time = std::chrono::duration_cast>( diff --git a/motion/ateam_motion_benchmark/src/velocity_benchmark.cpp b/motion/ateam_motion_benchmark/src/velocity_benchmark.cpp index 4d5f7db2b..3d4a65ce6 100644 --- a/motion/ateam_motion_benchmark/src/velocity_benchmark.cpp +++ b/motion/ateam_motion_benchmark/src/velocity_benchmark.cpp @@ -363,12 +363,15 @@ class VelocityBenchmarkNode : public rclcpp::Node robot.state->twist_body.linear.x : robot.state->twist_body.linear.y; const auto vision_perp_speed = options_.axis == Axis::X ? robot.state->twist_body.linear.y : robot.state->twist_body.linear.x; - const auto firmware_speed = options_.axis == Axis::X ? - robot.motion_feedback->cgkf_body_velocity_state_estimate[0] : - robot.motion_feedback->cgkf_body_velocity_state_estimate[1]; - const auto firmware_perp_speed = options_.axis == Axis::X ? - robot.motion_feedback->cgkf_body_velocity_state_estimate[1] : - robot.motion_feedback->cgkf_body_velocity_state_estimate[0]; + // TODO These values are not in the new packet definitions? + // const auto firmware_speed = options_.axis == Axis::X ? + // robot.motion_feedback->cgkf_body_velocity_state_estimate[0] : + // robot.motion_feedback->cgkf_body_velocity_state_estimate[1]; + // const auto firmware_perp_speed = options_.axis == Axis::X ? + // robot.motion_feedback->cgkf_body_velocity_state_estimate[1] : + // robot.motion_feedback->cgkf_body_velocity_state_estimate[0]; + const auto firmware_speed = 0.0; + const auto firmware_perp_speed = 0.0; DataEntry entry{ .time = std::chrono::duration_cast>( From d80b17e2bda4c7aa0ade7851016f287be06a35b8 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Sat, 2 May 2026 06:34:22 -0400 Subject: [PATCH 14/16] Fixes linting failures in ateam_radio_msgs --- radio/ateam_radio_msgs/cmake/generate_version_header.cmake | 6 +++++- radio/ateam_radio_msgs/scripts/generate_conversion_code.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/radio/ateam_radio_msgs/cmake/generate_version_header.cmake b/radio/ateam_radio_msgs/cmake/generate_version_header.cmake index 04f039160..ac3667f65 100644 --- a/radio/ateam_radio_msgs/cmake/generate_version_header.cmake +++ b/radio/ateam_radio_msgs/cmake/generate_version_header.cmake @@ -49,6 +49,10 @@ function(generate_version_header) set(SOFT_COMMS_SUBMODULE_DIRTY false) endif() - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/templates/version.hpp.in "${CMAKE_CURRENT_BINARY_DIR}/ateam_generated/include/version.hpp" @ONLY) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/templates/version.hpp.in + "${CMAKE_CURRENT_BINARY_DIR}/ateam_generated/include/version.hpp" + @ONLY + ) endfunction() diff --git a/radio/ateam_radio_msgs/scripts/generate_conversion_code.py b/radio/ateam_radio_msgs/scripts/generate_conversion_code.py index def5cf419..f1a10586b 100644 --- a/radio/ateam_radio_msgs/scripts/generate_conversion_code.py +++ b/radio/ateam_radio_msgs/scripts/generate_conversion_code.py @@ -204,7 +204,8 @@ def generate_union_switch_copy_lines(field_node, param_name, struct_names, selec enum_details = next(e for e in enums if e['type_name'] == selector_field.type.spelling) # Convention: enum value 0 means "none/off" (no active union member). # Remaining values sorted ascending map positionally to union members in - # declaration order, so BCM_GLOBAL_POSITION(1)->global_pos, BCM_GLOBAL_VELOCITY(2)->global_vel, etc. + # declaration order, so BCM_GLOBAL_POSITION(1)->global_pos, + # BCM_GLOBAL_VELOCITY(2)->global_vel, etc. non_zero_cases = sorted( [(name, val) for name, val in enum_details['values'] if val != 0], key=lambda x: x[1], From c758f080d57c349754d224ccc9d943e1c9deef91 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Sun, 3 May 2026 21:30:17 -0400 Subject: [PATCH 15/16] Switches unit tests to gmock in CMake --- radio/ateam_radio_bridge/test/unit_tests/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radio/ateam_radio_bridge/test/unit_tests/CMakeLists.txt b/radio/ateam_radio_bridge/test/unit_tests/CMakeLists.txt index b640cb185..60de857a1 100644 --- a/radio/ateam_radio_bridge/test/unit_tests/CMakeLists.txt +++ b/radio/ateam_radio_bridge/test/unit_tests/CMakeLists.txt @@ -1,5 +1,5 @@ -find_package(ament_cmake_gtest REQUIRED) -ament_add_gtest(unit_tests +find_package(ament_cmake_gmock REQUIRED) +ament_add_gmock(unit_tests rnp_packet_helpers_tests.cpp ) target_link_libraries(unit_tests ${PROJECT_NAME}) From 8ab14923f8e38ac46fe2fa8b87a787fd709f2840 Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Sun, 3 May 2026 21:42:19 -0400 Subject: [PATCH 16/16] Makes "coms" abbreviation more consistently spelled. --- .../src/radio_bridge_node.cpp | 6 ++--- .../launch_tests/bridge_discovery_test.py | 6 ++--- .../test/launch_tests/mock_robot.py | 6 ++--- .../test/launch_tests/version_helpers.py | 8 +++---- .../cmake/generate_version_header.cmake | 22 +++++++++---------- .../ateam_radio_msgs/templates/version.hpp.in | 4 ++-- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/radio/ateam_radio_bridge/src/radio_bridge_node.cpp b/radio/ateam_radio_bridge/src/radio_bridge_node.cpp index d80ac0e1c..a6680322e 100644 --- a/radio/ateam_radio_bridge/src/radio_bridge_node.cpp +++ b/radio/ateam_radio_bridge/src/radio_bridge_node.cpp @@ -323,14 +323,14 @@ class RadioBridgeNode : public rclcpp::Node HelloRequest hello_data = std::get(data_variant); - const uint32_t incoming_comms_hash = hello_data.coms_hash[0] | (hello_data.coms_hash[1] << 8) | + const uint32_t incoming_coms_hash = hello_data.coms_hash[0] | (hello_data.coms_hash[1] << 8) | (hello_data.coms_hash[2] << 16) | (hello_data.coms_hash[3] << 24); - if (incoming_comms_hash != ateam_radio_msgs::COMMS_HASH) { + if (incoming_coms_hash != ateam_radio_msgs::kComsHash) { RCLCPP_WARN(get_logger(), "Ignoring discovery packet. Packet version hash mismatch."); return; } - if (ateam_radio_msgs::COMMS_DIRTY) { + if (ateam_radio_msgs::kComsDirty) { RCLCPP_WARN(get_logger(), "Local packet version is dirty. Compatibility check may be unreliable."); } diff --git a/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py b/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py index c64de6c43..a1082296d 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py +++ b/radio/ateam_radio_bridge/test/launch_tests/bridge_discovery_test.py @@ -11,7 +11,7 @@ import launch_testing.actions -from version_helpers import get_comms_submodule_hash, get_comms_submodule_dirty +from version_helpers import get_coms_submodule_hash, get_coms_submodule_dirty discovery_address = "224.4.20.70" @@ -51,8 +51,8 @@ def tearDownClass(cls): cls.sock.close() def test_discoveryResponse(self): - dirty_flags = 0b00000001 if get_comms_submodule_dirty() else 0 - coms_hash = get_comms_submodule_hash() + dirty_flags = 0b00000001 if get_coms_submodule_dirty() else 0 + coms_hash = get_coms_submodule_hash() self.sock.sendto( struct.pack( "IBBH BBBIII", diff --git a/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py b/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py index ed0556468..700d3a662 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py +++ b/radio/ateam_radio_bridge/test/launch_tests/mock_robot.py @@ -5,7 +5,7 @@ import struct import time from threading import Thread -from version_helpers import get_comms_submodule_hash, get_comms_submodule_dirty +from version_helpers import get_coms_submodule_hash, get_coms_submodule_dirty class MockRobot: @@ -66,7 +66,7 @@ def stopAsync(self): self._async_thread = None def _runDiscovery(self): - dirty_flags = 0b00000001 if get_comms_submodule_dirty() else 0 + dirty_flags = 0b00000001 if get_coms_submodule_dirty() else 0 packet = struct.pack('IBBH BBBBIII', 0, # CRC 21, # CC Hello Req @@ -76,7 +76,7 @@ def _runDiscovery(self): 1 if self._color == 'blue' else 0, # Team Color dirty_flags, # dirty flags 0, # reserved - get_comms_submodule_hash(), # coms hash + get_coms_submodule_hash(), # coms hash 0, # controls hash 0 # firmware hash ) diff --git a/radio/ateam_radio_bridge/test/launch_tests/version_helpers.py b/radio/ateam_radio_bridge/test/launch_tests/version_helpers.py index dda95a41e..d47e92e1d 100644 --- a/radio/ateam_radio_bridge/test/launch_tests/version_helpers.py +++ b/radio/ateam_radio_bridge/test/launch_tests/version_helpers.py @@ -30,10 +30,10 @@ def find_constant_value(symbol_name): return tokens[token_index + 2].spelling raise RuntimeError(f"Failed to find constant {symbol_name} in version.hpp") -def get_comms_submodule_hash(): +def get_coms_submodule_hash(): """Get the current commit hash of the software communication submodule.""" - return int(find_constant_value("COMMS_HASH")) + return int(find_constant_value("kComsHash")) -def get_comms_submodule_dirty(): +def get_coms_submodule_dirty(): """Check if the software communication submodule has uncommitted changes.""" - return bool(find_constant_value("COMMS_DIRTY")) + return bool(find_constant_value("kComsDirty")) diff --git a/radio/ateam_radio_msgs/cmake/generate_version_header.cmake b/radio/ateam_radio_msgs/cmake/generate_version_header.cmake index ac3667f65..ff246e6cb 100644 --- a/radio/ateam_radio_msgs/cmake/generate_version_header.cmake +++ b/radio/ateam_radio_msgs/cmake/generate_version_header.cmake @@ -20,33 +20,33 @@ function(generate_version_header) - set(SOFT_COMMS_SUBMODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/software-communication") + set(SOFT_COMS_SUBMODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/software-communication") find_package(Git REQUIRED) execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - WORKING_DIRECTORY ${SOFT_COMMS_SUBMODULE_DIR} - OUTPUT_VARIABLE SOFT_COMMS_SUBMODULE_SHA + WORKING_DIRECTORY ${SOFT_COMS_SUBMODULE_DIR} + OUTPUT_VARIABLE SOFT_COMS_SUBMODULE_SHA OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) - string(SUBSTRING "${SOFT_COMMS_SUBMODULE_SHA}" 0 8 SOFT_COMMS_SUBMODULE_SHA_SHORT) - set(_hex_string "0x${SOFT_COMMS_SUBMODULE_SHA_SHORT}") - math(EXPR SOFT_COMMS_SUBMODULE_SHA_SHORT_NUM "${_hex_string} + 0") + string(SUBSTRING "${SOFT_COMS_SUBMODULE_SHA}" 0 8 SOFT_COMS_SUBMODULE_SHA_SHORT) + set(_hex_string "0x${SOFT_COMS_SUBMODULE_SHA_SHORT}") + math(EXPR SOFT_COMS_SUBMODULE_SHA_SHORT_NUM "${_hex_string} + 0") execute_process( COMMAND ${GIT_EXECUTABLE} status --porcelain - WORKING_DIRECTORY ${SOFT_COMMS_SUBMODULE_DIR} - OUTPUT_VARIABLE SOFT_COMMS_SUBMODULE_STATUS + WORKING_DIRECTORY ${SOFT_COMS_SUBMODULE_DIR} + OUTPUT_VARIABLE SOFT_COMS_SUBMODULE_STATUS OUTPUT_STRIP_TRAILING_WHITESPACE ) - if(SOFT_COMMS_SUBMODULE_STATUS) - set(SOFT_COMMS_SUBMODULE_DIRTY true) + if(SOFT_COMS_SUBMODULE_STATUS) + set(SOFT_COMS_SUBMODULE_DIRTY true) else() - set(SOFT_COMMS_SUBMODULE_DIRTY false) + set(SOFT_COMS_SUBMODULE_DIRTY false) endif() configure_file( diff --git a/radio/ateam_radio_msgs/templates/version.hpp.in b/radio/ateam_radio_msgs/templates/version.hpp.in index 7e5044570..7095e7a4b 100644 --- a/radio/ateam_radio_msgs/templates/version.hpp.in +++ b/radio/ateam_radio_msgs/templates/version.hpp.in @@ -23,9 +23,9 @@ namespace ateam_radio_msgs { - constexpr uint32_t COMMS_HASH = @SOFT_COMMS_SUBMODULE_SHA_SHORT_NUM@; + constexpr uint32_t kComsHash = @SOFT_COMS_SUBMODULE_SHA_SHORT_NUM@; - constexpr bool COMMS_DIRTY = @SOFT_COMMS_SUBMODULE_DIRTY@; + constexpr bool kComsDirty = @SOFT_COMS_SUBMODULE_DIRTY@; } #endif // ATEAM_RADIO_MSGS__VERSION_HPP_