From 40277772cf5abe5e1a2910e9b44026f6f9031f99 Mon Sep 17 00:00:00 2001 From: Will Stuckey Date: Wed, 5 Aug 2026 18:16:46 -0400 Subject: [PATCH 1/7] initial pass of auto proto to ros msg conversion --- .gitignore | 3 + ssl_league_msgs/.gitignore | 1 + ssl_league_msgs/CMakeLists.txt | 102 ++--- ssl_league_msgs/cmake/Ros2MsgGen.cmake | 146 +++++++ ssl_league_msgs/cmake/ateam_proto_shared.py | 69 ++++ ssl_league_msgs/cmake/protoc_gen_ros2msg.py | 391 ++++++++++++++++++ .../cmake/ssl_ros_annotations.json | 190 +++++++++ .../game_controller/common/msg/Division.msg | 5 - .../game_controller/common/msg/RobotId.msg | 2 - .../game_controller/common/msg/Team.msg | 5 - .../game_events/msg/AimlessKick.msg | 4 - .../msg/AttackerDoubleTouchedBall.msg | 3 - .../msg/AttackerTooCloseToDefenseArea.msg | 5 - .../msg/AttackerTouchedBallInDefenseArea.msg | 4 - .../AttackerTouchedOpponentInDefenseArea.msg | 4 - .../game_events/msg/BallLeftField.msg | 3 - .../game_events/msg/BotCrashDrawn.msg | 6 - .../game_events/msg/BotCrashUnique.msg | 7 - .../game_events/msg/BotDribbledBallTooFar.msg | 4 - .../game_events/msg/BotDroppedParts.msg | 4 - .../msg/BotHeldBallDeliberately.msg | 4 - .../msg/BotInterferedPlacement.msg | 3 - .../game_events/msg/BotKickedBallTooFast.msg | 5 - .../game_events/msg/BotPushedBot.msg | 5 - .../game_events/msg/BotSubstitution.msg | 1 - .../game_events/msg/BotTippedOver.msg | 4 - .../game_events/msg/BotTooFastInStop.msg | 4 - .../game_events/msg/BoundaryCrossing.msg | 2 - .../game_events/msg/ChallengeFlag.msg | 1 - .../game_events/msg/ChallengeFlagHandled.msg | 2 - .../game_events/msg/ChippedGoal.msg | 5 - .../game_events/msg/DefenderInDefenseArea.msg | 4 - .../msg/DefenderInDefenseAreaPartially.msg | 5 - .../msg/DefenderTooCloseToKickPoint.msg | 4 - .../game_events/msg/EmergencyStop.msg | 1 - .../msg/ExcessiveBotSubstitution.msg | 1 - .../game_controller/game_events/msg/Goal.msg | 9 - .../game_events/msg/IndirectGoal.msg | 4 - .../game_events/msg/KeeperHeldBall.msg | 3 - .../game_events/msg/KickTimeout.msg | 3 - .../game_events/msg/MultipleCards.msg | 1 - .../game_events/msg/MultipleFouls.msg | 4 - .../msg/MultiplePlacementFailures.msg | 1 - .../game_events/msg/NoProgressInGame.msg | 2 - .../game_events/msg/PenaltyKickFailed.msg | 3 - .../game_events/msg/PlacementFailed.msg | 3 - .../game_events/msg/PlacementSucceeded.msg | 4 - .../game_events/msg/Prepared.msg | 1 - .../game_events/msg/TooManyRobots.msg | 4 - .../msg/UnsportingBehaviorMajor.msg | 2 - .../msg/UnsportingBehaviorMinor.msg | 2 - .../game_controller/msg/ControllerReply.msg | 12 - .../game_controller/msg/GameEvent.msg | 103 ----- .../msg/GameEventProposalGroup.msg | 3 - .../game_controller/msg/Referee.msg | 60 --- .../game_controller/msg/TeamInfo.msg | 17 - .../simulator/msg/SimulatorControl.msg | 4 - .../simulator/msg/TeleportBallCommand.msg | 19 - .../simulator/msg/TeleportRobotCommand.msg | 20 - .../vision/msg/VisionDetectionBall.msg | 4 - .../vision/msg/VisionDetectionFrame.msg | 8 - .../vision/msg/VisionDetectionRobot.msg | 5 - .../vision/msg/VisionFieldCircularArc.msg | 6 - .../vision/msg/VisionFieldLineSegment.msg | 4 - .../msg/VisionGeometryCameraCalibration.msg | 6 - .../vision/msg/VisionGeometryData.msg | 2 - .../vision/msg/VisionGeometryFieldSize.msg | 15 - ssl_league_msgs/vision/msg/VisionWrapper.msg | 2 - 68 files changed, 825 insertions(+), 520 deletions(-) create mode 100644 .gitignore create mode 100644 ssl_league_msgs/.gitignore create mode 100644 ssl_league_msgs/cmake/Ros2MsgGen.cmake create mode 100644 ssl_league_msgs/cmake/ateam_proto_shared.py create mode 100755 ssl_league_msgs/cmake/protoc_gen_ros2msg.py create mode 100644 ssl_league_msgs/cmake/ssl_ros_annotations.json delete mode 100644 ssl_league_msgs/game_controller/common/msg/Division.msg delete mode 100644 ssl_league_msgs/game_controller/common/msg/RobotId.msg delete mode 100644 ssl_league_msgs/game_controller/common/msg/Team.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/AimlessKick.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/AttackerDoubleTouchedBall.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/AttackerTooCloseToDefenseArea.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/AttackerTouchedBallInDefenseArea.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/AttackerTouchedOpponentInDefenseArea.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BallLeftField.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotCrashDrawn.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotCrashUnique.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotDribbledBallTooFar.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotDroppedParts.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotHeldBallDeliberately.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotInterferedPlacement.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotKickedBallTooFast.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotPushedBot.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotSubstitution.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotTippedOver.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BotTooFastInStop.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/BoundaryCrossing.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/ChallengeFlag.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/ChallengeFlagHandled.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/ChippedGoal.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/DefenderInDefenseArea.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/DefenderInDefenseAreaPartially.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/DefenderTooCloseToKickPoint.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/EmergencyStop.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/ExcessiveBotSubstitution.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/Goal.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/IndirectGoal.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/KeeperHeldBall.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/KickTimeout.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/MultipleCards.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/MultipleFouls.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/MultiplePlacementFailures.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/NoProgressInGame.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/PenaltyKickFailed.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/PlacementFailed.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/PlacementSucceeded.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/Prepared.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/TooManyRobots.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/UnsportingBehaviorMajor.msg delete mode 100644 ssl_league_msgs/game_controller/game_events/msg/UnsportingBehaviorMinor.msg delete mode 100644 ssl_league_msgs/game_controller/msg/ControllerReply.msg delete mode 100644 ssl_league_msgs/game_controller/msg/GameEvent.msg delete mode 100644 ssl_league_msgs/game_controller/msg/GameEventProposalGroup.msg delete mode 100644 ssl_league_msgs/game_controller/msg/Referee.msg delete mode 100644 ssl_league_msgs/game_controller/msg/TeamInfo.msg delete mode 100644 ssl_league_msgs/simulator/msg/SimulatorControl.msg delete mode 100644 ssl_league_msgs/simulator/msg/TeleportBallCommand.msg delete mode 100644 ssl_league_msgs/simulator/msg/TeleportRobotCommand.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionDetectionBall.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionDetectionFrame.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionDetectionRobot.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionFieldCircularArc.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionFieldLineSegment.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionGeometryCameraCalibration.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionGeometryData.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionGeometryFieldSize.msg delete mode 100644 ssl_league_msgs/vision/msg/VisionWrapper.msg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1345657 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +install/ +log/ diff --git a/ssl_league_msgs/.gitignore b/ssl_league_msgs/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/ssl_league_msgs/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/ssl_league_msgs/CMakeLists.txt b/ssl_league_msgs/CMakeLists.txt index a2dd2b5..c17cb53 100644 --- a/ssl_league_msgs/CMakeLists.txt +++ b/ssl_league_msgs/CMakeLists.txt @@ -1,89 +1,37 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.16) project(ssl_league_msgs) find_package(ament_cmake REQUIRED) find_package(rosidl_default_generators REQUIRED) find_package(builtin_interfaces REQUIRED) -find_package(std_msgs REQUIRED) find_package(geometry_msgs REQUIRED) -set(GC_MSG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/game_controller) -set(VISION_MSG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vision) -set(SIMULATOR_MSG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/simulator) +include(cmake/Ros2MsgGen.cmake) + +set(_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_protobufs/proto") + +generate_ros2_msgs( + PROTO_FILES + ${_PROTO_DIR}/ssl_gc_common.proto + ${_PROTO_DIR}/ssl_gc_geometry.proto + ${_PROTO_DIR}/ssl_gc_game_event.proto + ${_PROTO_DIR}/ssl_gc_referee_message.proto + ${_PROTO_DIR}/ssl_gc_rcon.proto + ${_PROTO_DIR}/ssl_vision_detection.proto + ${_PROTO_DIR}/ssl_vision_geometry.proto + ${_PROTO_DIR}/ssl_vision_wrapper.proto + ${_PROTO_DIR}/ssl_simulation_control.proto + ${_PROTO_DIR}/ssl_simulation_config.proto + ${_PROTO_DIR}/ssl_simulation_error.proto + PROTO_PATHS + ${_PROTO_DIR} + SIDECAR + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ssl_ros_annotations.json +) rosidl_generate_interfaces(${PROJECT_NAME} - ${GC_MSG_DIR}:msg/ControllerReply.msg - ${GC_MSG_DIR}:msg/GameEvent.msg - ${GC_MSG_DIR}:msg/GameEventProposalGroup.msg - ${GC_MSG_DIR}:msg/Referee.msg - ${GC_MSG_DIR}:msg/TeamInfo.msg - - ${GC_MSG_DIR}/common:msg/Team.msg - ${GC_MSG_DIR}/common:msg/RobotId.msg - ${GC_MSG_DIR}/common:msg/Division.msg - - ${GC_MSG_DIR}/game_events:msg/DefenderInDefenseArea.msg - ${GC_MSG_DIR}/game_events:msg/TooManyRobots.msg - ${GC_MSG_DIR}/game_events:msg/BotSubstitution.msg - ${GC_MSG_DIR}/game_events:msg/EmergencyStop.msg - ${GC_MSG_DIR}/game_events:msg/UnsportingBehaviorMajor.msg - ${GC_MSG_DIR}/game_events:msg/AttackerTooCloseToDefenseArea.msg - ${GC_MSG_DIR}/game_events:msg/BotInterferedPlacement.msg - ${GC_MSG_DIR}/game_events:msg/KeeperHeldBall.msg - ${GC_MSG_DIR}/game_events:msg/BotCrashUnique.msg - ${GC_MSG_DIR}/game_events:msg/Prepared.msg - ${GC_MSG_DIR}/game_events:msg/AttackerTouchedOpponentInDefenseArea.msg - ${GC_MSG_DIR}/game_events:msg/DefenderTooCloseToKickPoint.msg - ${GC_MSG_DIR}/game_events:msg/PlacementFailed.msg - ${GC_MSG_DIR}/game_events:msg/DefenderInDefenseAreaPartially.msg - ${GC_MSG_DIR}/game_events:msg/BotDribbledBallTooFar.msg - ${GC_MSG_DIR}/game_events:msg/UnsportingBehaviorMinor.msg - ${GC_MSG_DIR}/game_events:msg/PenaltyKickFailed.msg - ${GC_MSG_DIR}/game_events:msg/KickTimeout.msg - ${GC_MSG_DIR}/game_events:msg/BotCrashDrawn.msg - ${GC_MSG_DIR}/game_events:msg/ChippedGoal.msg - ${GC_MSG_DIR}/game_events:msg/NoProgressInGame.msg - ${GC_MSG_DIR}/game_events:msg/PlacementSucceeded.msg - ${GC_MSG_DIR}/game_events:msg/BotKickedBallTooFast.msg - ${GC_MSG_DIR}/game_events:msg/MultiplePlacementFailures.msg - ${GC_MSG_DIR}/game_events:msg/ChallengeFlag.msg - ${GC_MSG_DIR}/game_events:msg/BallLeftField.msg - ${GC_MSG_DIR}/game_events:msg/Goal.msg - ${GC_MSG_DIR}/game_events:msg/BotHeldBallDeliberately.msg - ${GC_MSG_DIR}/game_events:msg/AttackerTouchedBallInDefenseArea.msg - ${GC_MSG_DIR}/game_events:msg/BotTooFastInStop.msg - ${GC_MSG_DIR}/game_events:msg/AimlessKick.msg - ${GC_MSG_DIR}/game_events:msg/IndirectGoal.msg - ${GC_MSG_DIR}/game_events:msg/BotPushedBot.msg - ${GC_MSG_DIR}/game_events:msg/MultipleFouls.msg - ${GC_MSG_DIR}/game_events:msg/BotTippedOver.msg - ${GC_MSG_DIR}/game_events:msg/AttackerDoubleTouchedBall.msg - ${GC_MSG_DIR}/game_events:msg/BoundaryCrossing.msg - ${GC_MSG_DIR}/game_events:msg/MultipleCards.msg - ${GC_MSG_DIR}/game_events:msg/BotDroppedParts.msg - ${GC_MSG_DIR}/game_events:msg/ChallengeFlagHandled.msg - ${GC_MSG_DIR}/game_events:msg/ExcessiveBotSubstitution.msg - - ${VISION_MSG_DIR}:msg/VisionDetectionBall.msg - ${VISION_MSG_DIR}:msg/VisionDetectionRobot.msg - ${VISION_MSG_DIR}:msg/VisionDetectionFrame.msg - - ${VISION_MSG_DIR}:msg/VisionFieldCircularArc.msg - ${VISION_MSG_DIR}:msg/VisionFieldLineSegment.msg - ${VISION_MSG_DIR}:msg/VisionGeometryFieldSize.msg - ${VISION_MSG_DIR}:msg/VisionGeometryCameraCalibration.msg - ${VISION_MSG_DIR}:msg/VisionGeometryData.msg - - ${VISION_MSG_DIR}:msg/VisionWrapper.msg - - ${SIMULATOR_MSG_DIR}:msg/SimulatorControl.msg - ${SIMULATOR_MSG_DIR}:msg/TeleportBallCommand.msg - ${SIMULATOR_MSG_DIR}:msg/TeleportRobotCommand.msg - - DEPENDENCIES - builtin_interfaces - std_msgs - geometry_msgs + ${GENERATED_ROS2_MSGS} + DEPENDENCIES builtin_interfaces geometry_msgs ) ament_export_dependencies(rosidl_default_runtime) diff --git a/ssl_league_msgs/cmake/Ros2MsgGen.cmake b/ssl_league_msgs/cmake/Ros2MsgGen.cmake new file mode 100644 index 0000000..2364ca8 --- /dev/null +++ b/ssl_league_msgs/cmake/Ros2MsgGen.cmake @@ -0,0 +1,146 @@ +# Ros2MsgGen.cmake +# +# Provides generate_ros2_msgs() — runs the protoc ros2msg plugin at CMake +# configure time and returns the list of generated .msg files. +# +# Usage: +# generate_ros2_msgs( +# PROTO_FILES path/to/a.proto path/to/b.proto ... +# PROTO_PATHS path/to/proto/include/dir ... # --proto_path roots +# OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/msg # default if omitted +# OPTIONAL_SUBMSG HAS_FIELD # or ERROR; default HAS_FIELD +# SIDECAR path/to/annotations.json # optional +# ) +# # After the call, ${GENERATED_ROS2_MSGS} contains the .msg file list. +# rosidl_generate_interfaces(${PROJECT_NAME} ${GENERATED_ROS2_MSGS}) +# +# OPTIONAL_SUBMSG controls handling of non-oneof message-type fields: +# HAS_FIELD (default) Emit a bool has_ presence sentinel. +# ERROR Fail the build if any such field exists, forcing the schema +# author to either move it into a oneof or switch to HAS_FIELD. +# +# SIDECAR (optional) path to a JSON annotation file. Fields listed in the +# sidecar are consumed and emitted as annotated ROS types; remaining fields +# pass through normally. See protoc_gen_ros2msg.py docstring for schema. +# +# Generation runs at configure time so .msg files exist when +# rosidl_generate_interfaces() is called. CMake re-runs automatically when +# any PROTO_FILES changes (CMAKE_CONFIGURE_DEPENDS). + +cmake_minimum_required(VERSION 3.16) + +function(generate_ros2_msgs) + cmake_parse_arguments( + _ARG + "" + "OUTPUT_DIR;OPTIONAL_SUBMSG;SIDECAR" + "PROTO_FILES;PROTO_PATHS" + ${ARGN} + ) + + # --- Validate arguments --- + if(NOT _ARG_PROTO_FILES) + message(FATAL_ERROR "generate_ros2_msgs: PROTO_FILES is required") + endif() + + # --- Defaults --- + if(NOT _ARG_OUTPUT_DIR) + set(_ARG_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/ros2_msgs") + endif() + + if(NOT _ARG_OPTIONAL_SUBMSG) + set(_ARG_OPTIONAL_SUBMSG "HAS_FIELD") + endif() + string(TOLOWER "${_ARG_OPTIONAL_SUBMSG}" _opt_submsg) + + if(NOT _opt_submsg STREQUAL "has_field" AND NOT _opt_submsg STREQUAL "error") + message(FATAL_ERROR + "generate_ros2_msgs: OPTIONAL_SUBMSG must be HAS_FIELD or ERROR, " + "got '${_ARG_OPTIONAL_SUBMSG}'" + ) + endif() + + # --- Find tools --- + find_program(_PROTOC protoc REQUIRED + DOC "protoc compiler (install via nix: protobuf)" + ) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + + # --- Plugin path (sibling of this .cmake file) --- + get_filename_component(_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + set(_PLUGIN_SRC "${_CMAKE_DIR}/protoc_gen_ros2msg.py") + + if(NOT EXISTS "${_PLUGIN_SRC}") + message(FATAL_ERROR + "generate_ros2_msgs: plugin not found at ${_PLUGIN_SRC}" + ) + endif() + + # Generate an executable wrapper in the build tree so we can pass an + # explicit Python interpreter without relying on the script's shebang. + set(_PLUGIN_WRAPPER "${CMAKE_BINARY_DIR}/protoc_gen_ros2msg") + file(WRITE "${_PLUGIN_WRAPPER}" + "#!/bin/sh\nset -e\nexec \"${Python3_EXECUTABLE}\" \"${_PLUGIN_SRC}\" \"$@\"\n" + ) + file(CHMOD "${_PLUGIN_WRAPPER}" + PERMISSIONS + OWNER_READ OWNER_WRITE OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE + ) + + # --- Output directory --- + file(MAKE_DIRECTORY "${_ARG_OUTPUT_DIR}") + + # --- Build --proto_path arguments --- + set(_proto_path_args) + foreach(_path ${_ARG_PROTO_PATHS}) + list(APPEND _proto_path_args "--proto_path=${_path}") + endforeach() + + # --- Build plugin options --- + set(_plugin_opt "optional_submsg=${_opt_submsg}") + if(_ARG_SIDECAR) + if(NOT EXISTS "${_ARG_SIDECAR}") + message(FATAL_ERROR "generate_ros2_msgs: SIDECAR file not found: ${_ARG_SIDECAR}") + endif() + string(APPEND _plugin_opt ",sidecar=${_ARG_SIDECAR}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_ARG_SIDECAR}") + endif() + + # --- Run protoc at configure time --- + execute_process( + COMMAND + "${_PROTOC}" + "--plugin=protoc-gen-ros2msg=${_PLUGIN_WRAPPER}" + "--ros2msg_opt=${_plugin_opt}" + "--ros2msg_out=${_ARG_OUTPUT_DIR}" + ${_proto_path_args} + ${_ARG_PROTO_FILES} + RESULT_VARIABLE _result + ERROR_VARIABLE _stderr + OUTPUT_QUIET + ) + + if(NOT _result EQUAL 0) + message(FATAL_ERROR + "generate_ros2_msgs: protoc failed (exit ${_result}):\n${_stderr}" + ) + endif() + + # Re-run CMake configure when any proto file changes. + set_property( + DIRECTORY APPEND PROPERTY + CMAKE_CONFIGURE_DEPENDS ${_ARG_PROTO_FILES} + ) + + # Collect results and expose to caller. + file(GLOB _generated "${_ARG_OUTPUT_DIR}/*.msg") + if(NOT _generated) + message(FATAL_ERROR + "generate_ros2_msgs: no .msg files found in ${_ARG_OUTPUT_DIR} after generation" + ) + endif() + + set(GENERATED_ROS2_MSGS "${_generated}" PARENT_SCOPE) +endfunction() diff --git a/ssl_league_msgs/cmake/ateam_proto_shared.py b/ssl_league_msgs/cmake/ateam_proto_shared.py new file mode 100644 index 0000000..9d72dca --- /dev/null +++ b/ssl_league_msgs/cmake/ateam_proto_shared.py @@ -0,0 +1,69 @@ +""" +Shared helpers used by both protoc_gen_ros2msg.py and protoc_gen_ros2cpp.py. + +Factored out to avoid duplication; import with: + from ateam_proto_shared import ( + parse_options, flatten_type_name, strip_package, + build_map_entry_type_names, iter_messages, + ) +""" + +from google.protobuf import descriptor_pb2 + + +def parse_options(parameter: str) -> dict: + if not parameter: + return {} + return dict(kv.split("=", 1) for kv in parameter.split(",") if "=" in kv) + + +def flatten_type_name(type_name: str) -> str: + """Convert a fully-qualified proto type name to a flat ROS2/C++-compatible name. + + Package components (conventionally all-lowercase) are stripped; nested type + components (CamelCase, start with uppercase) are joined with '_'. + + Examples: + .ateam.BasicControl → BasicControl + .GameEvent.BallLeftField → GameEvent_BallLeftField + .ateam_test.OuterMessage.Inner → OuterMessage_Inner + """ + parts = type_name.lstrip(".").split(".") + type_parts = [p for p in parts if p and p[0].isupper()] + return "_".join(type_parts) if type_parts else parts[-1] + + +# Alias preserved for callers that import the name directly. +strip_package = flatten_type_name + + +def build_map_entry_type_names(request) -> frozenset: + """Return fully-qualified field.type_name values that are synthetic map-entry types.""" + result: set = set() + + def _walk(msg, parent_fqn: str) -> None: + fqn = f"{parent_fqn}.{msg.name}" + if msg.options.map_entry: + result.add(fqn) + for nested in msg.nested_type: + _walk(nested, fqn) + + for fd in request.proto_file: + pkg_prefix = f".{fd.package}" if fd.package else "" + for msg in fd.message_type: + _walk(msg, pkg_prefix) + + return frozenset(result) + + +def iter_messages(fd): + """Yield (flat_name, msg) for all non-map-entry messages in fd, including nested.""" + def _walk(msg, parent_flat: str): + flat = f"{parent_flat}_{msg.name}" if parent_flat else msg.name + if not msg.options.map_entry: + yield flat, msg + for nested in msg.nested_type: + yield from _walk(nested, flat) + + for msg in fd.message_type: + yield from _walk(msg, "") diff --git a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py new file mode 100755 index 0000000..8810eb3 --- /dev/null +++ b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +""" +protoc plugin: generates ROS2 .msg files from .proto definitions. + +Invoked by protoc as a subprocess; reads CodeGeneratorRequest from stdin, +writes CodeGeneratorResponse to stdout (standard protoc plugin protocol). + +Type mappings: + Proto scalar → ROS2 primitive + message Foo → Foo.msg (separate file, referenced by name) + enum Foo → Foo.msg (constants-only message) + repeated T → T[] field + oneof foo → uint8 foo_case + constants + all arm fields + enum field → int32 (ROS2 has no enum type; constants in Foo.msg) + nested message Foo.Bar → Bar.msg with flat name Foo_Bar + map field → skipped (no ROS2 map type) + +Options (via --ros2msg_opt=key=value,key=value): + optional_submsg=has_field (default) Emit bool has_ before each + non-oneof message-type field. + optional_submsg=error Reject any non-oneof message-type field as a + build error; forces schema authors to be explicit. + sidecar= Path to a JSON annotation file. Fields listed in + the sidecar are consumed and emitted as annotated + ROS types; remaining fields pass through normally. + +Sidecar annotation format (per message): + "MsgName": { + "fields": { + "proto_field": { "ros_type": "...", "ros_field": "...", "scale": ... } + }, + "outputs": [ + { + "ros_field": "name", "ros_type": "geometry_msgs/Point32", + "from": { "ros_component": "proto_field" }, + "scale": 1e-3 + }, + { + "ros_field": "pose", "ros_type": "geometry_msgs/Pose", + "position": { "from": { "x": "tx", ... }, "scale": 1e-3 }, + "orientation": { "from": { "x": "q0", ... } } + } + ] + } + + Consumed fields (right-hand side of all "from" maps, plus all "fields" keys) + are excluded from passthrough. Generator errors on unknown field references + or user-supplied conversion_func values (only intrinsic conversions supported). + +Intrinsic conversions (triggered by ros_type only, no conversion_func needed): + builtin_interfaces/Time float/double proto field → from_seconds() + int/uint proto field → from_microseconds() + builtin_interfaces/Duration same rules as Time +""" + +import json +import sys +from pathlib import Path +from google.protobuf.compiler import plugin_pb2 +from google.protobuf import descriptor_pb2 + +sys.path.insert(0, str(Path(__file__).parent)) +from ateam_proto_shared import ( # noqa: E402 + parse_options, + flatten_type_name, + build_map_entry_type_names, + iter_messages, +) + +# Public alias: tests may import plugin.strip_package. +strip_package = flatten_type_name + +FD = descriptor_pb2.FieldDescriptorProto + +SCALAR_TYPE_MAP = { + FD.TYPE_DOUBLE: "float64", + FD.TYPE_FLOAT: "float32", + FD.TYPE_INT64: "int64", + FD.TYPE_UINT64: "uint64", + FD.TYPE_INT32: "int32", + FD.TYPE_FIXED64: "uint64", + FD.TYPE_FIXED32: "uint32", + FD.TYPE_BOOL: "bool", + FD.TYPE_STRING: "string", + FD.TYPE_BYTES: "uint8[]", + FD.TYPE_UINT32: "uint32", + FD.TYPE_SINT32: "int32", + FD.TYPE_SINT64: "int64", + FD.TYPE_SFIXED32: "int32", + FD.TYPE_SFIXED64: "int64", +} + +_FLOAT_TYPES = frozenset({FD.TYPE_FLOAT, FD.TYPE_DOUBLE}) +_INT_TYPES = frozenset({ + FD.TYPE_INT32, FD.TYPE_INT64, FD.TYPE_UINT32, FD.TYPE_UINT64, + FD.TYPE_SINT32, FD.TYPE_SINT64, FD.TYPE_FIXED32, FD.TYPE_FIXED64, + FD.TYPE_SFIXED32, FD.TYPE_SFIXED64, +}) + +_INTRINSIC_ROS_TYPES = frozenset({ + "builtin_interfaces/Time", + "builtin_interfaces/Duration", +}) + + +def ros2_field_type(field: descriptor_pb2.FieldDescriptorProto) -> str: + if field.type in SCALAR_TYPE_MAP: + return SCALAR_TYPE_MAP[field.type] + if field.type == FD.TYPE_ENUM: + return "int32" + if field.type == FD.TYPE_MESSAGE: + return flatten_type_name(field.type_name) + raise ValueError(f"unhandled proto field type {field.type} in field '{field.name}'") + + +def iter_enums(fd): + """Yield (flat_name, enum) for all enums in fd, including those nested in messages.""" + for enum in fd.enum_type: + yield enum.name, enum + + def _walk(msg, parent_flat: str): + flat = f"{parent_flat}_{msg.name}" if parent_flat else msg.name + for enum in msg.enum_type: + yield f"{flat}_{enum.name}", enum + for nested in msg.nested_type: + yield from _walk(nested, flat) + + for msg in fd.message_type: + yield from _walk(msg, "") + + +def generate_enum_msg(enum: descriptor_pb2.EnumDescriptorProto) -> str: + lines = [f"# Generated from proto enum {enum.name}"] + for v in enum.value: + lines.append(f"int32 {v.name}={v.number}") + return "\n".join(lines) + "\n" + + +# --------------------------------------------------------------------------- +# Sidecar helpers +# --------------------------------------------------------------------------- + +def load_sidecar(path: str | None) -> dict: + if not path: + return {} + with open(path) as f: + return json.load(f) + + +def _output_proto_fields(out: dict): + """Yield proto field names consumed by one outputs entry.""" + for v in out.get("from", {}).values(): + yield v + for sub in ("position", "orientation"): + if sub in out: + for v in out[sub].get("from", {}).values(): + yield v + + +def _sidecar_consumed(sidecar_entry: dict) -> frozenset: + """Return the set of proto field names consumed by a message's sidecar entry.""" + consumed = set(sidecar_entry.get("fields", {}).keys()) + for out in sidecar_entry.get("outputs", []): + consumed.update(_output_proto_fields(out)) + return frozenset(consumed) + + +def _validate_sidecar_entry(sidecar_entry: dict, proto_field_map: dict, flat_name: str, errors: list): + for proto_name, ann in sidecar_entry.get("fields", {}).items(): + if proto_name not in proto_field_map: + errors.append(f"{flat_name}: sidecar 'fields' references unknown proto field '{proto_name}'") + if "conversion_func" in ann: + errors.append( + f"{flat_name}.{proto_name}: user-supplied 'conversion_func' is not supported; " + f"only intrinsic conversions (builtin_interfaces/Time, /Duration) are available." + ) + for out in sidecar_entry.get("outputs", []): + ros_field = out.get("ros_field", "") + for proto_name in _output_proto_fields(out): + if proto_name not in proto_field_map: + errors.append( + f"{flat_name}: sidecar output '{ros_field}' references unknown proto field '{proto_name}'" + ) + if "conversion_func" in out: + errors.append( + f"{flat_name}: output '{ros_field}': user-supplied 'conversion_func' is not supported." + ) + + +def _emit_sidecar_outputs(sidecar_entry: dict, lines: list): + """Emit ROS struct fields from outputs entries.""" + for out in sidecar_entry.get("outputs", []): + lines.append(f"{out['ros_type']} {out['ros_field']}") + + +def _emit_sidecar_fields( + sidecar_entry: dict, + proto_field_map: dict, + lines: list, + proto2: bool, + errors: list, + flat_name: str, +): + """Emit annotated single-field overrides (ros_type, ros_field, scale).""" + for proto_name, ann in sidecar_entry.get("fields", {}).items(): + pf = proto_field_map.get(proto_name) + if pf is None: + continue # already captured in validation + + ros_name = ann.get("ros_field", proto_name) + + if "ros_type" in ann: + ros_type = ann["ros_type"] + if ros_type in _INTRINSIC_ROS_TYPES and pf.type not in (_FLOAT_TYPES | _INT_TYPES): + # Non-scalar source for Time/Duration — still emit, bridge layer will handle + pass + elif "scale" in ann and pf.type not in _FLOAT_TYPES: + errors.append( + f"{flat_name}.{proto_name}: 'scale' on non-float field " + f"({SCALAR_TYPE_MAP.get(pf.type, f'type={pf.type}')}) requires an explicit " + f"'ros_type' — scaling an integer without type promotion loses precision." + ) + continue + else: + ros_type = ros2_field_type(pf) + + is_repeated = pf.label == FD.LABEL_REPEATED + in_oneof = pf.HasField("oneof_index") + is_proto2_optional = proto2 and pf.label == FD.LABEL_OPTIONAL and not in_oneof + + if is_repeated or is_proto2_optional: + lines.append(f"{ros_type}[] {ros_name}") + else: + lines.append(f"{ros_type} {ros_name}") + + +# --------------------------------------------------------------------------- +# Main message generator +# --------------------------------------------------------------------------- + +def generate_message_msg( + msg: descriptor_pb2.DescriptorProto, + flat_name: str, + optional_submsg: str, + errors: list, + map_entry_type_names: frozenset, + proto2: bool = False, + sidecar_entry: dict | None = None, +) -> str: + if sidecar_entry is None: + sidecar_entry = {} + + lines = [f"# Generated from proto message {flat_name}"] + + proto_field_map = {f.name: f for f in msg.field} + + # Validate sidecar references before emitting anything. + _validate_sidecar_entry(sidecar_entry, proto_field_map, flat_name, errors) + + consumed = _sidecar_consumed(sidecar_entry) + + # 1. Sidecar outputs (multi-field → ROS struct). + _emit_sidecar_outputs(sidecar_entry, lines) + + # 2. Sidecar field overrides (single-field with ros_type / ros_field / scale). + _emit_sidecar_fields(sidecar_entry, proto_field_map, lines, proto2, errors, flat_name) + + # 3. Passthrough: proto fields not consumed by the sidecar. + emitted_oneofs: set = set() + + for field in msg.field: + if field.name in consumed: + continue + if field.type == FD.TYPE_MESSAGE and field.type_name in map_entry_type_names: + continue + + is_repeated = field.label == FD.LABEL_REPEATED + in_oneof = field.HasField("oneof_index") + is_proto2_optional = ( + proto2 + and field.label == FD.LABEL_OPTIONAL + and not in_oneof + ) + + if in_oneof: + oi = field.oneof_index + if oi in emitted_oneofs: + continue + emitted_oneofs.add(oi) + + oneof_name = msg.oneof_decl[oi].name + oneof_fields = [ + f for f in msg.field + if f.HasField("oneof_index") and f.oneof_index == oi + ] + + lines.append("") + lines.append(f"# oneof {oneof_name}") + lines.append(f"# case constants use proto field numbers (stable across reordering)") + lines.append(f"uint8 ONEOF_{oneof_name.upper()}_NONE=0") + for of in oneof_fields: + if of.number > 255: + errors.append( + f"{flat_name}: oneof '{oneof_name}' field '{of.name}' has field " + f"number {of.number} which exceeds the uint8 range (max 255) used " + f"for the case discriminant. Use field numbers ≤ 255 in oneof " + f"declarations, or file a request to widen the discriminant type." + ) + continue + const = f"ONEOF_{oneof_name.upper()}_{of.name.upper()}" + lines.append(f"uint8 {const}={of.number}") + lines.append(f"uint8 {oneof_name}_case") + for of in oneof_fields: + lines.append(f"{ros2_field_type(of)} {of.name}") + continue + + ros2_type = ros2_field_type(field) + + if is_repeated or is_proto2_optional: + lines.append(f"{ros2_type}[] {field.name}") + elif field.type == FD.TYPE_MESSAGE and not is_repeated: + if proto2: + lines.append(f"{ros2_type} {field.name}") + elif optional_submsg == "error": + errors.append( + f"{flat_name}.{field.name}: non-oneof message-type field has " + f"implicit proto3 presence — set optional_submsg=has_field to " + f"auto-generate a bool presence flag, or move into a oneof." + ) + continue + else: + lines.append(f"bool has_{field.name}") + lines.append(f"{ros2_type} {field.name}") + else: + lines.append(f"{ros2_type} {field.name}") + + return "\n".join(lines) + "\n" + + +def main() -> None: + data = sys.stdin.buffer.read() + request = plugin_pb2.CodeGeneratorRequest() + request.ParseFromString(data) + + response = plugin_pb2.CodeGeneratorResponse() + response.supported_features = ( + plugin_pb2.CodeGeneratorResponse.FEATURE_PROTO3_OPTIONAL + ) + + opts = parse_options(request.parameter) + optional_submsg = opts.get("optional_submsg", "has_field") + if optional_submsg not in ("has_field", "error"): + response.error = ( + f"Unknown optional_submsg={optional_submsg!r}. " + f"Valid values: 'has_field', 'error'." + ) + sys.stdout.buffer.write(response.SerializeToString()) + return + + sidecar_path = opts.get("sidecar", None) + sidecar = load_sidecar(sidecar_path) + + map_entry_type_names = build_map_entry_type_names(request) + all_files = {f.name: f for f in request.proto_file} + errors: list = [] + + for file_name in request.file_to_generate: + fd = all_files[file_name] + + for flat_name, enum in iter_enums(fd): + out = response.file.add() + out.name = f"{flat_name}.msg" + out.content = generate_enum_msg(enum) + + proto2 = (fd.syntax != "proto3") + for flat_name, msg in iter_messages(fd): + out = response.file.add() + out.name = f"{flat_name}.msg" + out.content = generate_message_msg( + msg, flat_name, optional_submsg, errors, map_entry_type_names, + proto2, sidecar_entry=sidecar.get(flat_name, {}) + ) + + if errors: + response.error = "\n".join(errors) + + sys.stdout.buffer.write(response.SerializeToString()) + + +if __name__ == "__main__": + main() diff --git a/ssl_league_msgs/cmake/ssl_ros_annotations.json b/ssl_league_msgs/cmake/ssl_ros_annotations.json new file mode 100644 index 0000000..6a41ee6 --- /dev/null +++ b/ssl_league_msgs/cmake/ssl_ros_annotations.json @@ -0,0 +1,190 @@ +{ + "_comment": "Sidecar annotation file for SSL league proto → ROS2 msg generation.", + "_schema": { + "fields": "Map of proto_field_name → annotation. Consumed by the annotation; not emitted as passthrough.", + "outputs": "List of multi-field → single ROS struct rules. All proto fields referenced in 'from' (or nested position/orientation 'from') are consumed.", + "from": "Map of ros_component_name → proto_field_name (direction: proto → ROS).", + "scale": "Multiply all consumed scalar components by this factor (metadata; applied at bridge layer).", + "ros_type": "Override emitted ROS type. Required for builtin_interfaces/Time and /Duration (triggers intrinsic conversion based on proto wire type: float/double → from_seconds, int/uint → from_microseconds).", + "ros_field": "Override output field name. Omit to keep proto field name.", + "conversion_func": "Reserved for future user-supplied conversions. Only intrinsic conversions (Time/Duration) are supported now; generator errors on any user-supplied value." + }, + + "SSL_DetectionBall": { + "outputs": [ + { + "ros_field": "pos", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "x", "y": "y", "z": "z" }, + "scale": 1e-3, + "_consumed": ["x", "y", "z"], + "_comment": "World position in mm → m" + }, + { + "ros_field": "pixel", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "pixel_x", "y": "pixel_y" }, + "_consumed": ["pixel_x", "pixel_y"], + "_comment": "Image-space pixel coordinates, no scale" + } + ] + }, + + "SSL_DetectionRobot": { + "outputs": [ + { + "ros_field": "pos", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "x", "y": "y" }, + "scale": 1e-3, + "_consumed": ["x", "y"], + "_comment": "World position in mm → m; z omitted (2D system)" + }, + { + "ros_field": "pixel", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "pixel_x", "y": "pixel_y" }, + "_consumed": ["pixel_x", "pixel_y"] + } + ], + "fields": { + "height": { "scale": 1e-3 } + } + }, + + "SSL_DetectionFrame": { + "fields": { + "t_capture": { "ros_type": "builtin_interfaces/Time" }, + "t_sent": { "ros_type": "builtin_interfaces/Time" }, + "t_capture_camera": { "ros_type": "builtin_interfaces/Time" } + } + }, + + "SSL_GeometryCameraCalibration": { + "outputs": [ + { + "ros_field": "pose", + "ros_type": "geometry_msgs/Pose", + "position": { + "from": { "x": "tx", "y": "ty", "z": "tz" }, + "scale": 1e-3, + "_comment": "Camera translation in mm → m" + }, + "orientation": { + "from": { "x": "q0", "y": "q1", "z": "q2", "w": "q3" }, + "_comment": "SSL convention: q0=x q1=y q2=z q3=w (non-standard order)" + }, + "_consumed": ["tx", "ty", "tz", "q0", "q1", "q2", "q3"] + }, + { + "ros_field": "principal_point", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "principal_point_x", "y": "principal_point_y" }, + "_consumed": ["principal_point_x", "principal_point_y"] + }, + { + "ros_field": "derived_camera_world_t", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "derived_camera_world_tx", "y": "derived_camera_world_ty", "z": "derived_camera_world_tz" }, + "scale": 1e-3, + "_consumed": ["derived_camera_world_tx", "derived_camera_world_ty", "derived_camera_world_tz"] + } + ] + }, + + "SSL_GeometryFieldSize": { + "fields": { + "field_length": { "scale": 1e-3, "ros_type": "float32" }, + "field_width": { "scale": 1e-3, "ros_type": "float32" }, + "goal_width": { "scale": 1e-3, "ros_type": "float32" }, + "goal_depth": { "scale": 1e-3, "ros_type": "float32" }, + "boundary_width": { "scale": 1e-3, "ros_type": "float32" }, + "penalty_area_depth": { "scale": 1e-3, "ros_type": "float32" }, + "penalty_area_width": { "scale": 1e-3, "ros_type": "float32" }, + "center_circle_radius": { "scale": 1e-3, "ros_type": "float32" }, + "line_thickness": { "scale": 1e-3, "ros_type": "float32" }, + "goal_center_to_penalty_mark": { "scale": 1e-3, "ros_type": "float32" }, + "goal_height": { "scale": 1e-3, "ros_type": "float32" }, + "ball_radius": { "scale": 1e-3 }, + "max_robot_radius": { "scale": 1e-3 } + } + }, + + "SSL_FieldCircularArc": { + "fields": { + "radius": { "scale": 1e-3 }, + "thickness": { "scale": 1e-3 } + } + }, + + "SSL_FieldLineSegment": { + "fields": { + "thickness": { "scale": 1e-3 } + } + }, + + "Vector2f": { + "fields": { + "x": { "scale": 1e-3 }, + "y": { "scale": 1e-3 } + } + }, + + "SSL_GeometryData": { + "fields": { + "calib": { "ros_field": "calibration" } + } + }, + + "Referee": { + "fields": { + "packet_timestamp": { "ros_type": "builtin_interfaces/Time" }, + "command_timestamp": { "ros_type": "builtin_interfaces/Time" }, + "stage_time_left": { "ros_type": "builtin_interfaces/Duration" }, + "current_action_time_remaining": { "ros_type": "builtin_interfaces/Duration" } + } + }, + + "Referee_Point": { + "fields": { + "x": { "scale": 1e-3 }, + "y": { "scale": 1e-3 } + } + }, + + "KeeperHeldBall": { + "fields": { + "duration": { "ros_type": "builtin_interfaces/Duration" } + } + }, + + "BotHeldBallDeliberately": { + "fields": { + "duration": { "ros_type": "builtin_interfaces/Duration" } + } + }, + + "KickTimeout": { + "fields": { + "time": { "ros_type": "builtin_interfaces/Duration" } + } + }, + + "NoProgressInGame": { + "fields": { + "time": { "ros_type": "builtin_interfaces/Duration" } + } + }, + + "PlacementSucceeded": { + "fields": { + "time_taken": { "ros_type": "builtin_interfaces/Duration" } + } + }, + + "Prepared": { + "fields": { + "time_taken": { "ros_type": "builtin_interfaces/Duration" } + } + } +} diff --git a/ssl_league_msgs/game_controller/common/msg/Division.msg b/ssl_league_msgs/game_controller/common/msg/Division.msg deleted file mode 100644 index b012f6d..0000000 --- a/ssl_league_msgs/game_controller/common/msg/Division.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint8 division - -uint8 DIVISON_UNKNOWN = 0 -uint8 DIVISION_DIV_A = 1 -uint8 DIVISION_DIV_B = 2 diff --git a/ssl_league_msgs/game_controller/common/msg/RobotId.msg b/ssl_league_msgs/game_controller/common/msg/RobotId.msg deleted file mode 100644 index 698aaff..0000000 --- a/ssl_league_msgs/game_controller/common/msg/RobotId.msg +++ /dev/null @@ -1,2 +0,0 @@ -uint32[] id -ssl_league_msgs/Team[] team diff --git a/ssl_league_msgs/game_controller/common/msg/Team.msg b/ssl_league_msgs/game_controller/common/msg/Team.msg deleted file mode 100644 index 375dfa7..0000000 --- a/ssl_league_msgs/game_controller/common/msg/Team.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint8 color - -uint8 COLOR_UNKNOWN = 0 -uint8 COLOR_YELLOW = 1 -uint8 COLOR_BLUE = 2 diff --git a/ssl_league_msgs/game_controller/game_events/msg/AimlessKick.msg b/ssl_league_msgs/game_controller/game_events/msg/AimlessKick.msg deleted file mode 100644 index 36f4e95..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/AimlessKick.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -geometry_msgs/Point32[] kick_location diff --git a/ssl_league_msgs/game_controller/game_events/msg/AttackerDoubleTouchedBall.msg b/ssl_league_msgs/game_controller/game_events/msg/AttackerDoubleTouchedBall.msg deleted file mode 100644 index 61d9407..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/AttackerDoubleTouchedBall.msg +++ /dev/null @@ -1,3 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location diff --git a/ssl_league_msgs/game_controller/game_events/msg/AttackerTooCloseToDefenseArea.msg b/ssl_league_msgs/game_controller/game_events/msg/AttackerTooCloseToDefenseArea.msg deleted file mode 100644 index b51ecbf..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/AttackerTooCloseToDefenseArea.msg +++ /dev/null @@ -1,5 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -float32[] distance -geometry_msgs/Point32[] ball_location diff --git a/ssl_league_msgs/game_controller/game_events/msg/AttackerTouchedBallInDefenseArea.msg b/ssl_league_msgs/game_controller/game_events/msg/AttackerTouchedBallInDefenseArea.msg deleted file mode 100644 index d7fd7ef..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/AttackerTouchedBallInDefenseArea.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -float32[] distance diff --git a/ssl_league_msgs/game_controller/game_events/msg/AttackerTouchedOpponentInDefenseArea.msg b/ssl_league_msgs/game_controller/game_events/msg/AttackerTouchedOpponentInDefenseArea.msg deleted file mode 100644 index 93e10b5..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/AttackerTouchedOpponentInDefenseArea.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -uint32[] victim -geometry_msgs/Point32[] location diff --git a/ssl_league_msgs/game_controller/game_events/msg/BallLeftField.msg b/ssl_league_msgs/game_controller/game_events/msg/BallLeftField.msg deleted file mode 100644 index 61d9407..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BallLeftField.msg +++ /dev/null @@ -1,3 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotCrashDrawn.msg b/ssl_league_msgs/game_controller/game_events/msg/BotCrashDrawn.msg deleted file mode 100644 index 9278070..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotCrashDrawn.msg +++ /dev/null @@ -1,6 +0,0 @@ -uint32[] bot_yellow -uint32[] bot_blue -geometry_msgs/Point32[] location -float32[] crash_speed -float32[] speed_diff -float32[] crash_angle diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotCrashUnique.msg b/ssl_league_msgs/game_controller/game_events/msg/BotCrashUnique.msg deleted file mode 100644 index f9ee9ec..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotCrashUnique.msg +++ /dev/null @@ -1,7 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] violator -uint32[] victim -geometry_msgs/Point32[] location -float32[] crash_speed -float32[] speed_diff -float32[] crash_angle diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotDribbledBallTooFar.msg b/ssl_league_msgs/game_controller/game_events/msg/BotDribbledBallTooFar.msg deleted file mode 100644 index a74eb6b..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotDribbledBallTooFar.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] start -geometry_msgs/Point32[] end diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotDroppedParts.msg b/ssl_league_msgs/game_controller/game_events/msg/BotDroppedParts.msg deleted file mode 100644 index 73df4d6..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotDroppedParts.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -geometry_msgs/Point32[] ball_location diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotHeldBallDeliberately.msg b/ssl_league_msgs/game_controller/game_events/msg/BotHeldBallDeliberately.msg deleted file mode 100644 index c4009d9..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotHeldBallDeliberately.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -float32[] duration diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotInterferedPlacement.msg b/ssl_league_msgs/game_controller/game_events/msg/BotInterferedPlacement.msg deleted file mode 100644 index 61d9407..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotInterferedPlacement.msg +++ /dev/null @@ -1,3 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotKickedBallTooFast.msg b/ssl_league_msgs/game_controller/game_events/msg/BotKickedBallTooFast.msg deleted file mode 100644 index 100e0d4..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotKickedBallTooFast.msg +++ /dev/null @@ -1,5 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -float32[] initial_ball_speed -bool[] chipped diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotPushedBot.msg b/ssl_league_msgs/game_controller/game_events/msg/BotPushedBot.msg deleted file mode 100644 index f9314dc..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotPushedBot.msg +++ /dev/null @@ -1,5 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] violator -uint32[] victim -geometry_msgs/Point32[] location -float32[] pushed_distance diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotSubstitution.msg b/ssl_league_msgs/game_controller/game_events/msg/BotSubstitution.msg deleted file mode 100644 index fbf970d..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotSubstitution.msg +++ /dev/null @@ -1 +0,0 @@ -ssl_league_msgs/Team by_team diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotTippedOver.msg b/ssl_league_msgs/game_controller/game_events/msg/BotTippedOver.msg deleted file mode 100644 index 73df4d6..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotTippedOver.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -geometry_msgs/Point32[] ball_location diff --git a/ssl_league_msgs/game_controller/game_events/msg/BotTooFastInStop.msg b/ssl_league_msgs/game_controller/game_events/msg/BotTooFastInStop.msg deleted file mode 100644 index 7609dc0..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BotTooFastInStop.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -float32[] speed diff --git a/ssl_league_msgs/game_controller/game_events/msg/BoundaryCrossing.msg b/ssl_league_msgs/game_controller/game_events/msg/BoundaryCrossing.msg deleted file mode 100644 index f0766ea..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/BoundaryCrossing.msg +++ /dev/null @@ -1,2 +0,0 @@ -ssl_league_msgs/Team by_team -geometry_msgs/Point32[] location diff --git a/ssl_league_msgs/game_controller/game_events/msg/ChallengeFlag.msg b/ssl_league_msgs/game_controller/game_events/msg/ChallengeFlag.msg deleted file mode 100644 index fbf970d..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/ChallengeFlag.msg +++ /dev/null @@ -1 +0,0 @@ -ssl_league_msgs/Team by_team diff --git a/ssl_league_msgs/game_controller/game_events/msg/ChallengeFlagHandled.msg b/ssl_league_msgs/game_controller/game_events/msg/ChallengeFlagHandled.msg deleted file mode 100644 index 7d4cb72..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/ChallengeFlagHandled.msg +++ /dev/null @@ -1,2 +0,0 @@ -ssl_league_msgs/Team by_team -bool accepted diff --git a/ssl_league_msgs/game_controller/game_events/msg/ChippedGoal.msg b/ssl_league_msgs/game_controller/game_events/msg/ChippedGoal.msg deleted file mode 100644 index b3b6a4d..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/ChippedGoal.msg +++ /dev/null @@ -1,5 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -geometry_msgs/Point32[] kick_location -float32[] max_ball_height diff --git a/ssl_league_msgs/game_controller/game_events/msg/DefenderInDefenseArea.msg b/ssl_league_msgs/game_controller/game_events/msg/DefenderInDefenseArea.msg deleted file mode 100644 index d7fd7ef..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/DefenderInDefenseArea.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -float32[] distance diff --git a/ssl_league_msgs/game_controller/game_events/msg/DefenderInDefenseAreaPartially.msg b/ssl_league_msgs/game_controller/game_events/msg/DefenderInDefenseAreaPartially.msg deleted file mode 100644 index b51ecbf..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/DefenderInDefenseAreaPartially.msg +++ /dev/null @@ -1,5 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -float32[] distance -geometry_msgs/Point32[] ball_location diff --git a/ssl_league_msgs/game_controller/game_events/msg/DefenderTooCloseToKickPoint.msg b/ssl_league_msgs/game_controller/game_events/msg/DefenderTooCloseToKickPoint.msg deleted file mode 100644 index d7fd7ef..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/DefenderTooCloseToKickPoint.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -float32[] distance diff --git a/ssl_league_msgs/game_controller/game_events/msg/EmergencyStop.msg b/ssl_league_msgs/game_controller/game_events/msg/EmergencyStop.msg deleted file mode 100644 index fbf970d..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/EmergencyStop.msg +++ /dev/null @@ -1 +0,0 @@ -ssl_league_msgs/Team by_team diff --git a/ssl_league_msgs/game_controller/game_events/msg/ExcessiveBotSubstitution.msg b/ssl_league_msgs/game_controller/game_events/msg/ExcessiveBotSubstitution.msg deleted file mode 100644 index fbf970d..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/ExcessiveBotSubstitution.msg +++ /dev/null @@ -1 +0,0 @@ -ssl_league_msgs/Team by_team diff --git a/ssl_league_msgs/game_controller/game_events/msg/Goal.msg b/ssl_league_msgs/game_controller/game_events/msg/Goal.msg deleted file mode 100644 index 2469f41..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/Goal.msg +++ /dev/null @@ -1,9 +0,0 @@ -ssl_league_msgs/Team by_team -ssl_league_msgs/Team[] kicking_team -uint32[] kicking_bot -geometry_msgs/Point32[] location -geometry_msgs/Point32[] kick_location -float32[] max_ball_height -uint32[] num_robots_by_team -uint64[] last_touch_by_team -string[] message diff --git a/ssl_league_msgs/game_controller/game_events/msg/IndirectGoal.msg b/ssl_league_msgs/game_controller/game_events/msg/IndirectGoal.msg deleted file mode 100644 index 36f4e95..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/IndirectGoal.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -uint32[] by_bot -geometry_msgs/Point32[] location -geometry_msgs/Point32[] kick_location diff --git a/ssl_league_msgs/game_controller/game_events/msg/KeeperHeldBall.msg b/ssl_league_msgs/game_controller/game_events/msg/KeeperHeldBall.msg deleted file mode 100644 index c15023b..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/KeeperHeldBall.msg +++ /dev/null @@ -1,3 +0,0 @@ -ssl_league_msgs/Team by_team -geometry_msgs/Point32[] location -float32[] duration diff --git a/ssl_league_msgs/game_controller/game_events/msg/KickTimeout.msg b/ssl_league_msgs/game_controller/game_events/msg/KickTimeout.msg deleted file mode 100644 index 789bdcc..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/KickTimeout.msg +++ /dev/null @@ -1,3 +0,0 @@ -ssl_league_msgs/Team by_team -geometry_msgs/Point32[] location -float32[] time diff --git a/ssl_league_msgs/game_controller/game_events/msg/MultipleCards.msg b/ssl_league_msgs/game_controller/game_events/msg/MultipleCards.msg deleted file mode 100644 index fbf970d..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/MultipleCards.msg +++ /dev/null @@ -1 +0,0 @@ -ssl_league_msgs/Team by_team diff --git a/ssl_league_msgs/game_controller/game_events/msg/MultipleFouls.msg b/ssl_league_msgs/game_controller/game_events/msg/MultipleFouls.msg deleted file mode 100644 index ae46dd7..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/MultipleFouls.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team - -# TODO(barulicm) Recursive message types aren't possible. Not sure how to handle this yet. -# ssl_league_msgs/GameEvent[] caused_game_events diff --git a/ssl_league_msgs/game_controller/game_events/msg/MultiplePlacementFailures.msg b/ssl_league_msgs/game_controller/game_events/msg/MultiplePlacementFailures.msg deleted file mode 100644 index fbf970d..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/MultiplePlacementFailures.msg +++ /dev/null @@ -1 +0,0 @@ -ssl_league_msgs/Team by_team diff --git a/ssl_league_msgs/game_controller/game_events/msg/NoProgressInGame.msg b/ssl_league_msgs/game_controller/game_events/msg/NoProgressInGame.msg deleted file mode 100644 index 8e1f913..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/NoProgressInGame.msg +++ /dev/null @@ -1,2 +0,0 @@ -geometry_msgs/Point32[] location -float32[] time diff --git a/ssl_league_msgs/game_controller/game_events/msg/PenaltyKickFailed.msg b/ssl_league_msgs/game_controller/game_events/msg/PenaltyKickFailed.msg deleted file mode 100644 index d0d9131..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/PenaltyKickFailed.msg +++ /dev/null @@ -1,3 +0,0 @@ -ssl_league_msgs/Team by_team -geometry_msgs/Point32[] location -string[] reason diff --git a/ssl_league_msgs/game_controller/game_events/msg/PlacementFailed.msg b/ssl_league_msgs/game_controller/game_events/msg/PlacementFailed.msg deleted file mode 100644 index f1c8414..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/PlacementFailed.msg +++ /dev/null @@ -1,3 +0,0 @@ -ssl_league_msgs/Team by_team -float32[] remaining_distance -float32[] nearest_own_bot_distance diff --git a/ssl_league_msgs/game_controller/game_events/msg/PlacementSucceeded.msg b/ssl_league_msgs/game_controller/game_events/msg/PlacementSucceeded.msg deleted file mode 100644 index 6ea2fb2..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/PlacementSucceeded.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -float32[] time_taken -float32[] precision -float32[] distance diff --git a/ssl_league_msgs/game_controller/game_events/msg/Prepared.msg b/ssl_league_msgs/game_controller/game_events/msg/Prepared.msg deleted file mode 100644 index bda9961..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/Prepared.msg +++ /dev/null @@ -1 +0,0 @@ -float32[] time_taken diff --git a/ssl_league_msgs/game_controller/game_events/msg/TooManyRobots.msg b/ssl_league_msgs/game_controller/game_events/msg/TooManyRobots.msg deleted file mode 100644 index 35edadb..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/TooManyRobots.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/Team by_team -int32[] num_robots_allowed -int32[] num_robots_on_field -geometry_msgs/Point32[] ball_location diff --git a/ssl_league_msgs/game_controller/game_events/msg/UnsportingBehaviorMajor.msg b/ssl_league_msgs/game_controller/game_events/msg/UnsportingBehaviorMajor.msg deleted file mode 100644 index ed644cf..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/UnsportingBehaviorMajor.msg +++ /dev/null @@ -1,2 +0,0 @@ -ssl_league_msgs/Team by_team -string reason diff --git a/ssl_league_msgs/game_controller/game_events/msg/UnsportingBehaviorMinor.msg b/ssl_league_msgs/game_controller/game_events/msg/UnsportingBehaviorMinor.msg deleted file mode 100644 index ed644cf..0000000 --- a/ssl_league_msgs/game_controller/game_events/msg/UnsportingBehaviorMinor.msg +++ /dev/null @@ -1,2 +0,0 @@ -ssl_league_msgs/Team by_team -string reason diff --git a/ssl_league_msgs/game_controller/msg/ControllerReply.msg b/ssl_league_msgs/game_controller/msg/ControllerReply.msg deleted file mode 100644 index bad4dd9..0000000 --- a/ssl_league_msgs/game_controller/msg/ControllerReply.msg +++ /dev/null @@ -1,12 +0,0 @@ -uint8 status_code -string reason -string next_token -uint8 verification - -uint8 STATUS_CODE_UNKNOWN = 0 -uint8 STATUS_CODE_OK = 1 -uint8 STATUS_CODE_REJECTED = 2 - -uint8 VERIFICATION_UNKNOWN = 0 -uint8 VERIFICATION_VERIFIED = 1 -uint8 VERIFICATION_UNVERIFIED = 2 \ No newline at end of file diff --git a/ssl_league_msgs/game_controller/msg/GameEvent.msg b/ssl_league_msgs/game_controller/msg/GameEvent.msg deleted file mode 100644 index 01f4d58..0000000 --- a/ssl_league_msgs/game_controller/msg/GameEvent.msg +++ /dev/null @@ -1,103 +0,0 @@ -string id -uint8 type -string[] origin -uint64 created_timestamp -ssl_league_msgs/BallLeftField[] ball_left_field_touch_line -ssl_league_msgs/BallLeftField[] ball_left_field_goal_line -ssl_league_msgs/AimlessKick[] aimless_kick -ssl_league_msgs/AttackerTooCloseToDefenseArea[] attacker_too_close_to_defense_area -ssl_league_msgs/DefenderInDefenseArea[] defender_in_defense_area -ssl_league_msgs/BoundaryCrossing[] boundary_crossing -ssl_league_msgs/KeeperHeldBall[] keeper_held_ball -ssl_league_msgs/BotDribbledBallTooFar[] bot_dribbled_ball_too_far -ssl_league_msgs/BotPushedBot[] bot_pushed_bot -ssl_league_msgs/BotHeldBallDeliberately[] bot_held_ball_deliberately -ssl_league_msgs/BotTippedOver[] bot_tipped_over -ssl_league_msgs/BotDroppedParts[] bot_dropped_parts -ssl_league_msgs/AttackerTouchedBallInDefenseArea[] attacker_touched_ball_in_defense_area -ssl_league_msgs/BotKickedBallTooFast[] bot_kicked_ball_too_fast -ssl_league_msgs/BotCrashUnique[] bot_crash_unique -ssl_league_msgs/BotCrashDrawn[] bot_crash_drawn -ssl_league_msgs/DefenderTooCloseToKickPoint[] defender_too_close_to_kick_point -ssl_league_msgs/BotTooFastInStop[] bot_too_fast_in_stop -ssl_league_msgs/BotInterferedPlacement[] bot_interfered_placement -ssl_league_msgs/Goal[] possible_goal -ssl_league_msgs/Goal[] goal -ssl_league_msgs/Goal[] invalid_goal -ssl_league_msgs/AttackerDoubleTouchedBall[] attacker_double_touched_ball -ssl_league_msgs/PlacementSucceeded[] placement_succeeded -ssl_league_msgs/PenaltyKickFailed[] penalty_kick_failed -ssl_league_msgs/NoProgressInGame[] no_progress_in_game -ssl_league_msgs/PlacementFailed[] placement_failed -ssl_league_msgs/MultipleCards[] multiple_cards -ssl_league_msgs/MultipleFouls[] multiple_fouls -ssl_league_msgs/BotSubstitution[] bot_substitution -ssl_league_msgs/ExcessiveBotSubstitution[] excessive_bot_substitution -ssl_league_msgs/TooManyRobots[] too_many_robots -ssl_league_msgs/ChallengeFlag[] challenge_flag -ssl_league_msgs/ChallengeFlagHandled[] challenge_flag_handled -ssl_league_msgs/EmergencyStop[] emergency_stop -ssl_league_msgs/UnsportingBehaviorMinor[] unsporting_behavior_minor -ssl_league_msgs/UnsportingBehaviorMajor[] unsporting_behavior_major -ssl_league_msgs/Prepared[] prepared -ssl_league_msgs/IndirectGoal[] indirect_goal -ssl_league_msgs/ChippedGoal[] chipped_goal -ssl_league_msgs/KickTimeout[] kick_timeout -ssl_league_msgs/AttackerTouchedOpponentInDefenseArea[] attacker_touched_opponent_in_defense_area -ssl_league_msgs/AttackerTouchedOpponentInDefenseArea[] attacker_touched_opponent_in_defense_area_skipped -ssl_league_msgs/BotCrashUnique[] bot_crash_unique_skipped -ssl_league_msgs/BotPushedBot[] bot_pushed_bot_skipped -ssl_league_msgs/DefenderInDefenseAreaPartially[] defender_in_defense_area_partially -ssl_league_msgs/MultiplePlacementFailures[] multiple_placement_failures - - -# Types -uint8 TYPE_UNKNOWN_GAME_EVENT_TYPE = 0 -uint8 TYPE_BALL_LEFT_FIELD_TOUCH_LINE = 6 -uint8 TYPE_BALL_LEFT_FIELD_GOAL_LINE = 7 -uint8 TYPE_AIMLESS_KICK = 11 -uint8 TYPE_ATTACKER_TOO_CLOSE_TO_DEFENSE_AREA = 19 -uint8 TYPE_DEFENDER_IN_DEFENSE_AREA = 31 -uint8 TYPE_BOUNDARY_CROSSING = 41 -uint8 TYPE_KEEPER_HELD_BALL = 13 -uint8 TYPE_BOT_DRIBBLED_BALL_TOO_FAR = 17 -uint8 TYPE_BOT_PUSHED_BOT = 24 -uint8 TYPE_BOT_HELD_BALL_DELIBERATELY = 26 -uint8 TYPE_BOT_TIPPED_OVER = 27 -uint8 TYPE_BOT_DROPPED_PARTS = 47 -uint8 TYPE_ATTACKER_TOUCHED_BALL_IN_DEFENSE_AREA = 15 -uint8 TYPE_BOT_KICKED_BALL_TOO_FAST = 18 -uint8 TYPE_BOT_CRASH_UNIQUE = 22 -uint8 TYPE_BOT_CRASH_DRAWN = 21 -uint8 TYPE_DEFENDER_TOO_CLOSE_TO_KICK_POINT = 29 -uint8 TYPE_BOT_TOO_FAST_IN_STOP = 28 -uint8 TYPE_BOT_INTERFERED_PLACEMENT = 20 -uint8 TYPE_EXCESSIVE_BOT_SUBSTITUTION = 48 -uint8 TYPE_POSSIBLE_GOAL = 39 -uint8 TYPE_GOAL = 8 -uint8 TYPE_INVALID_GOAL = 42 -uint8 TYPE_ATTACKER_DOUBLE_TOUCHED_BALL = 14 -uint8 TYPE_PLACEMENT_SUCCEEDED = 5 -uint8 TYPE_PENALTY_KICK_FAILED = 43 -uint8 TYPE_NO_PROGRESS_IN_GAME = 2 -uint8 TYPE_PLACEMENT_FAILED = 3 -uint8 TYPE_MULTIPLE_CARDS = 32 -uint8 TYPE_MULTIPLE_FOULS = 34 -uint8 TYPE_BOT_SUBSTITUTION = 37 -uint8 TYPE_TOO_MANY_ROBOTS = 38 -uint8 TYPE_CHALLENGE_FLAG = 44 -uint8 TYPE_CHALLENGE_FLAG_HANDLED = 46 -uint8 TYPE_EMERGENCY_STOP = 45 -uint8 TYPE_UNSPORTING_BEHAVIOR_MINOR = 35 -uint8 TYPE_UNSPORTING_BEHAVIOR_MAJOR = 36 -# deprecated types -uint8 TYPE_PREPARED = 1 -uint8 TYPE_INDIRECT_GOAL = 9 -uint8 TYPE_CHIPPED_GOAL = 10 -uint8 TYPE_KICK_TIMEOUT = 12 -uint8 TYPE_ATTACKER_TOUCHED_OPPONENT_IN_DEFENSE_AREA = 16 -uint8 TYPE_ATTACKER_TOUCHED_OPPONENT_IN_DEFENSE_AREA_SKIPPED = 40 -uint8 TYPE_BOT_CRASH_UNIQUE_SKIPPED = 23 -uint8 TYPE_BOT_PUSHED_BOT_SKIPPED = 25 -uint8 TYPE_DEFENDER_IN_DEFENSE_AREA_PARTIALLY = 30 -uint8 TYPE_MULTIPLE_PLACEMENT_FAILURES = 33 diff --git a/ssl_league_msgs/game_controller/msg/GameEventProposalGroup.msg b/ssl_league_msgs/game_controller/msg/GameEventProposalGroup.msg deleted file mode 100644 index 808bef0..0000000 --- a/ssl_league_msgs/game_controller/msg/GameEventProposalGroup.msg +++ /dev/null @@ -1,3 +0,0 @@ -string[] id -ssl_league_msgs/GameEvent[] game_events -bool accepted diff --git a/ssl_league_msgs/game_controller/msg/Referee.msg b/ssl_league_msgs/game_controller/msg/Referee.msg deleted file mode 100644 index d8ed0d6..0000000 --- a/ssl_league_msgs/game_controller/msg/Referee.msg +++ /dev/null @@ -1,60 +0,0 @@ -string[] source_identifier -uint8[] match_type -builtin_interfaces/Time timestamp -uint8 stage -int64[] stage_time_left -uint8 command -uint32 command_counter -builtin_interfaces/Time command_timestamp -ssl_league_msgs/TeamInfo yellow -ssl_league_msgs/TeamInfo blue -geometry_msgs/Point32[] designated_position -bool[] blue_team_on_positive_half -uint8[] next_command -ssl_league_msgs/GameEvent[] game_events -ssl_league_msgs/GameEventProposalGroup[] game_event_proposals -int64[] current_action_time_remaining -string[] status_message - -# Match Types -uint8 MATCH_TYPE_UNKNOWN_MATCH = 0 -uint8 MATCH_TYPE_GROUP_PHASE = 1 -uint8 MATCH_TYPE_ELIMINATION_PHASE = 2 -uint8 MATCH_TYPE_FRIENDLY = 3 - -# Stages -uint8 STAGE_NORMAL_FIRST_HALF_PRE = 0 -uint8 STAGE_NORMAL_FIRST_HALF = 1 -uint8 STAGE_NORMAL_HALF_TIME = 2 -uint8 STAGE_NORMAL_SECOND_HALF_PRE = 3 -uint8 STAGE_NORMAL_SECOND_HALF = 4 -uint8 STAGE_EXTRA_TIME_BREAK = 5 -uint8 STAGE_EXTRA_FIRST_HALF_PRE = 6 -uint8 STAGE_EXTRA_FIRST_HALF = 7 -uint8 STAGE_EXTRA_HALF_TIME = 8 -uint8 STAGE_EXTRA_SECOND_HALF_PRE = 9 -uint8 STAGE_EXTRA_SECOND_HALF = 10 -uint8 STAGE_PENALTY_SHOOTOUT_BREAK = 11 -uint8 STAGE_PENALTY_SHOOTOUT = 12 -uint8 STAGE_POST_GAME = 13 - -# Commands -uint8 COMMAND_HALT = 0 -uint8 COMMAND_STOP = 1 -uint8 COMMAND_NORMAL_START = 2 -uint8 COMMAND_FORCE_START = 3 -uint8 COMMAND_PREPARE_KICKOFF_YELLOW = 4 -uint8 COMMAND_PREPARE_KICKOFF_BLUE = 5 -uint8 COMMAND_PREPARE_PENALTY_YELLOW = 6 -uint8 COMMAND_PREPARE_PENALTY_BLUE = 7 -uint8 COMMAND_DIRECT_FREE_YELLOW = 8 -uint8 COMMAND_DIRECT_FREE_BLUE = 9 -uint8 COMMAND_TIMEOUT_YELLOW = 12 -uint8 COMMAND_TIMEOUT_BLUE = 13 -uint8 COMMAND_BALL_PLACEMENT_YELLOW = 16 -uint8 COMMAND_BALL_PLACEMENT_BLUE = 17 -# Deprecated commands -uint8 COMMAND_INDIRECT_FREE_YELLOW = 10 -uint8 COMMAND_INDIRECT_FREE_BLUE = 11 -uint8 COMMAND_GOAL_YELLOW = 14 -uint8 COMMAND_GOAL_BLUE = 15 diff --git a/ssl_league_msgs/game_controller/msg/TeamInfo.msg b/ssl_league_msgs/game_controller/msg/TeamInfo.msg deleted file mode 100644 index a99d2af..0000000 --- a/ssl_league_msgs/game_controller/msg/TeamInfo.msg +++ /dev/null @@ -1,17 +0,0 @@ -string name -uint32 score -uint32 red_cards -uint32[] yellow_card_times -uint32 yellow_cards -uint32 timeouts -uint32 timeout_time -uint32 goalkeeper -uint32[] foul_counter -uint32[] ball_placement_failures -bool[] can_place_ball -uint32[] max_allowed_bots -bool[] bot_substitution_intent -bool[] ball_placement_failures_reached -bool[] bot_substitution_allowed -uint32[] bot_substitutions_left -uint32[] bot_substitution_time_left diff --git a/ssl_league_msgs/simulator/msg/SimulatorControl.msg b/ssl_league_msgs/simulator/msg/SimulatorControl.msg deleted file mode 100644 index 6a0d434..0000000 --- a/ssl_league_msgs/simulator/msg/SimulatorControl.msg +++ /dev/null @@ -1,4 +0,0 @@ -ssl_league_msgs/TeleportBallCommand[] teleport_ball -ssl_league_msgs/TeleportRobotCommand[] teleport_robot - -float32 simulation_speed 1.0 \ No newline at end of file diff --git a/ssl_league_msgs/simulator/msg/TeleportBallCommand.msg b/ssl_league_msgs/simulator/msg/TeleportBallCommand.msg deleted file mode 100644 index 941d053..0000000 --- a/ssl_league_msgs/simulator/msg/TeleportBallCommand.msg +++ /dev/null @@ -1,19 +0,0 @@ -# Teleport the ball to a new location and optionally set it to some velocity -geometry_msgs/Pose pose -geometry_msgs/Twist twist - -# Teleport the ball safely to the target, for example by -# moving robots out of the way in case of collision and set speed of robots close-by to zero -bool teleport_safely - -# Adapt the angular ball velocity such that the ball is rolling -bool roll - -# Instead of teleporting the ball, apply some force to make sure -# the ball reaches the required position soon (velocity is ignored if true) -# WARNING: A command with by_force stays active (the move will take some time) -# until cancled by another TeleportBall command with by_force = false. -# To avoid teleporting the ball at the end and resetting its current spin, -# do not set any of the optional fields in this message to end the force without triggering -# an additional teleportation -bool by_force \ No newline at end of file diff --git a/ssl_league_msgs/simulator/msg/TeleportRobotCommand.msg b/ssl_league_msgs/simulator/msg/TeleportRobotCommand.msg deleted file mode 100644 index 26b74d8..0000000 --- a/ssl_league_msgs/simulator/msg/TeleportRobotCommand.msg +++ /dev/null @@ -1,20 +0,0 @@ -# Teleport a robot to some location and give it a velocity -ssl_league_msgs/RobotId id - -geometry_msgs/Pose pose -geometry_msgs/Twist twist - -# Robot should be present on the field? -# true -> robot will be added, if it does not exist yet -# false -> robot will be removed, if it is present -bool present - -# Instead of teleporting, apply some force to make sure -# the robot reaches the required position soon (velocity is ignored if true) -# WARNING: A command with by_force stays active (the move will take some time) -# until cancled by another TeleportRobot command for the same bot with by_force = false. -# To avoid teleporting at the end, -# do not set any of the optional fields in this message -# to end the force without triggering -# an additional teleportation -bool by_force \ No newline at end of file diff --git a/ssl_league_msgs/vision/msg/VisionDetectionBall.msg b/ssl_league_msgs/vision/msg/VisionDetectionBall.msg deleted file mode 100644 index be550e0..0000000 --- a/ssl_league_msgs/vision/msg/VisionDetectionBall.msg +++ /dev/null @@ -1,4 +0,0 @@ -float32 confidence -uint32 area -geometry_msgs/Point32 pos -geometry_msgs/Point32 pixel \ No newline at end of file diff --git a/ssl_league_msgs/vision/msg/VisionDetectionFrame.msg b/ssl_league_msgs/vision/msg/VisionDetectionFrame.msg deleted file mode 100644 index 83582cc..0000000 --- a/ssl_league_msgs/vision/msg/VisionDetectionFrame.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint32 frame_number -builtin_interfaces/Time t_capture -builtin_interfaces/Time t_sent -builtin_interfaces/Time t_capture_camera -uint32 camera_id -ssl_league_msgs/VisionDetectionBall[] balls -ssl_league_msgs/VisionDetectionRobot[] robots_yellow -ssl_league_msgs/VisionDetectionRobot[] robots_blue \ No newline at end of file diff --git a/ssl_league_msgs/vision/msg/VisionDetectionRobot.msg b/ssl_league_msgs/vision/msg/VisionDetectionRobot.msg deleted file mode 100644 index 29867b3..0000000 --- a/ssl_league_msgs/vision/msg/VisionDetectionRobot.msg +++ /dev/null @@ -1,5 +0,0 @@ -float32 confidence -uint32 robot_id -geometry_msgs/Pose pose -geometry_msgs/Point32 pixel -float32 height \ No newline at end of file diff --git a/ssl_league_msgs/vision/msg/VisionFieldCircularArc.msg b/ssl_league_msgs/vision/msg/VisionFieldCircularArc.msg deleted file mode 100644 index 484f0f3..0000000 --- a/ssl_league_msgs/vision/msg/VisionFieldCircularArc.msg +++ /dev/null @@ -1,6 +0,0 @@ -string name -geometry_msgs/Point32 center -float32 radius -float32 a1 -float32 a2 -float32 thickness \ No newline at end of file diff --git a/ssl_league_msgs/vision/msg/VisionFieldLineSegment.msg b/ssl_league_msgs/vision/msg/VisionFieldLineSegment.msg deleted file mode 100644 index acd2fff..0000000 --- a/ssl_league_msgs/vision/msg/VisionFieldLineSegment.msg +++ /dev/null @@ -1,4 +0,0 @@ -string name -geometry_msgs/Point32 p1 -geometry_msgs/Point32 p2 -float32 thickness \ No newline at end of file diff --git a/ssl_league_msgs/vision/msg/VisionGeometryCameraCalibration.msg b/ssl_league_msgs/vision/msg/VisionGeometryCameraCalibration.msg deleted file mode 100644 index bf64e56..0000000 --- a/ssl_league_msgs/vision/msg/VisionGeometryCameraCalibration.msg +++ /dev/null @@ -1,6 +0,0 @@ -uint32 camera_id -float32 focal_length -geometry_msgs/Point32 principal_point -float32 distortion -geometry_msgs/Pose pose -geometry_msgs/Point32 derived_camera_world_t \ No newline at end of file diff --git a/ssl_league_msgs/vision/msg/VisionGeometryData.msg b/ssl_league_msgs/vision/msg/VisionGeometryData.msg deleted file mode 100644 index 2b2b7f9..0000000 --- a/ssl_league_msgs/vision/msg/VisionGeometryData.msg +++ /dev/null @@ -1,2 +0,0 @@ -ssl_league_msgs/VisionGeometryFieldSize field -ssl_league_msgs/VisionGeometryCameraCalibration[] calibration \ No newline at end of file diff --git a/ssl_league_msgs/vision/msg/VisionGeometryFieldSize.msg b/ssl_league_msgs/vision/msg/VisionGeometryFieldSize.msg deleted file mode 100644 index eb8579d..0000000 --- a/ssl_league_msgs/vision/msg/VisionGeometryFieldSize.msg +++ /dev/null @@ -1,15 +0,0 @@ -float32 field_length -float32 field_width -float32 goal_width -float32 goal_depth -float32 boundary_width -ssl_league_msgs/VisionFieldLineSegment[] field_lines -ssl_league_msgs/VisionFieldCircularArc[] field_arcs -float32 penalty_area_depth -float32 penalty_area_width -float32 center_circle_radius -float32 line_thickness -float32 goal_center_to_penalty_mark -float32 goal_height -float32 ball_radius -float32 max_robot_radius diff --git a/ssl_league_msgs/vision/msg/VisionWrapper.msg b/ssl_league_msgs/vision/msg/VisionWrapper.msg deleted file mode 100644 index 57deadb..0000000 --- a/ssl_league_msgs/vision/msg/VisionWrapper.msg +++ /dev/null @@ -1,2 +0,0 @@ -ssl_league_msgs/VisionDetectionFrame[] detection -ssl_league_msgs/VisionGeometryData[] geometry \ No newline at end of file From 58f61e39e1e21e534c4a3f0cdd57a9e5892875bf Mon Sep 17 00:00:00 2001 From: Matthew Barulic Date: Sun, 9 Aug 2026 22:37:54 -0400 Subject: [PATCH 2/7] Fixes an assortment of msg generation issues --- ssl_league_msgs/CMakeLists.txt | 1 - ssl_league_msgs/cmake/Ros2MsgGen.cmake | 12 ++++---- ssl_league_msgs/cmake/ateam_proto_shared.py | 7 ++--- ssl_league_msgs/cmake/protoc_gen_ros2msg.py | 30 +++++++++---------- .../cmake/ssl_ros_annotations.json | 16 +++++----- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/ssl_league_msgs/CMakeLists.txt b/ssl_league_msgs/CMakeLists.txt index c17cb53..fcb6b95 100644 --- a/ssl_league_msgs/CMakeLists.txt +++ b/ssl_league_msgs/CMakeLists.txt @@ -21,7 +21,6 @@ generate_ros2_msgs( ${_PROTO_DIR}/ssl_vision_geometry.proto ${_PROTO_DIR}/ssl_vision_wrapper.proto ${_PROTO_DIR}/ssl_simulation_control.proto - ${_PROTO_DIR}/ssl_simulation_config.proto ${_PROTO_DIR}/ssl_simulation_error.proto PROTO_PATHS ${_PROTO_DIR} diff --git a/ssl_league_msgs/cmake/Ros2MsgGen.cmake b/ssl_league_msgs/cmake/Ros2MsgGen.cmake index 2364ca8..fbfa2ba 100644 --- a/ssl_league_msgs/cmake/Ros2MsgGen.cmake +++ b/ssl_league_msgs/cmake/Ros2MsgGen.cmake @@ -68,7 +68,7 @@ function(generate_ros2_msgs) # --- Plugin path (sibling of this .cmake file) --- get_filename_component(_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) - set(_PLUGIN_SRC "${_CMAKE_DIR}/protoc_gen_ros2msg.py") + set(_PLUGIN_SRC "${_CMAKE_DIR}/cmake/protoc_gen_ros2msg.py") if(NOT EXISTS "${_PLUGIN_SRC}") message(FATAL_ERROR @@ -90,7 +90,7 @@ function(generate_ros2_msgs) ) # --- Output directory --- - file(MAKE_DIRECTORY "${_ARG_OUTPUT_DIR}") + file(MAKE_DIRECTORY "${_ARG_OUTPUT_DIR}/msg") # --- Build --proto_path arguments --- set(_proto_path_args) @@ -109,12 +109,13 @@ function(generate_ros2_msgs) endif() # --- Run protoc at configure time --- + # TODO convert this to add_custom_command execute_process( COMMAND "${_PROTOC}" "--plugin=protoc-gen-ros2msg=${_PLUGIN_WRAPPER}" "--ros2msg_opt=${_plugin_opt}" - "--ros2msg_out=${_ARG_OUTPUT_DIR}" + "--ros2msg_out=${_ARG_OUTPUT_DIR}/msg" ${_proto_path_args} ${_ARG_PROTO_FILES} RESULT_VARIABLE _result @@ -135,10 +136,11 @@ function(generate_ros2_msgs) ) # Collect results and expose to caller. - file(GLOB _generated "${_ARG_OUTPUT_DIR}/*.msg") + file(GLOB _generated RELATIVE ${_ARG_OUTPUT_DIR} "${_ARG_OUTPUT_DIR}/msg/*.msg") + list(TRANSFORM _generated PREPEND "${_ARG_OUTPUT_DIR}:") if(NOT _generated) message(FATAL_ERROR - "generate_ros2_msgs: no .msg files found in ${_ARG_OUTPUT_DIR} after generation" + "generate_ros2_msgs: no .msg files found in ${_ARG_OUTPUT_DIR}/msg after generation" ) endif() diff --git a/ssl_league_msgs/cmake/ateam_proto_shared.py b/ssl_league_msgs/cmake/ateam_proto_shared.py index 9d72dca..099a809 100644 --- a/ssl_league_msgs/cmake/ateam_proto_shared.py +++ b/ssl_league_msgs/cmake/ateam_proto_shared.py @@ -28,9 +28,7 @@ def flatten_type_name(type_name: str) -> str: .GameEvent.BallLeftField → GameEvent_BallLeftField .ateam_test.OuterMessage.Inner → OuterMessage_Inner """ - parts = type_name.lstrip(".").split(".") - type_parts = [p for p in parts if p and p[0].isupper()] - return "_".join(type_parts) if type_parts else parts[-1] + return type_name.lstrip(".").split(".")[-1] # Alias preserved for callers that import the name directly. @@ -59,7 +57,8 @@ def _walk(msg, parent_fqn: str) -> None: def iter_messages(fd): """Yield (flat_name, msg) for all non-map-entry messages in fd, including nested.""" def _walk(msg, parent_flat: str): - flat = f"{parent_flat}_{msg.name}" if parent_flat else msg.name + flat = f"{msg.name}" if parent_flat else msg.name + flat = flat.replace("SSL_", "") if not msg.options.map_entry: yield flat, msg for nested in msg.nested_type: diff --git a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py index 8810eb3..f2605b8 100755 --- a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py +++ b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py @@ -107,9 +107,11 @@ def ros2_field_type(field: descriptor_pb2.FieldDescriptorProto) -> str: if field.type in SCALAR_TYPE_MAP: return SCALAR_TYPE_MAP[field.type] if field.type == FD.TYPE_ENUM: - return "int32" + return "int8" if field.type == FD.TYPE_MESSAGE: - return flatten_type_name(field.type_name) + if field.type_name == ".google.protobuf.Any": + raise ValueError("Fields with type 'Any' are not supported.") + return flatten_type_name(field.type_name).replace("SSL_", "") raise ValueError(f"unhandled proto field type {field.type} in field '{field.name}'") @@ -129,13 +131,6 @@ def _walk(msg, parent_flat: str): yield from _walk(msg, "") -def generate_enum_msg(enum: descriptor_pb2.EnumDescriptorProto) -> str: - lines = [f"# Generated from proto enum {enum.name}"] - for v in enum.value: - lines.append(f"int32 {v.name}={v.number}") - return "\n".join(lines) + "\n" - - # --------------------------------------------------------------------------- # Sidecar helpers # --------------------------------------------------------------------------- @@ -265,7 +260,17 @@ def generate_message_msg( # 2. Sidecar field overrides (single-field with ros_type / ros_field / scale). _emit_sidecar_fields(sidecar_entry, proto_field_map, lines, proto2, errors, flat_name) - # 3. Passthrough: proto fields not consumed by the sidecar. + # 3. Enum value constants + + for enum in msg.enum_type: + for value in enum.value: + constant_name = enum.name.upper() + "_" + value.name.upper() + constant_value = value.number + lines.append(f"uint8 {constant_name} = {constant_value}") + + lines.append("\n") + + # 4. Passthrough: proto fields not consumed by the sidecar. emitted_oneofs: set = set() for field in msg.field: @@ -367,11 +372,6 @@ def main() -> None: for file_name in request.file_to_generate: fd = all_files[file_name] - for flat_name, enum in iter_enums(fd): - out = response.file.add() - out.name = f"{flat_name}.msg" - out.content = generate_enum_msg(enum) - proto2 = (fd.syntax != "proto3") for flat_name, msg in iter_messages(fd): out = response.file.add() diff --git a/ssl_league_msgs/cmake/ssl_ros_annotations.json b/ssl_league_msgs/cmake/ssl_ros_annotations.json index 6a41ee6..f908246 100644 --- a/ssl_league_msgs/cmake/ssl_ros_annotations.json +++ b/ssl_league_msgs/cmake/ssl_ros_annotations.json @@ -10,7 +10,7 @@ "conversion_func": "Reserved for future user-supplied conversions. Only intrinsic conversions (Time/Duration) are supported now; generator errors on any user-supplied value." }, - "SSL_DetectionBall": { + "DetectionBall": { "outputs": [ { "ros_field": "pos", @@ -30,7 +30,7 @@ ] }, - "SSL_DetectionRobot": { + "DetectionRobot": { "outputs": [ { "ros_field": "pos", @@ -52,7 +52,7 @@ } }, - "SSL_DetectionFrame": { + "DetectionFrame": { "fields": { "t_capture": { "ros_type": "builtin_interfaces/Time" }, "t_sent": { "ros_type": "builtin_interfaces/Time" }, @@ -60,7 +60,7 @@ } }, - "SSL_GeometryCameraCalibration": { + "GeometryCameraCalibration": { "outputs": [ { "ros_field": "pose", @@ -92,7 +92,7 @@ ] }, - "SSL_GeometryFieldSize": { + "GeometryFieldSize": { "fields": { "field_length": { "scale": 1e-3, "ros_type": "float32" }, "field_width": { "scale": 1e-3, "ros_type": "float32" }, @@ -110,14 +110,14 @@ } }, - "SSL_FieldCircularArc": { + "FieldCircularArc": { "fields": { "radius": { "scale": 1e-3 }, "thickness": { "scale": 1e-3 } } }, - "SSL_FieldLineSegment": { + "FieldLineSegment": { "fields": { "thickness": { "scale": 1e-3 } } @@ -130,7 +130,7 @@ } }, - "SSL_GeometryData": { + "GeometryData": { "fields": { "calib": { "ros_field": "calibration" } } From 9311a8c9cbf2da5d90696360fc296a6bf9351ce3 Mon Sep 17 00:00:00 2001 From: Will Stuckey Date: Wed, 12 Aug 2026 12:51:44 -0400 Subject: [PATCH 3/7] updates on bug fixes --- ssl_league_msgs/cmake/Ros2MsgGen.cmake | 2 +- ssl_ros_bridge/CMakeLists.txt | 25 + ssl_ros_bridge/cmake/MsgConversionGen.cmake | 80 +++ .../cmake/gen_message_conversion.py | 590 ++++++++++++++++++ ssl_ros_bridge/src/core/CMakeLists.txt | 5 +- 5 files changed, 700 insertions(+), 2 deletions(-) create mode 100644 ssl_ros_bridge/cmake/MsgConversionGen.cmake create mode 100644 ssl_ros_bridge/cmake/gen_message_conversion.py diff --git a/ssl_league_msgs/cmake/Ros2MsgGen.cmake b/ssl_league_msgs/cmake/Ros2MsgGen.cmake index fbfa2ba..1aa6a37 100644 --- a/ssl_league_msgs/cmake/Ros2MsgGen.cmake +++ b/ssl_league_msgs/cmake/Ros2MsgGen.cmake @@ -68,7 +68,7 @@ function(generate_ros2_msgs) # --- Plugin path (sibling of this .cmake file) --- get_filename_component(_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) - set(_PLUGIN_SRC "${_CMAKE_DIR}/cmake/protoc_gen_ros2msg.py") + set(_PLUGIN_SRC "${_CMAKE_DIR}/protoc_gen_ros2msg.py") if(NOT EXISTS "${_PLUGIN_SRC}") message(FATAL_ERROR diff --git a/ssl_ros_bridge/CMakeLists.txt b/ssl_ros_bridge/CMakeLists.txt index e36f169..dbc4081 100644 --- a/ssl_ros_bridge/CMakeLists.txt +++ b/ssl_ros_bridge/CMakeLists.txt @@ -12,10 +12,35 @@ find_package(rclcpp_components REQUIRED) find_package(rosbag2_cpp REQUIRED) find_package(tf2 REQUIRED) find_package(tf2_geometry_msgs REQUIRED) +find_package(builtin_interfaces REQUIRED) +find_package(geometry_msgs REQUIRED) find_package(ssl_league_msgs REQUIRED) find_package(ssl_league_protobufs REQUIRED) find_package(ssl_ros_bridge_msgs REQUIRED) +include(cmake/MsgConversionGen.cmake) + +set(_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_protobufs/proto") +set(_SIDECAR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_msgs/cmake/ssl_ros_annotations.json") +set(_CONV_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated_conversion") + +generate_message_conversion( + PROTO_FILES + ${_PROTO_DIR}/ssl_gc_common.proto + ${_PROTO_DIR}/ssl_gc_geometry.proto + ${_PROTO_DIR}/ssl_gc_game_event.proto + ${_PROTO_DIR}/ssl_gc_referee_message.proto + ${_PROTO_DIR}/ssl_gc_rcon.proto + ${_PROTO_DIR}/ssl_vision_detection.proto + ${_PROTO_DIR}/ssl_vision_geometry.proto + ${_PROTO_DIR}/ssl_vision_wrapper.proto + ${_PROTO_DIR}/ssl_simulation_control.proto + ${_PROTO_DIR}/ssl_simulation_error.proto + PROTO_PATHS ${_PROTO_DIR} + SIDECAR ${_SIDECAR} + OUTPUT_DIR ${_CONV_OUT} +) + add_subdirectory(src/core) add_subdirectory(src/game_controller_bridge) add_subdirectory(src/log2bag) diff --git a/ssl_ros_bridge/cmake/MsgConversionGen.cmake b/ssl_ros_bridge/cmake/MsgConversionGen.cmake new file mode 100644 index 0000000..0fb2b97 --- /dev/null +++ b/ssl_ros_bridge/cmake/MsgConversionGen.cmake @@ -0,0 +1,80 @@ +# MsgConversionGen.cmake +# +# Provides generate_message_conversion() — runs gen_message_conversion.py at +# CMake configure time and emits message_conversion_generated.{hpp,cpp}. +# +# Usage: +# generate_message_conversion( +# PROTO_FILES path/to/a.proto path/to/b.proto ... +# PROTO_PATHS path/to/proto/include/dir ... +# SIDECAR path/to/annotations.json # optional +# OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated_conversion +# ) +# # Generated files live in OUTPUT_DIR and must be added to a target manually: +# add_library(mylib ... ${OUTPUT_DIR}/message_conversion_generated.cpp) +# target_include_directories(mylib PUBLIC ${OUTPUT_DIR}) +# +# CMake re-runs automatically when any PROTO_FILES or the SIDECAR changes. + +cmake_minimum_required(VERSION 3.16) + +function(generate_message_conversion) + cmake_parse_arguments(_ARG "" "OUTPUT_DIR;SIDECAR" "PROTO_FILES;PROTO_PATHS" ${ARGN}) + + if(NOT _ARG_PROTO_FILES) + message(FATAL_ERROR "generate_message_conversion: PROTO_FILES is required") + endif() + if(NOT _ARG_OUTPUT_DIR) + message(FATAL_ERROR "generate_message_conversion: OUTPUT_DIR is required") + endif() + + find_package(Python3 REQUIRED COMPONENTS Interpreter) + find_program(_PROTOC protoc REQUIRED DOC "protoc compiler") + + get_filename_component(_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + set(_SCRIPT "${_CMAKE_DIR}/gen_message_conversion.py") + + if(NOT EXISTS "${_SCRIPT}") + message(FATAL_ERROR "generate_message_conversion: script not found at ${_SCRIPT}") + endif() + + # Build --proto-paths args + set(_path_args) + foreach(_p ${_ARG_PROTO_PATHS}) + list(APPEND _path_args "--proto-paths" "${_p}") + endforeach() + + # Build --sidecar arg + set(_sidecar_arg) + if(_ARG_SIDECAR) + if(NOT EXISTS "${_ARG_SIDECAR}") + message(FATAL_ERROR "generate_message_conversion: SIDECAR not found: ${_ARG_SIDECAR}") + endif() + set(_sidecar_arg "--sidecar" "${_ARG_SIDECAR}") + endif() + + file(MAKE_DIRECTORY "${_ARG_OUTPUT_DIR}") + + execute_process( + COMMAND + "${Python3_EXECUTABLE}" "${_SCRIPT}" + "--proto-files" ${_ARG_PROTO_FILES} + ${_path_args} + ${_sidecar_arg} + "--output-dir" "${_ARG_OUTPUT_DIR}" + RESULT_VARIABLE _result + OUTPUT_VARIABLE _stdout + ERROR_VARIABLE _stderr + ) + + if(NOT _result EQUAL 0) + message(FATAL_ERROR + "generate_message_conversion: generator failed (exit ${_result}):\n${_stderr}") + endif() + + # Re-configure when proto files or sidecar change + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + ${_ARG_PROTO_FILES} + ${_ARG_SIDECAR} + ) +endfunction() diff --git a/ssl_ros_bridge/cmake/gen_message_conversion.py b/ssl_ros_bridge/cmake/gen_message_conversion.py new file mode 100644 index 0000000..2060c24 --- /dev/null +++ b/ssl_ros_bridge/cmake/gen_message_conversion.py @@ -0,0 +1,590 @@ +#!/usr/bin/env python3 +""" +gen_message_conversion.py — Generate C++ fromProto bridge functions. + +Reads SSL league proto files + sidecar JSON, emits two files: + /message_conversion_generated.hpp + /message_conversion_generated.cpp + +Usage (from CMake or command line): + python3 gen_message_conversion.py \\ + --proto-files ssl_vision_detection.proto ... \\ + --proto-paths /path/to/protos \\ + --sidecar ssl_ros_annotations.json \\ + --output-dir /path/to/output \\ + [--proto-include-prefix ssl_league_protobufs] \\ + [--ros-package ssl_league_msgs] \\ + [--cpp-namespace ssl_ros_bridge::message_conversion] + +Sidecar annotation format is the same as for protoc_gen_ros2msg.py. +Consumed fields (from 'outputs' groups + 'fields' keys) are excluded from +passthrough. All other proto fields are emitted using proto2/proto3-appropriate +accessor patterns. + +Intrinsic conversions (no 'conversion_func' needed in sidecar): + builtin_interfaces/Time float/double → from_seconds (×1e9 ns cast) + int/uint → from_microseconds (×1000 ns cast) + builtin_interfaces/Duration same rules +""" + +import argparse +import json +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +from google.protobuf import descriptor_pb2 + +FD = descriptor_pb2.FieldDescriptorProto + +FLOAT_TYPES = frozenset({FD.TYPE_FLOAT, FD.TYPE_DOUBLE}) +INT_TYPES = frozenset({ + FD.TYPE_INT32, FD.TYPE_INT64, FD.TYPE_UINT32, FD.TYPE_UINT64, + FD.TYPE_SINT32, FD.TYPE_SINT64, FD.TYPE_FIXED32, FD.TYPE_FIXED64, + FD.TYPE_SFIXED32, FD.TYPE_SFIXED64, +}) +INTRINSIC_ROS_TYPES = frozenset({"builtin_interfaces/Time", "builtin_interfaces/Duration"}) + +# ── Name helpers ────────────────────────────────────────────────────────────── + +def to_ros_include_name(name: str) -> str: + """PascalCase/underscore msg name → ROS2 snake_case include stem.""" + s1 = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1_\2', name) + s2 = re.sub(r'([a-z\d])([A-Z])', r'\1_\2', s1) + return s2.lower() + + +def ros_cpp_type(ros_type: str) -> str: + """'geometry_msgs/Point32' → 'geometry_msgs::msg::Point32'""" + if "/" in ros_type: + pkg, typ = ros_type.split("/", 1) + return f"{pkg}::msg::{typ}" + return ros_type + + +def ros_msg_type(flat: str, pkg: str) -> str: + return f"{pkg}::msg::{flat}" + + +def proto_oneof_const(field_name: str) -> str: + """'aimless_kick' → 'kAimlessKick'""" + return "k" + "".join(p.capitalize() for p in field_name.split("_")) + + +# ── Message iteration (mirrors protoc_gen_ros2msg.py) ──────────────────────── + +def iter_messages(fd): + def _walk(msg): + flat = msg.name.replace("SSL_", "") + if not msg.options.map_entry: + yield flat, msg + for nested in msg.nested_type: + yield from _walk(nested) + for msg in fd.message_type: + yield from _walk(msg) + + +def map_entry_type_names(fd) -> frozenset: + entries = set() + for msg in fd.message_type: + for nested in msg.nested_type: + if nested.options.map_entry: + pkg = f".{fd.package}" if fd.package else "" + entries.add(f"{pkg}.{msg.name}.{nested.name}") + return frozenset(entries) + + +# ── Sidecar helpers ─────────────────────────────────────────────────────────── + +def load_sidecar(path: str | None) -> dict: + if not path: + return {} + with open(path) as f: + return json.load(f) + + +def sidecar_consumed(entry: dict) -> frozenset: + s = set(entry.get("fields", {}).keys()) + for out in entry.get("outputs", []): + for v in out.get("from", {}).values(): + s.add(v) + for sub in ("position", "orientation"): + for v in out.get(sub, {}).get("from", {}).values(): + s.add(v) + return frozenset(s) + + +# ── C++ expression helpers ──────────────────────────────────────────────────── + +def scale_lit(val: float) -> str: + if abs(val - 1e-3) < 1e-12: + return "1e-3f" + return f"{val}f" + + +def acc(field_name: str) -> str: + return f"proto_msg.{field_name}()" + + +# ── Sidecar output codegen ──────────────────────────────────────────────────── + +def emit_output(out: dict, ind: str) -> list[str]: + """Emit C++ for one 'outputs' entry.""" + lines = [] + ros_field = out["ros_field"] + ros_type = out["ros_type"] + + if ros_type in ("geometry_msgs/Point32", "geometry_msgs/Vector3"): + scale = out.get("scale") + for ros_comp, pf in out["from"].items(): + expr = acc(pf) + if scale: + expr = f"{expr} * {scale_lit(scale)}" + lines.append(f"{ind}ros_msg.{ros_field}.{ros_comp} = {expr};") + + elif ros_type == "geometry_msgs/Quaternion": + for ros_comp, pf in out["from"].items(): + lines.append(f"{ind}ros_msg.{ros_field}.{ros_comp} = {acc(pf)};") + + elif ros_type == "geometry_msgs/Pose": + pos = out.get("position", {}) + ori = out.get("orientation", {}) + ps = pos.get("scale") + for ros_comp, pf in pos.get("from", {}).items(): + expr = acc(pf) + if ps: + expr = f"{expr} * {scale_lit(ps)}" + lines.append(f"{ind}ros_msg.{ros_field}.position.{ros_comp} = {expr};") + for ros_comp, pf in ori.get("from", {}).items(): + lines.append(f"{ind}ros_msg.{ros_field}.orientation.{ros_comp} = {acc(pf)};") + + else: + lines.append(f"{ind}// TODO: unsupported output ros_type '{ros_type}' → '{ros_field}'") + + return lines + + +# ── Sidecar field override codegen ──────────────────────────────────────────── + +def emit_field_override( + proto_name: str, + ann: dict, + field: descriptor_pb2.FieldDescriptorProto, + proto2: bool, + ind: str, +) -> list[str]: + lines = [] + ros_name = ann.get("ros_field", proto_name) + ros_type = ann.get("ros_type") + scale = ann.get("scale") + + is_rep = field.label == FD.LABEL_REPEATED + is_p2opt = proto2 and field.label == FD.LABEL_OPTIONAL and not field.HasField("oneof_index") + + def wrap_optional(inner: str) -> list[str]: + return [ + f"{ind}if (proto_msg.has_{proto_name}()) {{", + f"{ind} ros_msg.{ros_name} = {{{inner}}};", + f"{ind}}}", + ] + + if ros_type in INTRINSIC_ROS_TYPES: + is_time = ros_type == "builtin_interfaces/Time" + if field.type in FLOAT_TYPES: + if is_time: + expr = f"rclcpp::Time(static_cast({acc(proto_name)} * 1e9))" + else: + expr = f"rclcpp::Duration::from_nanoseconds(static_cast({acc(proto_name)} * 1e9))" + else: + if is_time: + expr = f"rclcpp::Time(static_cast({acc(proto_name)}) * 1000LL)" + else: + expr = f"rclcpp::Duration::from_nanoseconds(static_cast({acc(proto_name)}) * 1000LL)" + + if is_p2opt: + lines += wrap_optional(expr) + elif is_rep: + lines.append(f"{ind}// TODO: repeated Time/Duration not implemented") + else: + lines.append(f"{ind}ros_msg.{ros_name} = {expr};") + + elif scale is not None: + expr = f"{acc(proto_name)} * {scale_lit(scale)}" + if is_p2opt: + lines += wrap_optional(expr) + elif is_rep: + lines.append(f"{ind}std::transform(proto_msg.{proto_name}().begin(), proto_msg.{proto_name}().end(),") + lines.append(f"{ind} std::back_inserter(ros_msg.{ros_name}),") + lines.append(f"{ind} [](const auto & v) {{ return v * {scale_lit(scale)}; }});") + else: + lines.append(f"{ind}ros_msg.{ros_name} = {expr};") + + else: + # Rename only — same type, different ros field name + if field.type == FD.TYPE_MESSAGE: + inner = f"fromProto({acc(proto_name)})" + if is_p2opt: + lines += [ + f"{ind}if (proto_msg.has_{proto_name}()) {{", + f"{ind} ros_msg.{ros_name} = {{fromProto(proto_msg.{proto_name}())}};", + f"{ind}}}", + ] + elif is_rep: + lines.append(f"{ind}std::transform(proto_msg.{proto_name}().begin(), proto_msg.{proto_name}().end(),") + lines.append(f"{ind} std::back_inserter(ros_msg.{ros_name}),") + lines.append(f"{ind} [](const auto & p) {{ return fromProto(p); }});") + else: + lines.append(f"{ind}ros_msg.{ros_name} = fromProto({acc(proto_name)});") + elif field.type == FD.TYPE_ENUM: + inner = f"static_cast({acc(proto_name)})" + if is_p2opt: + lines += wrap_optional(inner) + else: + lines.append(f"{ind}ros_msg.{ros_name} = {inner};") + else: + if is_p2opt: + lines += wrap_optional(acc(proto_name)) + elif is_rep: + lines.append(f"{ind}std::copy(proto_msg.{proto_name}().begin(), proto_msg.{proto_name}().end(),") + lines.append(f"{ind} std::back_inserter(ros_msg.{ros_name}));") + else: + lines.append(f"{ind}ros_msg.{ros_name} = {acc(proto_name)};") + + return lines + + +# ── Passthrough codegen ─────────────────────────────────────────────────────── + +def emit_passthrough( + field: descriptor_pb2.FieldDescriptorProto, + proto2: bool, + ind: str, +) -> list[str]: + lines = [] + name = field.name + is_rep = field.label == FD.LABEL_REPEATED + is_p2opt = proto2 and field.label == FD.LABEL_OPTIONAL and not field.HasField("oneof_index") + + if field.type == FD.TYPE_BYTES: + if is_p2opt: + lines += [ + f"{ind}if (proto_msg.has_{name}()) {{", + f"{ind} auto & _b = {acc(name)};", + f"{ind} ros_msg.{name} = {{std::vector(_b.begin(), _b.end())}};", + f"{ind}}}", + ] + elif is_rep: + lines.append(f"{ind}// TODO: repeated bytes passthrough") + else: + lines += [ + f"{ind}{{", + f"{ind} auto & _b = {acc(name)};", + f"{ind} ros_msg.{name}.assign(_b.begin(), _b.end());", + f"{ind}}}", + ] + return lines + + if is_rep: + if field.type == FD.TYPE_MESSAGE: + lines.append(f"{ind}std::transform(proto_msg.{name}().begin(), proto_msg.{name}().end(),") + lines.append(f"{ind} std::back_inserter(ros_msg.{name}),") + lines.append(f"{ind} [](const auto & p) {{ return fromProto(p); }});") + elif field.type == FD.TYPE_ENUM: + lines.append(f"{ind}std::transform(proto_msg.{name}().begin(), proto_msg.{name}().end(),") + lines.append(f"{ind} std::back_inserter(ros_msg.{name}),") + lines.append(f"{ind} [](const auto & v) {{ return static_cast(v); }});") + else: + lines.append(f"{ind}std::copy(proto_msg.{name}().begin(), proto_msg.{name}().end(),") + lines.append(f"{ind} std::back_inserter(ros_msg.{name}));") + return lines + + if is_p2opt: + if field.type == FD.TYPE_MESSAGE: + lines += [ + f"{ind}if (proto_msg.has_{name}()) {{", + f"{ind} ros_msg.{name} = {{fromProto(proto_msg.{name}())}};", + f"{ind}}}", + ] + elif field.type == FD.TYPE_ENUM: + lines += [ + f"{ind}if (proto_msg.has_{name}()) {{", + f"{ind} ros_msg.{name} = {{static_cast(proto_msg.{name}())}};", + f"{ind}}}", + ] + else: + lines += [ + f"{ind}if (proto_msg.has_{name}()) {{", + f"{ind} ros_msg.{name} = {{proto_msg.{name}()}};", + f"{ind}}}", + ] + return lines + + # Singular non-optional (proto2 required or proto3 default) + if field.type == FD.TYPE_MESSAGE: + if proto2: + lines.append(f"{ind}ros_msg.{name} = fromProto(proto_msg.{name}());") + else: + # proto3 singular message — emit has_ sentinel + lines += [ + f"{ind}if (proto_msg.has_{name}()) {{", + f"{ind} ros_msg.has_{name} = true;", + f"{ind} ros_msg.{name} = fromProto(proto_msg.{name}());", + f"{ind}}}", + ] + elif field.type == FD.TYPE_ENUM: + lines.append(f"{ind}ros_msg.{name} = static_cast({acc(name)});") + else: + lines.append(f"{ind}ros_msg.{name} = {acc(name)};") + + return lines + + +def emit_oneof( + oi: int, + msg: descriptor_pb2.DescriptorProto, + consumed: frozenset, + ind: str, +) -> list[str]: + lines = [] + oneof_name = msg.oneof_decl[oi].name + arms = [f for f in msg.field + if f.HasField("oneof_index") and f.oneof_index == oi + and f.name not in consumed] + + lines.append(f"{ind}ros_msg.{oneof_name}_case = static_cast(proto_msg.{oneof_name}_case());") + lines.append(f"{ind}switch (proto_msg.{oneof_name}_case()) {{") + for f in arms: + const = f"{msg.name}::{proto_oneof_const(f.name)}" + lines.append(f"{ind} case {const}:") + if f.type == FD.TYPE_MESSAGE: + lines.append(f"{ind} ros_msg.{f.name} = fromProto(proto_msg.{f.name}());") + elif f.type == FD.TYPE_ENUM: + lines.append(f"{ind} ros_msg.{f.name} = static_cast({acc(f.name)});") + else: + lines.append(f"{ind} ros_msg.{f.name} = {acc(f.name)};") + lines.append(f"{ind} break;") + lines.append(f"{ind} default: break;") + lines.append(f"{ind}}}") + return lines + + +# ── Header + source generation ──────────────────────────────────────────────── + +LICENSE = """\ +// Copyright 2025 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. +""" + + +def generate_header( + fds: list, + ros_pkg: str, + proto_prefix: str, + namespace: str, +) -> str: + guard = "CORE__MESSAGE_CONVERSION_GENERATED_HPP_" + lines = [ + LICENSE, + "// AUTO-GENERATED — do not edit. Re-run gen_message_conversion.py.", + f"#ifndef {guard}", + f"#define {guard}", + "", + ] + + # Proto pb.h includes + for fd in fds: + pb_h = fd.name.replace(".proto", ".pb.h") + lines.append(f"#include <{proto_prefix}/{pb_h}>") + lines.append("") + + # ROS msg includes + seen_inc = set() + for fd in fds: + for flat, msg in iter_messages(fd): + inc = f"{ros_pkg}/msg/{to_ros_include_name(flat)}.hpp" + if inc not in seen_inc: + seen_inc.add(inc) + lines.append(f"#include <{inc}>") + lines.append("") + + # Common includes + lines += [ + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "", + ] + + for ns in namespace.split("::"): + lines.append(f"namespace {ns}") + lines.append("{") + lines.append("") + + for fd in fds: + for flat, msg in iter_messages(fd): + ros_t = ros_msg_type(flat, ros_pkg) + lines.append(f"{ros_t} fromProto(const {msg.name} & proto_msg);") + + lines.append("") + for ns in reversed(namespace.split("::")): + lines.append(f"}} // namespace {ns}") + lines.append("") + lines.append(f"#endif // {guard}") + return "\n".join(lines) + "\n" + + +def generate_source( + fds: list, + sidecar: dict, + ros_pkg: str, + namespace: str, +) -> str: + lines = [ + LICENSE, + "// AUTO-GENERATED — do not edit. Re-run gen_message_conversion.py.", + "", + '#include "message_conversion_generated.hpp"', + "#include ", + "#include ", + "", + ] + + for ns in namespace.split("::"): + lines.append(f"namespace {ns}") + lines.append("{") + lines.append("") + + for fd in fds: + proto2 = fd.syntax != "proto3" + map_entries = map_entry_type_names(fd) + + for flat, msg in iter_messages(fd): + entry = sidecar.get(flat, {}) + consumed = sidecar_consumed(entry) + pf_map = {f.name: f for f in msg.field} + + ros_t = ros_msg_type(flat, ros_pkg) + lines.append(f"{ros_t} fromProto(const {msg.name} & proto_msg)") + lines.append("{") + lines.append(f" {ros_t} ros_msg;") + + # Sidecar outputs (multi-field → ROS struct) + for out in entry.get("outputs", []): + lines += emit_output(out, " ") + + # Sidecar field overrides + for pname, ann in entry.get("fields", {}).items(): + pf = pf_map.get(pname) + if pf: + lines += emit_field_override(pname, ann, pf, proto2, " ") + + # Passthrough — skip consumed, skip map entries + emitted_oneofs: set = set() + for field in msg.field: + if field.name in consumed: + continue + if field.type == FD.TYPE_MESSAGE and field.type_name in map_entries: + continue + if field.HasField("oneof_index"): + oi = field.oneof_index + if oi not in emitted_oneofs: + emitted_oneofs.add(oi) + lines += emit_oneof(oi, msg, consumed, " ") + continue + lines += emit_passthrough(field, proto2, " ") + + lines.append(" return ros_msg;") + lines.append("}") + lines.append("") + + for ns in reversed(namespace.split("::")): + lines.append(f"}} // namespace {ns}") + + return "\n".join(lines) + "\n" + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--proto-files", nargs="+", required=True) + ap.add_argument("--proto-paths", nargs="+", default=[]) + ap.add_argument("--sidecar", default=None) + ap.add_argument("--output-dir", required=True) + ap.add_argument("--proto-include-prefix", default="ssl_league_protobufs") + ap.add_argument("--ros-package", default="ssl_league_msgs") + ap.add_argument("--cpp-namespace", default="ssl_ros_bridge::message_conversion") + args = ap.parse_args() + + # Produce a FileDescriptorSet via protoc --descriptor_set_out + desc_fd, desc_path = tempfile.mkstemp(suffix=".pb") + os.close(desc_fd) + try: + proto_path_args = [f"--proto_path={p}" for p in args.proto_paths] + r = subprocess.run( + ["protoc", f"--descriptor_set_out={desc_path}", "--include_imports"] + + proto_path_args + args.proto_files, + capture_output=True, text=True, + ) + if r.returncode != 0: + print(f"protoc failed:\n{r.stderr}", file=sys.stderr) + sys.exit(1) + + fds_pb = descriptor_pb2.FileDescriptorSet() + with open(desc_path, "rb") as f: + fds_pb.ParseFromString(f.read()) + finally: + os.unlink(desc_path) + + # Match requested files against descriptor + requested = {Path(p).name for p in args.proto_files} + target_fds = [fd for fd in fds_pb.file if Path(fd.name).name in requested] + if not target_fds: + print("No matching proto files found in descriptor.", file=sys.stderr) + sys.exit(1) + + sidecar = load_sidecar(args.sidecar) + + os.makedirs(args.output_dir, exist_ok=True) + + hpp = generate_header(target_fds, args.ros_package, args.proto_include_prefix, args.cpp_namespace) + cpp = generate_source(target_fds, sidecar, args.ros_package, args.cpp_namespace) + + hpp_path = os.path.join(args.output_dir, "message_conversion_generated.hpp") + cpp_path = os.path.join(args.output_dir, "message_conversion_generated.cpp") + + with open(hpp_path, "w") as f: + f.write(hpp) + with open(cpp_path, "w") as f: + f.write(cpp) + + print(f"Generated {hpp_path}") + print(f"Generated {cpp_path}") + + +if __name__ == "__main__": + main() diff --git a/ssl_ros_bridge/src/core/CMakeLists.txt b/ssl_ros_bridge/src/core/CMakeLists.txt index 4d0ffac..30abd3f 100644 --- a/ssl_ros_bridge/src/core/CMakeLists.txt +++ b/ssl_ros_bridge/src/core/CMakeLists.txt @@ -2,10 +2,13 @@ add_library(${PROJECT_NAME}_core SHARED get_ip_addresses.cpp message_conversion.cpp multicast_receiver.cpp + ${_CONV_OUT}/message_conversion_generated.cpp ) -target_include_directories(${PROJECT_NAME}_core PUBLIC .) +target_include_directories(${PROJECT_NAME}_core PUBLIC . ${_CONV_OUT}) ament_target_dependencies(${PROJECT_NAME}_core rclcpp + builtin_interfaces + geometry_msgs ssl_league_msgs ssl_league_protobufs tf2 From 42e3261060a57cf5c06cd33f9435df259ad888d9 Mon Sep 17 00:00:00 2001 From: Will Stuckey Date: Wed, 12 Aug 2026 16:16:27 -0400 Subject: [PATCH 4/7] prototype after code review + style --- .gitignore | 1 + ssl_league_msgs/CMakeLists.txt | 9 +- .../cmake/AteamProtoGenCommon.cmake | 53 ++ .../cmake/CheckGeneratedMsgList.cmake | 31 + ssl_league_msgs/cmake/Ros2MsgGen.cmake | 107 ++- ssl_league_msgs/cmake/ateam_proto_shared.py | 242 +++++- ssl_league_msgs/cmake/protoc_gen_ros2msg.py | 638 ++++++++++----- .../cmake/ssl_ros_annotations.json | 82 +- ssl_league_msgs/package.xml | 7 +- ssl_ros_bridge/CMakeLists.txt | 3 +- ssl_ros_bridge/cmake/MsgConversionGen.cmake | 62 +- .../cmake/gen_message_conversion.py | 625 ++++++++------- ssl_ros_bridge/src/core/CMakeLists.txt | 1 - .../src/core/message_conversion.cpp | 728 ------------------ .../src/core/message_conversion.hpp | 137 ---- .../gc_multicast_bridge_node.cpp | 2 +- ssl_ros_bridge/src/log2bag/log2bag.cpp | 10 +- .../vision_bridge/ssl_vision_bridge_node.cpp | 8 +- 18 files changed, 1293 insertions(+), 1453 deletions(-) create mode 100644 ssl_league_msgs/cmake/AteamProtoGenCommon.cmake create mode 100644 ssl_league_msgs/cmake/CheckGeneratedMsgList.cmake delete mode 100644 ssl_ros_bridge/src/core/message_conversion.cpp delete mode 100644 ssl_ros_bridge/src/core/message_conversion.hpp diff --git a/.gitignore b/.gitignore index 1345657..6ca2825 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ install/ log/ +**/__pycache__/ diff --git a/ssl_league_msgs/CMakeLists.txt b/ssl_league_msgs/CMakeLists.txt index fcb6b95..7255100 100644 --- a/ssl_league_msgs/CMakeLists.txt +++ b/ssl_league_msgs/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.16) +cmake_minimum_required(VERSION 3.18) project(ssl_league_msgs) find_package(ament_cmake REQUIRED) @@ -20,6 +20,7 @@ generate_ros2_msgs( ${_PROTO_DIR}/ssl_vision_detection.proto ${_PROTO_DIR}/ssl_vision_geometry.proto ${_PROTO_DIR}/ssl_vision_wrapper.proto + ${_PROTO_DIR}/ssl_simulation_config.proto ${_PROTO_DIR}/ssl_simulation_control.proto ${_PROTO_DIR}/ssl_simulation_error.proto PROTO_PATHS @@ -34,4 +35,10 @@ rosidl_generate_interfaces(${PROJECT_NAME} ) ament_export_dependencies(rosidl_default_runtime) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() +endif() + ament_package() diff --git a/ssl_league_msgs/cmake/AteamProtoGenCommon.cmake b/ssl_league_msgs/cmake/AteamProtoGenCommon.cmake new file mode 100644 index 0000000..536be0e --- /dev/null +++ b/ssl_league_msgs/cmake/AteamProtoGenCommon.cmake @@ -0,0 +1,53 @@ +# AteamProtoGenCommon.cmake +# +# Shared plumbing for the two proto-code-generator CMake modules — +# Ros2MsgGen.cmake (ssl_league_msgs, .msg generation) and +# MsgConversionGen.cmake (ssl_ros_bridge, C++ conversion codegen) — so the +# "run a generator script, fail loudly and consistently on error" pattern +# lives once instead of as two hand-copied blocks. +# +# Provides: +# ateam_require_script(SCRIPT_PATH ERROR_PREFIX) +# FATAL_ERROR if SCRIPT_PATH doesn't exist. +# +# ateam_require_sidecar(SIDECAR_PATH ERROR_PREFIX) +# FATAL_ERROR if SIDECAR_PATH is set but doesn't exist. No-op if unset. +# +# ateam_run_generator(COMMAND ERROR_PREFIX ) +# Runs COMMAND via execute_process(); on nonzero exit, FATAL_ERRORs with +# ERROR_PREFIX and the captured stderr. + +cmake_minimum_required(VERSION 3.18) + +function(ateam_require_script SCRIPT_PATH ERROR_PREFIX) + if(NOT EXISTS "${SCRIPT_PATH}") + message(FATAL_ERROR "${ERROR_PREFIX}: script not found at ${SCRIPT_PATH}") + endif() +endfunction() + +function(ateam_require_sidecar SIDECAR_PATH ERROR_PREFIX) + if(SIDECAR_PATH AND NOT EXISTS "${SIDECAR_PATH}") + message(FATAL_ERROR "${ERROR_PREFIX}: SIDECAR file not found: ${SIDECAR_PATH}") + endif() +endfunction() + +function(ateam_run_generator) + cmake_parse_arguments(_ARG "" "ERROR_PREFIX" "COMMAND" ${ARGN}) + if(NOT _ARG_COMMAND) + message(FATAL_ERROR "ateam_run_generator: COMMAND is required") + endif() + if(NOT _ARG_ERROR_PREFIX) + message(FATAL_ERROR "ateam_run_generator: ERROR_PREFIX is required") + endif() + + execute_process( + COMMAND ${_ARG_COMMAND} + RESULT_VARIABLE _result + ERROR_VARIABLE _stderr + OUTPUT_QUIET + ) + + if(NOT _result EQUAL 0) + message(FATAL_ERROR "${_ARG_ERROR_PREFIX} failed (exit ${_result}):\n${_stderr}") + endif() +endfunction() diff --git a/ssl_league_msgs/cmake/CheckGeneratedMsgList.cmake b/ssl_league_msgs/cmake/CheckGeneratedMsgList.cmake new file mode 100644 index 0000000..a7c4818 --- /dev/null +++ b/ssl_league_msgs/cmake/CheckGeneratedMsgList.cmake @@ -0,0 +1,31 @@ +# Invoked via `${CMAKE_COMMAND} -P` as a build-time step after protoc +# regenerates .msg files. rosidl_generate_interfaces() was told a fixed +# file list at the last CMake configure; if protoc now emits a different +# set of files (message added/removed in a .proto), that list is stale +# and only a reconfigure will fix it. Fail loudly rather than silently +# building against the old list. +# +# Args (via -D): OUTPUT_DIR, MANIFEST + +file(GLOB _actual "${OUTPUT_DIR}/msg/*.msg") +set(_actual_names) +foreach(_f ${_actual}) + get_filename_component(_n "${_f}" NAME) + list(APPEND _actual_names "${_n}") +endforeach() +list(SORT _actual_names) + +file(STRINGS "${MANIFEST}" _expected_names) +list(SORT _expected_names) + +if(NOT _actual_names STREQUAL _expected_names) + message(FATAL_ERROR + "Generated .msg file list changed since the last CMake configure " + "(a message was likely added/removed/renamed in a .proto). " + "rosidl_generate_interfaces() was called with a stale file list. " + "Re-run CMake configure (e.g. `colcon build --cmake-force-configure`) " + "and build again.\n" + " expected: ${_expected_names}\n" + " actual: ${_actual_names}" + ) +endif() diff --git a/ssl_league_msgs/cmake/Ros2MsgGen.cmake b/ssl_league_msgs/cmake/Ros2MsgGen.cmake index 1aa6a37..049c582 100644 --- a/ssl_league_msgs/cmake/Ros2MsgGen.cmake +++ b/ssl_league_msgs/cmake/Ros2MsgGen.cmake @@ -1,7 +1,12 @@ # Ros2MsgGen.cmake # -# Provides generate_ros2_msgs() — runs the protoc ros2msg plugin at CMake -# configure time and returns the list of generated .msg files. +# Provides generate_ros2_msgs() — runs the protoc ros2msg plugin once at +# CMake configure time to determine the .msg file list (required up front +# by rosidl_generate_interfaces()), and registers a build-time +# add_custom_command that reruns protoc whenever a proto or plugin script +# changes, so `ninja`/`make` alone regenerates content without a +# reconfigure. If the message *list* itself changes, a build-time check +# fails loudly telling you to reconfigure. # # Usage: # generate_ros2_msgs( @@ -23,11 +28,9 @@ # sidecar are consumed and emitted as annotated ROS types; remaining fields # pass through normally. See protoc_gen_ros2msg.py docstring for schema. # -# Generation runs at configure time so .msg files exist when -# rosidl_generate_interfaces() is called. CMake re-runs automatically when -# any PROTO_FILES changes (CMAKE_CONFIGURE_DEPENDS). +cmake_minimum_required(VERSION 3.18) # CMAKE_CURRENT_FUNCTION_LIST_DIR (3.17), find_program(REQUIRED) (3.18) -cmake_minimum_required(VERSION 3.16) +include("${CMAKE_CURRENT_LIST_DIR}/AteamProtoGenCommon.cmake") function(generate_ros2_msgs) cmake_parse_arguments( @@ -67,14 +70,12 @@ function(generate_ros2_msgs) find_package(Python3 REQUIRED COMPONENTS Interpreter) # --- Plugin path (sibling of this .cmake file) --- - get_filename_component(_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + set(_CMAKE_DIR "${CMAKE_CURRENT_FUNCTION_LIST_DIR}") set(_PLUGIN_SRC "${_CMAKE_DIR}/protoc_gen_ros2msg.py") - if(NOT EXISTS "${_PLUGIN_SRC}") - message(FATAL_ERROR - "generate_ros2_msgs: plugin not found at ${_PLUGIN_SRC}" - ) - endif() + ateam_require_script("${_PLUGIN_SRC}" "generate_ros2_msgs") + + file(GLOB _PLUGIN_DEPS "${_CMAKE_DIR}/*.py") # Generate an executable wrapper in the build tree so we can pass an # explicit Python interpreter without relying on the script's shebang. @@ -101,48 +102,76 @@ function(generate_ros2_msgs) # --- Build plugin options --- set(_plugin_opt "optional_submsg=${_opt_submsg}") if(_ARG_SIDECAR) - if(NOT EXISTS "${_ARG_SIDECAR}") - message(FATAL_ERROR "generate_ros2_msgs: SIDECAR file not found: ${_ARG_SIDECAR}") - endif() + ateam_require_sidecar("${_ARG_SIDECAR}" "generate_ros2_msgs") string(APPEND _plugin_opt ",sidecar=${_ARG_SIDECAR}") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_ARG_SIDECAR}") endif() - # --- Run protoc at configure time --- - # TODO convert this to add_custom_command - execute_process( - COMMAND - "${_PROTOC}" - "--plugin=protoc-gen-ros2msg=${_PLUGIN_WRAPPER}" - "--ros2msg_opt=${_plugin_opt}" - "--ros2msg_out=${_ARG_OUTPUT_DIR}/msg" - ${_proto_path_args} - ${_ARG_PROTO_FILES} - RESULT_VARIABLE _result - ERROR_VARIABLE _stderr - OUTPUT_QUIET + set(_protoc_command + "${_PROTOC}" + "--plugin=protoc-gen-ros2msg=${_PLUGIN_WRAPPER}" + "--ros2msg_opt=${_plugin_opt}" + "--ros2msg_out=${_ARG_OUTPUT_DIR}/msg" + ${_proto_path_args} + ${_ARG_PROTO_FILES} ) - if(NOT _result EQUAL 0) + # Run once now so the generated .msg file *list* is known at configure + # time — rosidl_generate_interfaces() requires it up front. + ateam_run_generator(COMMAND ${_protoc_command} ERROR_PREFIX "generate_ros2_msgs: protoc") + + file(GLOB _generated_abs "${_ARG_OUTPUT_DIR}/msg/*.msg") + if(NOT _generated_abs) message(FATAL_ERROR - "generate_ros2_msgs: protoc failed (exit ${_result}):\n${_stderr}" + "generate_ros2_msgs: no .msg files found in ${_ARG_OUTPUT_DIR}/msg after generation" ) endif() - # Re-run CMake configure when any proto file changes. + set(_depends ${_ARG_PROTO_FILES} ${_PLUGIN_DEPS}) + if(_ARG_SIDECAR) + list(APPEND _depends "${_ARG_SIDECAR}") + endif() + + # Snapshot the expected file list so a build-time check can catch it + # going stale (see CheckGeneratedMsgList.cmake). + set(_manifest "${_ARG_OUTPUT_DIR}/.expected_msgs.txt") + set(_expected_names) + foreach(_f ${_generated_abs}) + get_filename_component(_n "${_f}" NAME) + list(APPEND _expected_names "${_n}") + endforeach() + list(SORT _expected_names) + string(REPLACE ";" "\n" _manifest_contents "${_expected_names}") + file(WRITE "${_manifest}" "${_manifest_contents}\n") + + # Re-run this command at build time (no reconfigure needed) whenever a + # proto file or plugin script changes, so incremental `ninja`/`make` + # builds pick up content changes. The check afterward catches the case + # where the message *list* itself changed (new/removed/renamed message) + # and rosidl_generate_interfaces() now has a stale file list — that + # still requires a reconfigure, so fail loudly instead of building + # silently against old paths. + add_custom_command( + OUTPUT ${_generated_abs} + COMMAND ${_protoc_command} + COMMAND "${CMAKE_COMMAND}" + "-DOUTPUT_DIR=${_ARG_OUTPUT_DIR}" + "-DMANIFEST=${_manifest}" + -P "${_CMAKE_DIR}/CheckGeneratedMsgList.cmake" + DEPENDS ${_depends} + COMMENT "Regenerating ROS2 msg files for ${PROJECT_NAME} from proto sources" + VERBATIM + ) + + # Full reconfigure still required if the message *list* changes, since + # that changes what OUTPUT/rosidl need to know about up front. set_property( DIRECTORY APPEND PROPERTY - CMAKE_CONFIGURE_DEPENDS ${_ARG_PROTO_FILES} + CMAKE_CONFIGURE_DEPENDS ${_depends} ) - # Collect results and expose to caller. - file(GLOB _generated RELATIVE ${_ARG_OUTPUT_DIR} "${_ARG_OUTPUT_DIR}/msg/*.msg") + file(GLOB _generated RELATIVE "${_ARG_OUTPUT_DIR}" "${_ARG_OUTPUT_DIR}/msg/*.msg") list(TRANSFORM _generated PREPEND "${_ARG_OUTPUT_DIR}:") - if(NOT _generated) - message(FATAL_ERROR - "generate_ros2_msgs: no .msg files found in ${_ARG_OUTPUT_DIR}/msg after generation" - ) - endif() set(GENERATED_ROS2_MSGS "${_generated}" PARENT_SCOPE) endfunction() diff --git a/ssl_league_msgs/cmake/ateam_proto_shared.py b/ssl_league_msgs/cmake/ateam_proto_shared.py index 099a809..e1168ab 100644 --- a/ssl_league_msgs/cmake/ateam_proto_shared.py +++ b/ssl_league_msgs/cmake/ateam_proto_shared.py @@ -1,68 +1,250 @@ """ -Shared helpers used by both protoc_gen_ros2msg.py and protoc_gen_ros2cpp.py. +Shared helpers for protoc_gen_ros2msg.py and gen_message_conversion.py. -Factored out to avoid duplication; import with: +protoc_gen_ros2msg.py (ssl_league_msgs) is the .msg generator; +gen_message_conversion.py (ssl_ros_bridge) is the C++ proto<->ROS bridge +generator. Both must agree on naming, map-entry detection, and sidecar +semantics; that logic lives here once instead of two copies. + +Import with: from ateam_proto_shared import ( parse_options, flatten_type_name, strip_package, build_map_entry_type_names, iter_messages, + load_sidecar, sidecar_consumed, output_proto_fields, + field_shape, FieldShape, + FieldAnnotation, OutputEntry, MessageSidecarEntry, Sidecar, + HAS_FIELD_PREFIX, ) """ -from google.protobuf import descriptor_pb2 +import json +from typing import Any, Iterable, Iterator, NamedTuple, TypedDict +from google.protobuf import descriptor_pb2 -def parse_options(parameter: str) -> dict: +FD = descriptor_pb2.FieldDescriptorProto + +# --------------------------------------------------------------------------- +# Sidecar JSON shapes +# --------------------------------------------------------------------------- +# +# Typed as TypedDict, not a dataclass: this is JSON config with no behavior, +# and every call site already uses plain dict access (.get(...), [...]). +# TypedDict types that shape and catches a typo'd key at type-check time +# without changing the runtime value. See the sidecar annotation format in +# protoc_gen_ros2msg.py's module docstring. +# +# OutputEntry uses the functional TypedDict form because one of its keys is +# literally "from", a Python keyword. Class-based TypedDict syntax cannot +# declare an attribute named `from` — the functional constructor is +# required, not just an alternative spelling. + + +class FieldAnnotation(TypedDict, total=False): + """One entry under a message's sidecar 'fields' map.""" + + ros_type: str + ros_field: str + scale: float + skip: bool + default: bool | str | int | float + conversion_func: str # unsupported; present only so it can be rejected + + +# The 'position' or 'orientation' sub-object of a Pose-shaped output entry. +OutputComponentSpec = TypedDict( + 'OutputComponentSpec', + {'from': dict[str, str], 'scale': float}, + total=False, +) + +OutputEntry = TypedDict( + 'OutputEntry', + { + 'ros_field': str, + 'ros_type': str, + 'from': dict[str, str], + 'scale': float, + 'position': OutputComponentSpec, + 'orientation': OutputComponentSpec, + 'conversion_func': str, + }, + total=False, +) + + +class MessageSidecarEntry(TypedDict, total=False): + """The sidecar entry for one message, keyed by the message's flat_name.""" + + fields: dict[str, FieldAnnotation] + outputs: list[OutputEntry] + + +# The sidecar's top level mixes message-name keys (-> MessageSidecarEntry) +# with reserved keys ("_skip_types": list[str], documentation-only +# "_comment"/"_schema") that aren't message entries. TypedDict cannot +# express "arbitrary keys of type A except these keys of type B", so the +# top level stays loosely typed; only entries returned by +# sidecar.get(flat_name, {}) are typed as MessageSidecarEntry. +Sidecar = dict[str, Any] + +# Prefix for the ROS-side presence-sentinel bool emitted alongside a +# non-oneof message-type field under optional_submsg=has_field (see +# protoc_gen_ros2msg.py's module docstring) — e.g. "bool has_foo" next to +# "Foo foo". Shared so the .msg generator (emits the field) and the C++ +# generator (emits "ros_msg.has_foo = true;") cannot drift on the name. +# Not protobuf's own has_foo() accessor — that naming is protobuf's +# convention, not ours. +HAS_FIELD_PREFIX = 'has_' + + +def parse_options(parameter: str) -> dict[str, str]: if not parameter: return {} - return dict(kv.split("=", 1) for kv in parameter.split(",") if "=" in kv) + return dict(kv.split('=', 1) for kv in parameter.split(',') if '=' in kv) def flatten_type_name(type_name: str) -> str: - """Convert a fully-qualified proto type name to a flat ROS2/C++-compatible name. - - Package components (conventionally all-lowercase) are stripped; nested type - components (CamelCase, start with uppercase) are joined with '_'. - - Examples: - .ateam.BasicControl → BasicControl - .GameEvent.BallLeftField → GameEvent_BallLeftField - .ateam_test.OuterMessage.Inner → OuterMessage_Inner """ - return type_name.lstrip(".").split(".")[-1] + Convert a fully-qualified proto type name to a flat ROS2-compatible name. + + Package components (conventionally lowercase) are stripped; nested type + components (CamelCase) are concatenated with no separator. ROS2 message + type names must match '^[A-Z][A-Za-z0-9]*$' — rosidl_adapter rejects + underscores — so unlike protobuf's own C++ class names (which join + nested names with '_', e.g. 'Referee_Point'), the ROS-facing name + cannot use '_' as a nesting separator. + + For example: .ateam.BasicControl -> BasicControl, + .GameEvent.BallLeftField -> GameEventBallLeftField, + .ateam_test.OuterMessage.Inner -> OuterMessageInner. + """ + parts = [p for p in type_name.lstrip('.').split('.') if p and p[0].isupper()] + return ''.join(parts) # Alias preserved for callers that import the name directly. strip_package = flatten_type_name -def build_map_entry_type_names(request) -> frozenset: - """Return fully-qualified field.type_name values that are synthetic map-entry types.""" - result: set = set() +def build_map_entry_type_names( + proto_files: Iterable[descriptor_pb2.FileDescriptorProto], +) -> frozenset[str]: + """ + Return field.type_name values that are synthetic map-entry types. + + Recurses to arbitrary depth across every file in proto_files. proto_files + is any iterable of descriptor_pb2.FileDescriptorProto — e.g. a + CodeGeneratorRequest's `.proto_file`, or a FileDescriptorSet's `.file`. + """ + result: set[str] = set() - def _walk(msg, parent_fqn: str) -> None: - fqn = f"{parent_fqn}.{msg.name}" + def _walk(msg: descriptor_pb2.DescriptorProto, parent_fqn: str) -> None: + fqn = f'{parent_fqn}.{msg.name}' if msg.options.map_entry: result.add(fqn) for nested in msg.nested_type: _walk(nested, fqn) - for fd in request.proto_file: - pkg_prefix = f".{fd.package}" if fd.package else "" + for fd in proto_files: + pkg_prefix = f'.{fd.package}' if fd.package else '' for msg in fd.message_type: _walk(msg, pkg_prefix) return frozenset(result) -def iter_messages(fd): - """Yield (flat_name, msg) for all non-map-entry messages in fd, including nested.""" - def _walk(msg, parent_flat: str): - flat = f"{msg.name}" if parent_flat else msg.name - flat = flat.replace("SSL_", "") +def iter_messages( + fd: descriptor_pb2.FileDescriptorProto, +) -> Iterator[tuple[str, str, descriptor_pb2.DescriptorProto]]: + """ + Yield (flat_name, cpp_name, msg) for all non-map-entry messages in fd. + + Includes nested messages. cpp_name is protobuf's own generated C++ + class name: nested components joined with '_' (e.g. 'Referee_Point'). + Protobuf flattens nested messages to global-scope C++ classes rather + than nesting C++ namespaces or classes; see Referee_Point in a + generated .pb.h. + + flat_name is the ROS-facing name: nested components concatenated with + no separator (e.g. 'RefereePoint'), then any 'SSL_' prefix stripped. + ROS2 message type names must match '^[A-Z][A-Za-z0-9]*$' (no + underscores), so this cannot reuse cpp_name's '_'-joined form. See + flatten_type_name for the same logic applied to field references. + """ + def _walk( + msg: descriptor_pb2.DescriptorProto, parent_cpp: str, parent_flat: str, + ) -> Iterator[tuple[str, str, descriptor_pb2.DescriptorProto]]: + cpp = f'{parent_cpp}_{msg.name}' if parent_cpp else msg.name + flat = f'{parent_flat}{msg.name}' if parent_flat else msg.name + flat = flat.replace('SSL_', '') if not msg.options.map_entry: - yield flat, msg + yield flat, cpp, msg for nested in msg.nested_type: - yield from _walk(nested, flat) + yield from _walk(nested, cpp, flat) for msg in fd.message_type: - yield from _walk(msg, "") + yield from _walk(msg, '', '') + + +class FieldShape(NamedTuple): + """ + Classification of a proto field's ROS2 emission shape. + + One of: repeated (genuinely `repeated` in the proto), in a oneof, or a + proto2 `optional` scalar/message (modeled as a 0/1-element ROS array; + see ros2_field_type). This 3-line computation was duplicated across + both generators; one typed helper replaces it. + """ + + is_repeated: bool + in_oneof: bool + is_proto2_optional: bool + + +def field_shape(field: descriptor_pb2.FieldDescriptorProto, proto2: bool) -> FieldShape: + in_oneof = field.HasField('oneof_index') + return FieldShape( + is_repeated=field.label == FD.LABEL_REPEATED, + in_oneof=in_oneof, + is_proto2_optional=proto2 and field.label == FD.LABEL_OPTIONAL and not in_oneof, + ) + + +# --------------------------------------------------------------------------- +# Sidecar helpers +# --------------------------------------------------------------------------- +# +# Sidecar annotation format is documented in protoc_gen_ros2msg.py's module +# docstring (gen_message_conversion.py's docstring points there rather than +# duplicating it). Both generators consume the same JSON file and must +# agree on what a "consumed" field is, so that logic lives here once. + +def load_sidecar(path: str | None) -> Sidecar: + if not path: + return {} + with open(path) as f: + return json.load(f) + + +def output_proto_fields(out: OutputEntry) -> Iterator[str]: + """Yield proto field names consumed by one sidecar 'outputs' entry.""" + for v in out.get('from', {}).values(): + yield v + for sub in ('position', 'orientation'): + if sub in out: + for v in out[sub].get('from', {}).values(): + yield v + + +def sidecar_consumed(sidecar_entry: MessageSidecarEntry) -> frozenset[str]: + """ + Return the proto field names consumed by a message's sidecar entry. + + Right-hand side of all 'from' maps, plus all 'fields' keys. Excluded + from passthrough emission by both generators. + """ + consumed = set(sidecar_entry.get('fields', {}).keys()) + for out in sidecar_entry.get('outputs', []): + consumed.update(output_proto_fields(out)) + return frozenset(consumed) diff --git a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py index f2605b8..4b9bf65 100755 --- a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py +++ b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py @@ -11,7 +11,8 @@ enum Foo → Foo.msg (constants-only message) repeated T → T[] field oneof foo → uint8 foo_case + constants + all arm fields - enum field → int32 (ROS2 has no enum type; constants in Foo.msg) + enum field → uint8 (ROS2 has no enum type; matches the uint8 + constants emitted for the enum itself) nested message Foo.Bar → Bar.msg with flat name Foo_Bar map field → skipped (no ROS2 map type) @@ -27,7 +28,9 @@ Sidecar annotation format (per message): "MsgName": { "fields": { - "proto_field": { "ros_type": "...", "ros_field": "...", "scale": ... } + "proto_field": { "ros_type": "...", "ros_field": "...", "scale": ... }, + "unsupported_field": { "skip": true }, + "field_with_default": { "default": 1.0 } }, "outputs": [ { @@ -41,11 +44,29 @@ "orientation": { "from": { "x": "q0", ... } } } ] - } + }, + "_skip_types": ["MsgNameToOmitEntirely", ...] Consumed fields (right-hand side of all "from" maps, plus all "fields" keys) - are excluded from passthrough. Generator errors on unknown field references - or user-supplied conversion_func values (only intrinsic conversions supported). + are excluded from passthrough. The generator errors on unknown field + references or user-supplied conversion_func values (only intrinsic + conversions are supported). + + Some proto constructs have no sane ROS translation (e.g. google.protobuf.Any + has no fixed schema to map to a ROS type). The generator fails by default on + these; the sidecar can opt in to dropping them: + - A "skip": true field annotation (must be the only key) omits that field + from the generated .msg, like any other consumed field. + - A top-level "_skip_types" list omits an entire message (no .msg file + emitted). Any field referencing a skipped type must itself carry a + "skip" annotation, or generation errors — skipping a type never + silently skips its referencing fields. + + Recursive message types (a message referencing itself, directly or through + other messages) are rejected up front, with the full reference chain, since + ROS2 .msg files are fixed-layout structs and cannot be self-referential. Use + a "skip" field annotation on one field in the cycle to break it. See + find_type_cycles(). Intrinsic conversions (triggered by ros_type only, no conversion_func needed): builtin_interfaces/Time float/double proto field → from_seconds() @@ -53,41 +74,50 @@ builtin_interfaces/Duration same rules as Time """ -import json -import sys from pathlib import Path -from google.protobuf.compiler import plugin_pb2 +import sys +from typing import Iterable, Iterator + from google.protobuf import descriptor_pb2 +from google.protobuf.compiler import plugin_pb2 sys.path.insert(0, str(Path(__file__).parent)) -from ateam_proto_shared import ( # noqa: E402 - parse_options, - flatten_type_name, +# Must follow the sys.path.insert() above, so this can't sort before the +# google.protobuf imports the way import-order linting wants. +from ateam_proto_shared import ( # noqa: E402, I100 build_map_entry_type_names, + field_shape, + FieldAnnotation, + flatten_type_name, + HAS_FIELD_PREFIX, iter_messages, + load_sidecar, + MessageSidecarEntry, + output_proto_fields, + OutputEntry, + parse_options, + Sidecar, + sidecar_consumed, ) -# Public alias: tests may import plugin.strip_package. -strip_package = flatten_type_name - FD = descriptor_pb2.FieldDescriptorProto SCALAR_TYPE_MAP = { - FD.TYPE_DOUBLE: "float64", - FD.TYPE_FLOAT: "float32", - FD.TYPE_INT64: "int64", - FD.TYPE_UINT64: "uint64", - FD.TYPE_INT32: "int32", - FD.TYPE_FIXED64: "uint64", - FD.TYPE_FIXED32: "uint32", - FD.TYPE_BOOL: "bool", - FD.TYPE_STRING: "string", - FD.TYPE_BYTES: "uint8[]", - FD.TYPE_UINT32: "uint32", - FD.TYPE_SINT32: "int32", - FD.TYPE_SINT64: "int64", - FD.TYPE_SFIXED32: "int32", - FD.TYPE_SFIXED64: "int64", + FD.TYPE_DOUBLE: 'float64', + FD.TYPE_FLOAT: 'float32', + FD.TYPE_INT64: 'int64', + FD.TYPE_UINT64: 'uint64', + FD.TYPE_INT32: 'int32', + FD.TYPE_FIXED64: 'uint64', + FD.TYPE_FIXED32: 'uint32', + FD.TYPE_BOOL: 'bool', + FD.TYPE_STRING: 'string', + FD.TYPE_BYTES: 'uint8[]', + FD.TYPE_UINT32: 'uint32', + FD.TYPE_SINT32: 'int32', + FD.TYPE_SINT64: 'int64', + FD.TYPE_SFIXED32: 'int32', + FD.TYPE_SFIXED64: 'int64', } _FLOAT_TYPES = frozenset({FD.TYPE_FLOAT, FD.TYPE_DOUBLE}) @@ -98,135 +128,271 @@ }) _INTRINSIC_ROS_TYPES = frozenset({ - "builtin_interfaces/Time", - "builtin_interfaces/Duration", + 'builtin_interfaces/Time', + 'builtin_interfaces/Duration', }) +# Populated by main() from sidecar "_skip_types" before generation runs. +_SKIP_TYPES: frozenset = frozenset() + def ros2_field_type(field: descriptor_pb2.FieldDescriptorProto) -> str: if field.type in SCALAR_TYPE_MAP: return SCALAR_TYPE_MAP[field.type] + if field.type == FD.TYPE_ENUM: - return "int8" - if field.type == FD.TYPE_MESSAGE: - if field.type_name == ".google.protobuf.Any": - raise ValueError("Fields with type 'Any' are not supported.") - return flatten_type_name(field.type_name).replace("SSL_", "") - raise ValueError(f"unhandled proto field type {field.type} in field '{field.name}'") + return 'uint8' + if field.type == FD.TYPE_MESSAGE: + if field.type_name == '.google.protobuf.Any': + raise ValueError( + "Fields with type 'Any' are not supported (no fixed schema to map " + "to a ROS type). Add a 'skip' field annotation in the sidecar." + ) -def iter_enums(fd): - """Yield (flat_name, enum) for all enums in fd, including those nested in messages.""" - for enum in fd.enum_type: - yield enum.name, enum + flat = flatten_type_name(field.type_name).replace('SSL_', '') + if flat in _SKIP_TYPES: + raise ValueError( + f"references type '{flat}', which is skipped via sidecar " + f"'_skip_types'. Add a 'skip' field annotation for this field, " + f"or remove '{flat}' from '_skip_types'." + ) - def _walk(msg, parent_flat: str): - flat = f"{parent_flat}_{msg.name}" if parent_flat else msg.name - for enum in msg.enum_type: - yield f"{flat}_{enum.name}", enum - for nested in msg.nested_type: - yield from _walk(nested, flat) + return flat - for msg in fd.message_type: - yield from _walk(msg, "") + raise ValueError(f"unhandled proto field type {field.type} in field '{field.name}'") # --------------------------------------------------------------------------- -# Sidecar helpers +# Sidecar validation (error accumulation is specific to this script's +# protoc-plugin response.error mechanism, so this stays local; +# load_sidecar/sidecar_consumed/output_proto_fields are shared — see +# ateam_proto_shared.py) # --------------------------------------------------------------------------- -def load_sidecar(path: str | None) -> dict: - if not path: - return {} - with open(path) as f: - return json.load(f) - -def _output_proto_fields(out: dict): - """Yield proto field names consumed by one outputs entry.""" - for v in out.get("from", {}).values(): - yield v - for sub in ("position", "orientation"): - if sub in out: - for v in out[sub].get("from", {}).values(): - yield v +def _validate_sidecar_entry( + sidecar_entry: MessageSidecarEntry, + proto_field_map: dict[str, descriptor_pb2.FieldDescriptorProto], + flat_name: str, + errors: list[str], +) -> None: + for proto_name, ann in sidecar_entry.get('fields', {}).items(): + if proto_name not in proto_field_map: + errors.append( + f"{flat_name}: sidecar 'fields' references unknown proto field '{proto_name}'" + ) + continue -def _sidecar_consumed(sidecar_entry: dict) -> frozenset: - """Return the set of proto field names consumed by a message's sidecar entry.""" - consumed = set(sidecar_entry.get("fields", {}).keys()) - for out in sidecar_entry.get("outputs", []): - consumed.update(_output_proto_fields(out)) - return frozenset(consumed) + if ann.get('skip'): + extra_keys = sorted( + k for k in ann if k != 'skip' and not k.startswith('_') + ) + if extra_keys: + errors.append( + f"{flat_name}.{proto_name}: 'skip' must be the only key in a " + f'skip annotation (found {extra_keys})' + ) + continue -def _validate_sidecar_entry(sidecar_entry: dict, proto_field_map: dict, flat_name: str, errors: list): - for proto_name, ann in sidecar_entry.get("fields", {}).items(): - if proto_name not in proto_field_map: - errors.append(f"{flat_name}: sidecar 'fields' references unknown proto field '{proto_name}'") - if "conversion_func" in ann: + if 'conversion_func' in ann: errors.append( f"{flat_name}.{proto_name}: user-supplied 'conversion_func' is not supported; " - f"only intrinsic conversions (builtin_interfaces/Time, /Duration) are available." + f'only intrinsic conversions (builtin_interfaces/Time, /Duration) are available.' ) - for out in sidecar_entry.get("outputs", []): - ros_field = out.get("ros_field", "") - for proto_name in _output_proto_fields(out): + + if 'default' in ann: + pf = proto_field_map[proto_name] + if pf.label == FD.LABEL_REPEATED: + errors.append( + f"{flat_name}.{proto_name}: 'default' is not supported on a " + f"genuinely repeated proto field — a single default value can't " + f'stand in for a whole list.' + ) + + for out in sidecar_entry.get('outputs', []): + ros_field = out.get('ros_field', '') + for proto_name in output_proto_fields(out): if proto_name not in proto_field_map: errors.append( - f"{flat_name}: sidecar output '{ros_field}' references unknown proto field '{proto_name}'" + f"{flat_name}: sidecar output '{ros_field}' references unknown " + f"proto field '{proto_name}'" ) - if "conversion_func" in out: + + if 'conversion_func' in out: errors.append( - f"{flat_name}: output '{ros_field}': user-supplied 'conversion_func' is not supported." + f"{flat_name}: output '{ros_field}': user-supplied 'conversion_func' " + f'is not supported.' ) -def _emit_sidecar_outputs(sidecar_entry: dict, lines: list): - """Emit ROS struct fields from outputs entries.""" - for out in sidecar_entry.get("outputs", []): - lines.append(f"{out['ros_type']} {out['ros_field']}") +def _message_type_edges( + msg: descriptor_pb2.DescriptorProto, + consumed: frozenset[str], + map_entry_type_names: frozenset[str], +) -> Iterator[tuple[str, str]]: + """ + Yield (proto_field_name, target_flat_type_name) for msg's message-type fields. + + Only fields that will survive into the generated .msg — not consumed by + the sidecar, not a map entry, not Any, not a skipped type. + """ + for field in msg.field: + if field.name in consumed: + continue + if field.type != FD.TYPE_MESSAGE: + continue + if field.type_name in map_entry_type_names: + continue + if field.type_name == '.google.protobuf.Any': + continue + target = flatten_type_name(field.type_name).replace('SSL_', '') + if target in _SKIP_TYPES: + continue + yield field.name, target + + +def find_type_cycles( + all_files: dict[str, descriptor_pb2.FileDescriptorProto], + file_to_generate_names: Iterable[str], + sidecar: Sidecar, + map_entry_type_names: frozenset[str], +) -> list[str]: + """ + Detect cycles in the message-type reference graph across every generated message. + + Considers the graph post sidecar consumption/skip. ROS2 .msg files are + fixed-layout structs and cannot be self-referential, even indirectly — + a cycle always breaks generation downstream (rosidl_generator_type_description + has a latent bug: it crashes with an opaque KeyError instead of a clean + error; see calculate_type_hash's double-delete of a cycled type's + 'default_value' key after deepcopy aliasing). Detect it here instead, + with the concrete chain, so the sidecar can 'skip' a field to break it. + """ + graph: dict[str, list[tuple[str, str]]] = {} + for file_name in file_to_generate_names: + fd = all_files[file_name] + for flat_name, _cpp_name, msg in iter_messages(fd): + if flat_name in _SKIP_TYPES: + continue + + consumed = sidecar_consumed(sidecar.get(flat_name, {})) + graph[flat_name] = list(_message_type_edges(msg, consumed, map_entry_type_names)) + + found: set[tuple[tuple[str, ...], tuple[str, ...]]] = set() # canonicalized (nodes, fields) + + def dfs(node: str, path: list[str], path_fields: list[str]) -> None: + if node in path: + i = path.index(node) + nodes = tuple(path[i:]) + (node,) + fields = tuple(path_fields[i:]) + # Canonicalize so the same cycle found from different start + # nodes collapses to one report. + j = nodes[:-1].index(min(nodes[:-1])) + canon_nodes = nodes[j:-1] + nodes[:j] + (nodes[j],) + canon_fields = fields[j:] + fields[:j] + found.add((canon_nodes, canon_fields)) + + return + + for field_name, target in graph.get(node, []): + if target in graph: + dfs(target, path + [node], path_fields + [field_name]) + + for start in graph: + dfs(start, [], []) + + errors: list[str] = [] + for nodes, fields in sorted(found): + chain = ''.join( + f'{n} --[{f}]--> ' for n, f in zip(nodes, fields) + ) + nodes[-1] + errors.append( + f'Recursive message type reference detected: {chain}. ROS2 ' + f'messages are fixed-layout structs and cannot be ' + f"self-referential. Add a 'skip' field annotation in the " + f'sidecar on one of the fields in the cycle to break it.' + ) + + return errors + + +def _emit_one_output(out: OutputEntry, lines: list[str]) -> None: + """Emit the single ROS struct field for one sidecar 'outputs' entry.""" + lines.append(f"{out['ros_type']} {out['ros_field']}") + + +def _default_literal(value: bool | str | int | float) -> str: + """Format a sidecar 'default' JSON value as a ROS2 .msg default-value literal.""" + if isinstance(value, bool): + return 'true' if value else 'false' + + if isinstance(value, str): + escaped = value.replace('\\', '\\\\').replace('"', '\\"') + return f'"{escaped}"' + return repr(value) -def _emit_sidecar_fields( - sidecar_entry: dict, - proto_field_map: dict, - lines: list, + +def _emit_one_sidecar_field( + proto_name: str, + ann: FieldAnnotation, + pf: descriptor_pb2.FieldDescriptorProto, + lines: list[str], proto2: bool, - errors: list, + errors: list[str], flat_name: str, -): - """Emit annotated single-field overrides (ros_type, ros_field, scale).""" - for proto_name, ann in sidecar_entry.get("fields", {}).items(): - pf = proto_field_map.get(proto_name) - if pf is None: - continue # already captured in validation - - ros_name = ann.get("ros_field", proto_name) - - if "ros_type" in ann: - ros_type = ann["ros_type"] - if ros_type in _INTRINSIC_ROS_TYPES and pf.type not in (_FLOAT_TYPES | _INT_TYPES): - # Non-scalar source for Time/Duration — still emit, bridge layer will handle - pass - elif "scale" in ann and pf.type not in _FLOAT_TYPES: - errors.append( - f"{flat_name}.{proto_name}: 'scale' on non-float field " - f"({SCALAR_TYPE_MAP.get(pf.type, f'type={pf.type}')}) requires an explicit " - f"'ros_type' — scaling an integer without type promotion loses precision." - ) - continue - else: +) -> None: + """Emit one annotated single-field override (ros_type, ros_field, scale, default).""" + if ann.get('skip'): + return # already excluded from passthrough via `consumed` + + ros_name = ann.get('ros_field', proto_name) + + if 'ros_type' in ann: + ros_type = ann['ros_type'] + if ros_type in _INTRINSIC_ROS_TYPES and pf.type not in (_FLOAT_TYPES | _INT_TYPES): + # Non-scalar source for Time/Duration; still emit — the bridge layer handles it + pass + elif 'scale' in ann and pf.type not in _FLOAT_TYPES: + errors.append( + f"{flat_name}.{proto_name}: 'scale' on non-float field " + f"({SCALAR_TYPE_MAP.get(pf.type, f'type={pf.type}')}) requires an explicit " + f"'ros_type' — scaling an integer without type promotion loses precision." + ) + + return + else: + try: ros_type = ros2_field_type(pf) + except ValueError as e: + errors.append(f'{flat_name}.{proto_name}: {e}') - is_repeated = pf.label == FD.LABEL_REPEATED - in_oneof = pf.HasField("oneof_index") - is_proto2_optional = proto2 and pf.label == FD.LABEL_OPTIONAL and not in_oneof + return - if is_repeated or is_proto2_optional: - lines.append(f"{ros_type}[] {ros_name}") + shape = field_shape(pf, proto2) + + if 'default' not in ann: + if shape.is_repeated or shape.is_proto2_optional: + lines.append(f'{ros_type}[] {ros_name}') else: - lines.append(f"{ros_type} {ros_name}") + lines.append(f'{ros_type} {ros_name}') + + return + + # 'default' was already rejected at validation time for genuinely repeated + # fields (a single value cannot stand in for a whole list). A proto2 + # "optional" scalar is modeled as a 0/1-element ROS array (see + # ros2_field_type), so its default is a 1-element array literal, matching + # the presence convention: unset proto field -> empty array, default ROS + # field -> that one value present. + default_lit = _default_literal(ann['default']) + if shape.is_proto2_optional: + lines.append(f'{ros_type}[] {ros_name} [{default_lit}]') + else: + lines.append(f'{ros_type} {ros_name} {default_lit}') # --------------------------------------------------------------------------- @@ -236,110 +402,173 @@ def _emit_sidecar_fields( def generate_message_msg( msg: descriptor_pb2.DescriptorProto, flat_name: str, + source_file: str, optional_submsg: str, - errors: list, - map_entry_type_names: frozenset, + errors: list[str], + map_entry_type_names: frozenset[str], proto2: bool = False, - sidecar_entry: dict | None = None, + sidecar_entry: MessageSidecarEntry | None = None, ) -> str: if sidecar_entry is None: sidecar_entry = {} - lines = [f"# Generated from proto message {flat_name}"] + lines = [ + f'# Generated from proto message {flat_name}', + f'# Source: {source_file}', + '', + ] proto_field_map = {f.name: f for f in msg.field} # Validate sidecar references before emitting anything. _validate_sidecar_entry(sidecar_entry, proto_field_map, flat_name, errors) - consumed = _sidecar_consumed(sidecar_entry) - - # 1. Sidecar outputs (multi-field → ROS struct). - _emit_sidecar_outputs(sidecar_entry, lines) - - # 2. Sidecar field overrides (single-field with ros_type / ros_field / scale). - _emit_sidecar_fields(sidecar_entry, proto_field_map, lines, proto2, errors, flat_name) - - # 3. Enum value constants + consumed = sidecar_consumed(sidecar_entry) + field_overrides = sidecar_entry.get('fields', {}) + # 1. Enum value constants, upfront per ROS2 convention — one blank line + # after each enum's cluster. for enum in msg.enum_type: + lines.append(f'# {enum.name}') for value in enum.value: - constant_name = enum.name.upper() + "_" + value.name.upper() + constant_name = enum.name.upper() + '_' + value.name.upper() constant_value = value.number - lines.append(f"uint8 {constant_name} = {constant_value}") + lines.append(f'uint8 {constant_name} = {constant_value}') + + lines.append('') + + # 2. Fields, walked in proto declaration order — this holds even for + # sidecar-annotated fields/outputs, so the .msg reads in the same order + # as the .proto. A sidecar 'outputs' entry (several proto fields + # collapsing into one ROS field, e.g. x/y/z -> a Point32) is emitted once, + # at the position of the first proto field it consumes; the entry's other + # consumed fields are skipped where they would otherwise fall, rather + # than splitting the composite field's data across multiple positions. + pending_outputs: list[tuple[OutputEntry, frozenset[str]]] = [ + (out, frozenset(output_proto_fields(out))) + for out in sidecar_entry.get('outputs', []) + ] + output_trigger: dict[str, OutputEntry] = {} + for field in msg.field: + for i, (out, out_consumed) in enumerate(pending_outputs): + if field.name in out_consumed: + output_trigger[field.name] = out + del pending_outputs[i] - lines.append("\n") + break - # 4. Passthrough: proto fields not consumed by the sidecar. - emitted_oneofs: set = set() + emitted_oneofs: set[int] = set() for field in msg.field: + if field.name in output_trigger: + _emit_one_output(output_trigger[field.name], lines) + continue + if field.name in consumed: + if field.name in field_overrides: + _emit_one_sidecar_field( + field.name, field_overrides[field.name], field, lines, + proto2, errors, flat_name, + ) + # else: consumed by an 'outputs' entry, already emitted at + # that entry's trigger field position above. continue + if field.type == FD.TYPE_MESSAGE and field.type_name in map_entry_type_names: + errors.append( + f'{flat_name}.{field.name}: proto map fields have no ROS2 ' + f"equivalent and are not supported. Add a 'skip' field annotation " + f'in the sidecar for this field to drop it explicitly.' + ) continue - is_repeated = field.label == FD.LABEL_REPEATED - in_oneof = field.HasField("oneof_index") - is_proto2_optional = ( - proto2 - and field.label == FD.LABEL_OPTIONAL - and not in_oneof - ) + shape = field_shape(field, proto2) + is_repeated, in_oneof, is_proto2_optional = shape if in_oneof: oi = field.oneof_index if oi in emitted_oneofs: continue + emitted_oneofs.add(oi) oneof_name = msg.oneof_decl[oi].name oneof_fields = [ f for f in msg.field - if f.HasField("oneof_index") and f.oneof_index == oi + if f.HasField('oneof_index') and f.oneof_index == oi ] - lines.append("") - lines.append(f"# oneof {oneof_name}") - lines.append(f"# case constants use proto field numbers (stable across reordering)") - lines.append(f"uint8 ONEOF_{oneof_name.upper()}_NONE=0") + lines.append('') + lines.append(f'# oneof {oneof_name}') + lines.append('# case constants use proto field numbers (stable across reordering)') + lines.append(f'uint8 ONEOF_{oneof_name.upper()}_NONE=0') for of in oneof_fields: if of.number > 255: errors.append( f"{flat_name}: oneof '{oneof_name}' field '{of.name}' has field " - f"number {of.number} which exceeds the uint8 range (max 255) used " - f"for the case discriminant. Use field numbers ≤ 255 in oneof " - f"declarations, or file a request to widen the discriminant type." + f'number {of.number} which exceeds the uint8 range (max 255) used ' + f'for the case discriminant. Use field numbers ≤ 255 in oneof ' + f'declarations, or file a request to widen the discriminant type.' ) continue - const = f"ONEOF_{oneof_name.upper()}_{of.name.upper()}" - lines.append(f"uint8 {const}={of.number}") - lines.append(f"uint8 {oneof_name}_case") + + const = f'ONEOF_{oneof_name.upper()}_{of.name.upper()}' + lines.append(f'uint8 {const}={of.number}') + lines.append(f'uint8 {oneof_name}_case') for of in oneof_fields: - lines.append(f"{ros2_field_type(of)} {of.name}") + try: + lines.append(f'{ros2_field_type(of)} {of.name}') + except ValueError as e: + errors.append(f'{flat_name}.{of.name}: {e}') + continue - ros2_type = ros2_field_type(field) + try: + ros2_type = ros2_field_type(field) + except ValueError as e: + errors.append(f'{flat_name}.{field.name}: {e}') + continue if is_repeated or is_proto2_optional: - lines.append(f"{ros2_type}[] {field.name}") + lines.append(f'{ros2_type}[] {field.name}') elif field.type == FD.TYPE_MESSAGE and not is_repeated: if proto2: - lines.append(f"{ros2_type} {field.name}") - elif optional_submsg == "error": + lines.append(f'{ros2_type} {field.name}') + elif optional_submsg == 'error': errors.append( - f"{flat_name}.{field.name}: non-oneof message-type field has " - f"implicit proto3 presence — set optional_submsg=has_field to " - f"auto-generate a bool presence flag, or move into a oneof." + f'{flat_name}.{field.name}: non-oneof message-type field has ' + f'implicit proto3 presence — set optional_submsg=has_field to ' + f'auto-generate a bool presence flag, or move into a oneof.' ) continue else: - lines.append(f"bool has_{field.name}") - lines.append(f"{ros2_type} {field.name}") + lines.append(f'bool {HAS_FIELD_PREFIX}{field.name}') + lines.append(f'{ros2_type} {field.name}') else: - lines.append(f"{ros2_type} {field.name}") + lines.append(f'{ros2_type} {field.name}') - return "\n".join(lines) + "\n" + return '\n'.join(lines) + '\n' + + +def generate_enum_msg( + enum: descriptor_pb2.EnumDescriptorProto, flat_name: str, source_file: str, +) -> str: + """ + Constants-only .msg for a top-level proto enum. + + Nested enums are handled separately, inline in their containing message + (see the 'Enum value constants' step in generate_message_msg) — this + covers only enums declared at file scope, which have no containing + message to attach constants to. + """ + lines = [ + f'# Generated from proto enum {flat_name}', + f'# Source: {source_file}', + '', + ] + for value in enum.value: + lines.append(f'uint8 {value.name.upper()}={value.number}') + return '\n'.join(lines) + '\n' def main() -> None: @@ -353,39 +582,86 @@ def main() -> None: ) opts = parse_options(request.parameter) - optional_submsg = opts.get("optional_submsg", "has_field") - if optional_submsg not in ("has_field", "error"): + optional_submsg = opts.get('optional_submsg', 'has_field') + if optional_submsg not in ('has_field', 'error'): response.error = ( - f"Unknown optional_submsg={optional_submsg!r}. " + f'Unknown optional_submsg={optional_submsg!r}. ' f"Valid values: 'has_field', 'error'." ) sys.stdout.buffer.write(response.SerializeToString()) + return - sidecar_path = opts.get("sidecar", None) - sidecar = load_sidecar(sidecar_path) + sidecar_path = opts.get('sidecar', None) + sidecar: Sidecar = load_sidecar(sidecar_path) + + global _SKIP_TYPES + _SKIP_TYPES = frozenset(sidecar.get('_skip_types', [])) + seen_skip_types: set[str] = set() - map_entry_type_names = build_map_entry_type_names(request) - all_files = {f.name: f for f in request.proto_file} - errors: list = [] + map_entry_type_names = build_map_entry_type_names(request.proto_file) + all_files: dict[str, descriptor_pb2.FileDescriptorProto] = { + f.name: f for f in request.proto_file + } + errors: list[str] = [] + + errors.extend(find_type_cycles( + all_files, request.file_to_generate, sidecar, map_entry_type_names + )) + + emitted_names: set[str] = set() for file_name in request.file_to_generate: fd = all_files[file_name] + source_file = Path(file_name).name - proto2 = (fd.syntax != "proto3") - for flat_name, msg in iter_messages(fd): + proto2 = (fd.syntax != 'proto3') + for flat_name, _cpp_name, msg in iter_messages(fd): + if flat_name in _SKIP_TYPES: + seen_skip_types.add(flat_name) + continue + + if flat_name in emitted_names: + errors.append(f"duplicate generated message name '{flat_name}'") + continue + + emitted_names.add(flat_name) out = response.file.add() - out.name = f"{flat_name}.msg" + out.name = f'{flat_name}.msg' out.content = generate_message_msg( - msg, flat_name, optional_submsg, errors, map_entry_type_names, + msg, flat_name, source_file, optional_submsg, errors, map_entry_type_names, proto2, sidecar_entry=sidecar.get(flat_name, {}) ) + for enum in fd.enum_type: + flat_name = enum.name.replace('SSL_', '') + if flat_name in _SKIP_TYPES: + seen_skip_types.add(flat_name) + continue + + if flat_name in emitted_names: + errors.append( + f"duplicate generated message name '{flat_name}' (from top-level enum)" + ) + continue + + emitted_names.add(flat_name) + out = response.file.add() + out.name = f'{flat_name}.msg' + out.content = generate_enum_msg(enum, flat_name, source_file) + + unused_skip_types = _SKIP_TYPES - seen_skip_types + if unused_skip_types: + errors.append( + f"sidecar '_skip_types' references message(s) never encountered " + f'during generation: {sorted(unused_skip_types)}' + ) + if errors: - response.error = "\n".join(errors) + response.error = '\n'.join(errors) sys.stdout.buffer.write(response.SerializeToString()) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/ssl_league_msgs/cmake/ssl_ros_annotations.json b/ssl_league_msgs/cmake/ssl_ros_annotations.json index f908246..65c46b8 100644 --- a/ssl_league_msgs/cmake/ssl_ros_annotations.json +++ b/ssl_league_msgs/cmake/ssl_ros_annotations.json @@ -7,9 +7,14 @@ "scale": "Multiply all consumed scalar components by this factor (metadata; applied at bridge layer).", "ros_type": "Override emitted ROS type. Required for builtin_interfaces/Time and /Duration (triggers intrinsic conversion based on proto wire type: float/double → from_seconds, int/uint → from_microseconds).", "ros_field": "Override output field name. Omit to keep proto field name.", - "conversion_func": "Reserved for future user-supplied conversions. Only intrinsic conversions (Time/Duration) are supported now; generator errors on any user-supplied value." + "conversion_func": "Reserved for future user-supplied conversions. Only intrinsic conversions (Time/Duration) are supported now; generator errors on any user-supplied value.", + "skip": "On a field annotation: omit this field from the generated .msg entirely (must be the only key in the annotation). For proto constructs with no sane ROS mapping, e.g. google.protobuf.Any.", + "default": "ROS2 .msg default value for this field (proto has no equivalent — [default=] is proto2-only and not read by the generator). Not supported on genuinely repeated fields. On a proto2 'optional' scalar (modeled as a 0/1-element ROS array), emits a 1-element array default, matching the presence convention: proto unset -> empty array, ROS default -> that one value present.", + "_skip_types": "Top-level list of message names to omit entirely (no .msg file emitted). Any field elsewhere referencing a skipped type must itself carry a 'skip' annotation, or generation errors." }, + "_skip_types": [], + "DetectionBall": { "outputs": [ { @@ -145,46 +150,107 @@ } }, - "Referee_Point": { + "RefereePoint": { "fields": { "x": { "scale": 1e-3 }, "y": { "scale": 1e-3 } } }, - "KeeperHeldBall": { + "GameEventKeeperHeldBall": { "fields": { "duration": { "ros_type": "builtin_interfaces/Duration" } } }, - "BotHeldBallDeliberately": { + "GameEventBotHeldBallDeliberately": { "fields": { "duration": { "ros_type": "builtin_interfaces/Duration" } } }, - "KickTimeout": { + "GameEventKickTimeout": { "fields": { "time": { "ros_type": "builtin_interfaces/Duration" } } }, - "NoProgressInGame": { + "GameEventNoProgressInGame": { "fields": { "time": { "ros_type": "builtin_interfaces/Duration" } } }, - "PlacementSucceeded": { + "GameEventPlacementSucceeded": { "fields": { "time_taken": { "ros_type": "builtin_interfaces/Duration" } } }, - "Prepared": { + "GameEventPrepared": { "fields": { "time_taken": { "ros_type": "builtin_interfaces/Duration" } } + }, + + "TeleportBall": { + "outputs": [ + { + "ros_field": "pos", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "x", "y": "y", "z": "z" }, + "_consumed": ["x", "y", "z"], + "_comment": "already meters (proto field comments say [m]) -- no scale, unlike SSL vision/geometry protos which use mm" + }, + { + "ros_field": "velocity", + "ros_type": "geometry_msgs/Vector3", + "from": { "x": "vx", "y": "vy", "z": "vz" }, + "_consumed": ["vx", "vy", "vz"] + } + ] + }, + + "TeleportRobot": { + "outputs": [ + { + "ros_field": "pos", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "x", "y": "y" }, + "_consumed": ["x", "y"], + "_comment": "meters; z unused (2D). orientation (yaw, rad) passes through separately -- not packaged into a quaternion" + }, + { + "ros_field": "velocity", + "ros_type": "geometry_msgs/Vector3", + "from": { "x": "v_x", "y": "v_y" }, + "_consumed": ["v_x", "v_y"], + "_comment": "z unused; angular velocity is scalar v_angular (2D yaw rate), passes through separately rather than as a 3-vector" + } + ] + }, + + "RobotSpecs": { + "fields": { + "custom": { "skip": true, "_comment": "google.protobuf.Any; simulator-specific, no fixed schema to map to ROS" } + } + }, + + "RealismConfig": { + "fields": { + "custom": { "skip": true, "_comment": "google.protobuf.Any; simulator-specific, no fixed schema to map to ROS" } + } + }, + + "GameEventMultipleFouls": { + "fields": { + "caused_game_events": { "skip": true, "_comment": "repeated GameEvent -- recursive self-reference (GameEvent -> GameEventMultipleFouls -> GameEvent); ROS2 messages are fixed-layout structs and cannot be self-referential. Same conclusion reached by the hand-crafted msgs this generator replaces (see main branch MultipleFouls.msg TODO)." } + } + }, + + "SimulatorControl": { + "fields": { + "simulation_speed": { "default": 1.0, "_comment": "proto has no [default=]; matches the hand-crafted msg this generator replaces, which defaulted to normal (1x) speed" } + } } } diff --git a/ssl_league_msgs/package.xml b/ssl_league_msgs/package.xml index 1ff85b1..998c4b3 100644 --- a/ssl_league_msgs/package.xml +++ b/ssl_league_msgs/package.xml @@ -5,9 +5,11 @@ 0.0.0 Message definitions for RoboCup SSL league-defined messages Matthew Barulic - TODO: License declaration + MIT ament_cmake + protobuf-dev + python3-protobuf rosidl_default_generators @@ -17,6 +19,9 @@ std_msgs geometry_msgs + ament_lint_auto + ament_lint_common + rosidl_interface_packages diff --git a/ssl_ros_bridge/CMakeLists.txt b/ssl_ros_bridge/CMakeLists.txt index dbc4081..d31fb4e 100644 --- a/ssl_ros_bridge/CMakeLists.txt +++ b/ssl_ros_bridge/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.18) project(ssl_ros_bridge) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") @@ -34,6 +34,7 @@ generate_message_conversion( ${_PROTO_DIR}/ssl_vision_detection.proto ${_PROTO_DIR}/ssl_vision_geometry.proto ${_PROTO_DIR}/ssl_vision_wrapper.proto + ${_PROTO_DIR}/ssl_simulation_config.proto ${_PROTO_DIR}/ssl_simulation_control.proto ${_PROTO_DIR}/ssl_simulation_error.proto PROTO_PATHS ${_PROTO_DIR} diff --git a/ssl_ros_bridge/cmake/MsgConversionGen.cmake b/ssl_ros_bridge/cmake/MsgConversionGen.cmake index 0fb2b97..dce835d 100644 --- a/ssl_ros_bridge/cmake/MsgConversionGen.cmake +++ b/ssl_ros_bridge/cmake/MsgConversionGen.cmake @@ -16,7 +16,9 @@ # # CMake re-runs automatically when any PROTO_FILES or the SIDECAR changes. -cmake_minimum_required(VERSION 3.16) +cmake_minimum_required(VERSION 3.18) # CMAKE_CURRENT_FUNCTION_LIST_DIR (3.17), find_program(REQUIRED) (3.18) + +include("${CMAKE_CURRENT_LIST_DIR}/../../ssl_league_msgs/cmake/AteamProtoGenCommon.cmake") function(generate_message_conversion) cmake_parse_arguments(_ARG "" "OUTPUT_DIR;SIDECAR" "PROTO_FILES;PROTO_PATHS" ${ARGN}) @@ -31,12 +33,12 @@ function(generate_message_conversion) find_package(Python3 REQUIRED COMPONENTS Interpreter) find_program(_PROTOC protoc REQUIRED DOC "protoc compiler") - get_filename_component(_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + set(_CMAKE_DIR "${CMAKE_CURRENT_FUNCTION_LIST_DIR}") set(_SCRIPT "${_CMAKE_DIR}/gen_message_conversion.py") + set(_SHARED_SCRIPT "${_CMAKE_DIR}/../../ssl_league_msgs/cmake/ateam_proto_shared.py") - if(NOT EXISTS "${_SCRIPT}") - message(FATAL_ERROR "generate_message_conversion: script not found at ${_SCRIPT}") - endif() + ateam_require_script("${_SCRIPT}" "generate_message_conversion") + ateam_require_script("${_SHARED_SCRIPT}" "generate_message_conversion") # Build --proto-paths args set(_path_args) @@ -47,34 +49,44 @@ function(generate_message_conversion) # Build --sidecar arg set(_sidecar_arg) if(_ARG_SIDECAR) - if(NOT EXISTS "${_ARG_SIDECAR}") - message(FATAL_ERROR "generate_message_conversion: SIDECAR not found: ${_ARG_SIDECAR}") - endif() + ateam_require_sidecar("${_ARG_SIDECAR}" "generate_message_conversion") set(_sidecar_arg "--sidecar" "${_ARG_SIDECAR}") endif() file(MAKE_DIRECTORY "${_ARG_OUTPUT_DIR}") - execute_process( - COMMAND - "${Python3_EXECUTABLE}" "${_SCRIPT}" - "--proto-files" ${_ARG_PROTO_FILES} - ${_path_args} - ${_sidecar_arg} - "--output-dir" "${_ARG_OUTPUT_DIR}" - RESULT_VARIABLE _result - OUTPUT_VARIABLE _stdout - ERROR_VARIABLE _stderr + set(_gen_command + "${Python3_EXECUTABLE}" "${_SCRIPT}" + "--proto-files" ${_ARG_PROTO_FILES} + ${_path_args} + ${_sidecar_arg} + "--output-dir" "${_ARG_OUTPUT_DIR}" + "--protoc-path" "${_PROTOC}" ) - if(NOT _result EQUAL 0) - message(FATAL_ERROR - "generate_message_conversion: generator failed (exit ${_result}):\n${_stderr}") + set(_hpp "${_ARG_OUTPUT_DIR}/message_conversion_generated.hpp") + set(_cpp "${_ARG_OUTPUT_DIR}/message_conversion_generated.cpp") + + # Run once now so the generated files exist for configure-time consumers + # (e.g. add_library() argument lists). + ateam_run_generator(COMMAND ${_gen_command} ERROR_PREFIX "generate_message_conversion: generator") + + set(_depends ${_ARG_PROTO_FILES} "${_SCRIPT}" "${_SHARED_SCRIPT}") + if(_ARG_SIDECAR) + list(APPEND _depends "${_ARG_SIDECAR}") endif() - # Re-configure when proto files or sidecar change - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS - ${_ARG_PROTO_FILES} - ${_ARG_SIDECAR} + # Rerun at build time (no reconfigure needed) whenever a proto, the + # generator script, or the sidecar changes. + add_custom_command( + OUTPUT "${_hpp}" "${_cpp}" + COMMAND ${_gen_command} + DEPENDS ${_depends} + COMMENT "Regenerating message conversion code for ${PROJECT_NAME} from proto sources" + VERBATIM ) + + # Full reconfigure still required if the output file set itself would + # need to change (it doesn't here — always exactly these two files). + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_depends}) endfunction() diff --git a/ssl_ros_bridge/cmake/gen_message_conversion.py b/ssl_ros_bridge/cmake/gen_message_conversion.py index 2060c24..dd5ad6d 100644 --- a/ssl_ros_bridge/cmake/gen_message_conversion.py +++ b/ssl_ros_bridge/cmake/gen_message_conversion.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 -""" -gen_message_conversion.py — Generate C++ fromProto bridge functions. +r""" +Generate C++ fromProto bridge functions. Reads SSL league proto files + sidecar JSON, emits two files: /message_conversion_generated.hpp /message_conversion_generated.cpp Usage (from CMake or command line): - python3 gen_message_conversion.py \\ - --proto-files ssl_vision_detection.proto ... \\ - --proto-paths /path/to/protos \\ - --sidecar ssl_ros_annotations.json \\ - --output-dir /path/to/output \\ - [--proto-include-prefix ssl_league_protobufs] \\ - [--ros-package ssl_league_msgs] \\ + python3 gen_message_conversion.py \ + --proto-files ssl_vision_detection.proto ... \ + --proto-paths /path/to/protos \ + --sidecar ssl_ros_annotations.json \ + --output-dir /path/to/output \ + [--proto-include-prefix ssl_league_protobufs] \ + [--ros-package ssl_league_msgs] \ [--cpp-namespace ssl_ros_bridge::message_conversion] Sidecar annotation format is the same as for protoc_gen_ros2msg.py. @@ -28,15 +28,30 @@ """ import argparse -import json import os -import re +from pathlib import Path import subprocess import sys -import tempfile -from pathlib import Path from google.protobuf import descriptor_pb2 +from rosidl_pycommon import convert_camel_case_to_lower_case_underscore + +_LEAGUE_MSGS_CMAKE = Path(__file__).resolve().parent.parent.parent / 'ssl_league_msgs' / 'cmake' +sys.path.insert(0, str(_LEAGUE_MSGS_CMAKE)) +# Must follow the sys.path.insert() above, so this can't sort before the +# rosidl_pycommon import the way import-order linting wants. +from ateam_proto_shared import ( # noqa: E402, I100 + build_map_entry_type_names, + field_shape, + FieldAnnotation, + HAS_FIELD_PREFIX, + iter_messages, + load_sidecar, + MessageSidecarEntry, + OutputEntry, + Sidecar, + sidecar_consumed, +) FD = descriptor_pb2.FieldDescriptorProto @@ -46,212 +61,187 @@ FD.TYPE_SINT32, FD.TYPE_SINT64, FD.TYPE_FIXED32, FD.TYPE_FIXED64, FD.TYPE_SFIXED32, FD.TYPE_SFIXED64, }) -INTRINSIC_ROS_TYPES = frozenset({"builtin_interfaces/Time", "builtin_interfaces/Duration"}) +INTRINSIC_ROS_TYPES = frozenset({'builtin_interfaces/Time', 'builtin_interfaces/Duration'}) # ── Name helpers ────────────────────────────────────────────────────────────── -def to_ros_include_name(name: str) -> str: - """PascalCase/underscore msg name → ROS2 snake_case include stem.""" - s1 = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1_\2', name) - s2 = re.sub(r'([a-z\d])([A-Z])', r'\1_\2', s1) - return s2.lower() - def ros_cpp_type(ros_type: str) -> str: - """'geometry_msgs/Point32' → 'geometry_msgs::msg::Point32'""" - if "/" in ros_type: - pkg, typ = ros_type.split("/", 1) - return f"{pkg}::msg::{typ}" + """'geometry_msgs/Point32' → 'geometry_msgs::msg::Point32'.""" + if '/' in ros_type: + pkg, typ = ros_type.split('/', 1) + return f'{pkg}::msg::{typ}' return ros_type def ros_msg_type(flat: str, pkg: str) -> str: - return f"{pkg}::msg::{flat}" + return f'{pkg}::msg::{flat}' def proto_oneof_const(field_name: str) -> str: - """'aimless_kick' → 'kAimlessKick'""" - return "k" + "".join(p.capitalize() for p in field_name.split("_")) - - -# ── Message iteration (mirrors protoc_gen_ros2msg.py) ──────────────────────── - -def iter_messages(fd): - def _walk(msg): - flat = msg.name.replace("SSL_", "") - if not msg.options.map_entry: - yield flat, msg - for nested in msg.nested_type: - yield from _walk(nested) - for msg in fd.message_type: - yield from _walk(msg) - - -def map_entry_type_names(fd) -> frozenset: - entries = set() - for msg in fd.message_type: - for nested in msg.nested_type: - if nested.options.map_entry: - pkg = f".{fd.package}" if fd.package else "" - entries.add(f"{pkg}.{msg.name}.{nested.name}") - return frozenset(entries) - - -# ── Sidecar helpers ─────────────────────────────────────────────────────────── - -def load_sidecar(path: str | None) -> dict: - if not path: - return {} - with open(path) as f: - return json.load(f) - - -def sidecar_consumed(entry: dict) -> frozenset: - s = set(entry.get("fields", {}).keys()) - for out in entry.get("outputs", []): - for v in out.get("from", {}).values(): - s.add(v) - for sub in ("position", "orientation"): - for v in out.get(sub, {}).get("from", {}).values(): - s.add(v) - return frozenset(s) + """'aimless_kick' → 'kAimlessKick'.""" + return 'k' + ''.join(p.capitalize() for p in field_name.split('_')) # ── C++ expression helpers ──────────────────────────────────────────────────── def scale_lit(val: float) -> str: if abs(val - 1e-3) < 1e-12: - return "1e-3f" - return f"{val}f" + return '1e-3f' + return f'{val}f' def acc(field_name: str) -> str: - return f"proto_msg.{field_name}()" + return f'proto_msg.{field_name}()' # ── Sidecar output codegen ──────────────────────────────────────────────────── -def emit_output(out: dict, ind: str) -> list[str]: - """Emit C++ for one 'outputs' entry.""" +def _emit_component_assignments( + target: str, from_map: dict[str, str], scale: float | None, ind: str, +) -> list[str]: + """ + Emit 'target. = proto_msg.() [* scale];' for each from_map entry. + + from_map maps ros_component_name -> proto_field_name. Shared by every + 'outputs' shape below (Point32/Vector3 are one component group; Pose is + two — position and orientation — each handled by one call). + """ lines = [] - ros_field = out["ros_field"] - ros_type = out["ros_type"] - - if ros_type in ("geometry_msgs/Point32", "geometry_msgs/Vector3"): - scale = out.get("scale") - for ros_comp, pf in out["from"].items(): - expr = acc(pf) - if scale: - expr = f"{expr} * {scale_lit(scale)}" - lines.append(f"{ind}ros_msg.{ros_field}.{ros_comp} = {expr};") - - elif ros_type == "geometry_msgs/Quaternion": - for ros_comp, pf in out["from"].items(): - lines.append(f"{ind}ros_msg.{ros_field}.{ros_comp} = {acc(pf)};") - - elif ros_type == "geometry_msgs/Pose": - pos = out.get("position", {}) - ori = out.get("orientation", {}) - ps = pos.get("scale") - for ros_comp, pf in pos.get("from", {}).items(): - expr = acc(pf) - if ps: - expr = f"{expr} * {scale_lit(ps)}" - lines.append(f"{ind}ros_msg.{ros_field}.position.{ros_comp} = {expr};") - for ros_comp, pf in ori.get("from", {}).items(): - lines.append(f"{ind}ros_msg.{ros_field}.orientation.{ros_comp} = {acc(pf)};") + for ros_comp, pf in from_map.items(): + expr = acc(pf) + if scale: + expr = f'{expr} * {scale_lit(scale)}' - else: - lines.append(f"{ind}// TODO: unsupported output ros_type '{ros_type}' → '{ros_field}'") + lines.append(f'{ind}{target}.{ros_comp} = {expr};') return lines +def emit_output(out: OutputEntry, ind: str) -> list[str]: + """Emit C++ for one 'outputs' entry.""" + ros_field = out['ros_field'] + ros_type = out['ros_type'] + target = f'ros_msg.{ros_field}' + + if ros_type in ('geometry_msgs/Point32', 'geometry_msgs/Vector3'): + return _emit_component_assignments(target, out['from'], out.get('scale'), ind) + + if ros_type == 'geometry_msgs/Quaternion': + return _emit_component_assignments(target, out['from'], None, ind) + + if ros_type == 'geometry_msgs/Pose': + pos = out.get('position', {}) + ori = out.get('orientation', {}) + return ( + _emit_component_assignments( + f'{target}.position', pos.get('from', {}), pos.get('scale'), ind, + ) + + _emit_component_assignments(f'{target}.orientation', ori.get('from', {}), None, ind) + ) + + return [f"{ind}// TODO: unsupported output ros_type '{ros_type}' → '{ros_field}'"] + + # ── Sidecar field override codegen ──────────────────────────────────────────── +# (ros_type, is_float_source_field) -> wrapper around a proto accessor +# expression producing the intrinsic conversion. int/uint sources are in +# proto microseconds (x1000 -> ns); float/double sources are in proto +# seconds (x1e9 -> ns). +_INTRINSIC_WRAPPERS = { + ('builtin_interfaces/Time', True): + lambda e: f'rclcpp::Time(static_cast({e} * 1e9))', + ('builtin_interfaces/Time', False): + lambda e: f'rclcpp::Time(static_cast({e}) * 1000LL)', + ('builtin_interfaces/Duration', True): + lambda e: f'rclcpp::Duration::from_nanoseconds(static_cast({e} * 1e9))', + ('builtin_interfaces/Duration', False): + lambda e: f'rclcpp::Duration::from_nanoseconds(static_cast({e}) * 1000LL)', +} + + def emit_field_override( proto_name: str, - ann: dict, + ann: FieldAnnotation, field: descriptor_pb2.FieldDescriptorProto, proto2: bool, ind: str, ) -> list[str]: lines = [] - ros_name = ann.get("ros_field", proto_name) - ros_type = ann.get("ros_type") - scale = ann.get("scale") + ros_name = ann.get('ros_field', proto_name) + ros_type = ann.get('ros_type') + scale = ann.get('scale') - is_rep = field.label == FD.LABEL_REPEATED - is_p2opt = proto2 and field.label == FD.LABEL_OPTIONAL and not field.HasField("oneof_index") + is_rep, _in_oneof, is_p2opt = field_shape(field, proto2) def wrap_optional(inner: str) -> list[str]: return [ - f"{ind}if (proto_msg.has_{proto_name}()) {{", - f"{ind} ros_msg.{ros_name} = {{{inner}}};", - f"{ind}}}", + f'{ind}if (proto_msg.has_{proto_name}()) {{', + f'{ind} ros_msg.{ros_name} = {{{inner}}};', + f'{ind}}}', ] if ros_type in INTRINSIC_ROS_TYPES: - is_time = ros_type == "builtin_interfaces/Time" - if field.type in FLOAT_TYPES: - if is_time: - expr = f"rclcpp::Time(static_cast({acc(proto_name)} * 1e9))" - else: - expr = f"rclcpp::Duration::from_nanoseconds(static_cast({acc(proto_name)} * 1e9))" - else: - if is_time: - expr = f"rclcpp::Time(static_cast({acc(proto_name)}) * 1000LL)" - else: - expr = f"rclcpp::Duration::from_nanoseconds(static_cast({acc(proto_name)}) * 1000LL)" + expr = _INTRINSIC_WRAPPERS[(ros_type, field.type in FLOAT_TYPES)](acc(proto_name)) if is_p2opt: lines += wrap_optional(expr) elif is_rep: - lines.append(f"{ind}// TODO: repeated Time/Duration not implemented") + lines.append(f'{ind}// TODO: repeated Time/Duration not implemented') else: - lines.append(f"{ind}ros_msg.{ros_name} = {expr};") + lines.append(f'{ind}ros_msg.{ros_name} = {expr};') elif scale is not None: - expr = f"{acc(proto_name)} * {scale_lit(scale)}" + expr = f'{acc(proto_name)} * {scale_lit(scale)}' if is_p2opt: lines += wrap_optional(expr) elif is_rep: - lines.append(f"{ind}std::transform(proto_msg.{proto_name}().begin(), proto_msg.{proto_name}().end(),") - lines.append(f"{ind} std::back_inserter(ros_msg.{ros_name}),") - lines.append(f"{ind} [](const auto & v) {{ return v * {scale_lit(scale)}; }});") + lines.append( + f'{ind}std::transform(proto_msg.{proto_name}().begin(), ' + f'proto_msg.{proto_name}().end(),' + ) + lines.append(f'{ind} std::back_inserter(ros_msg.{ros_name}),') + lines.append(f'{ind} [](const auto & v) {{ return v * {scale_lit(scale)}; }});') else: - lines.append(f"{ind}ros_msg.{ros_name} = {expr};") + lines.append(f'{ind}ros_msg.{ros_name} = {expr};') else: # Rename only — same type, different ros field name if field.type == FD.TYPE_MESSAGE: - inner = f"fromProto({acc(proto_name)})" + inner = f'fromProto({acc(proto_name)})' if is_p2opt: lines += [ - f"{ind}if (proto_msg.has_{proto_name}()) {{", - f"{ind} ros_msg.{ros_name} = {{fromProto(proto_msg.{proto_name}())}};", - f"{ind}}}", + f'{ind}if (proto_msg.has_{proto_name}()) {{', + f'{ind} ros_msg.{ros_name} = {{fromProto(proto_msg.{proto_name}())}};', + f'{ind}}}', ] elif is_rep: - lines.append(f"{ind}std::transform(proto_msg.{proto_name}().begin(), proto_msg.{proto_name}().end(),") - lines.append(f"{ind} std::back_inserter(ros_msg.{ros_name}),") - lines.append(f"{ind} [](const auto & p) {{ return fromProto(p); }});") + lines.append( + f'{ind}std::transform(proto_msg.{proto_name}().begin(), ' + f'proto_msg.{proto_name}().end(),' + ) + lines.append(f'{ind} std::back_inserter(ros_msg.{ros_name}),') + lines.append(f'{ind} [](const auto & p) {{ return fromProto(p); }});') else: - lines.append(f"{ind}ros_msg.{ros_name} = fromProto({acc(proto_name)});") + lines.append(f'{ind}ros_msg.{ros_name} = fromProto({acc(proto_name)});') elif field.type == FD.TYPE_ENUM: - inner = f"static_cast({acc(proto_name)})" + inner = f'static_cast({acc(proto_name)})' if is_p2opt: lines += wrap_optional(inner) else: - lines.append(f"{ind}ros_msg.{ros_name} = {inner};") + lines.append(f'{ind}ros_msg.{ros_name} = {inner};') else: if is_p2opt: lines += wrap_optional(acc(proto_name)) elif is_rep: - lines.append(f"{ind}std::copy(proto_msg.{proto_name}().begin(), proto_msg.{proto_name}().end(),") - lines.append(f"{ind} std::back_inserter(ros_msg.{ros_name}));") + lines.append( + f'{ind}std::copy(proto_msg.{proto_name}().begin(), ' + f'proto_msg.{proto_name}().end(),' + ) + lines.append(f'{ind} std::back_inserter(ros_msg.{ros_name}));') else: - lines.append(f"{ind}ros_msg.{ros_name} = {acc(proto_name)};") + lines.append(f'{ind}ros_msg.{ros_name} = {acc(proto_name)};') return lines @@ -265,79 +255,85 @@ def emit_passthrough( ) -> list[str]: lines = [] name = field.name - is_rep = field.label == FD.LABEL_REPEATED - is_p2opt = proto2 and field.label == FD.LABEL_OPTIONAL and not field.HasField("oneof_index") + is_rep, _in_oneof, is_p2opt = field_shape(field, proto2) if field.type == FD.TYPE_BYTES: if is_p2opt: lines += [ - f"{ind}if (proto_msg.has_{name}()) {{", - f"{ind} auto & _b = {acc(name)};", - f"{ind} ros_msg.{name} = {{std::vector(_b.begin(), _b.end())}};", - f"{ind}}}", + f'{ind}if (proto_msg.has_{name}()) {{', + f'{ind} auto & _b = {acc(name)};', + f'{ind} ros_msg.{name} = {{std::vector(_b.begin(), _b.end())}};', + f'{ind}}}', ] elif is_rep: - lines.append(f"{ind}// TODO: repeated bytes passthrough") + lines.append(f'{ind}// TODO: repeated bytes passthrough') else: lines += [ - f"{ind}{{", - f"{ind} auto & _b = {acc(name)};", - f"{ind} ros_msg.{name}.assign(_b.begin(), _b.end());", - f"{ind}}}", + f'{ind}{{', + f'{ind} auto & _b = {acc(name)};', + f'{ind} ros_msg.{name}.assign(_b.begin(), _b.end());', + f'{ind}}}', ] + return lines if is_rep: if field.type == FD.TYPE_MESSAGE: - lines.append(f"{ind}std::transform(proto_msg.{name}().begin(), proto_msg.{name}().end(),") - lines.append(f"{ind} std::back_inserter(ros_msg.{name}),") - lines.append(f"{ind} [](const auto & p) {{ return fromProto(p); }});") + lines.append( + f'{ind}std::transform(proto_msg.{name}().begin(), proto_msg.{name}().end(),' + ) + lines.append(f'{ind} std::back_inserter(ros_msg.{name}),') + lines.append(f'{ind} [](const auto & p) {{ return fromProto(p); }});') elif field.type == FD.TYPE_ENUM: - lines.append(f"{ind}std::transform(proto_msg.{name}().begin(), proto_msg.{name}().end(),") - lines.append(f"{ind} std::back_inserter(ros_msg.{name}),") - lines.append(f"{ind} [](const auto & v) {{ return static_cast(v); }});") + lines.append( + f'{ind}std::transform(proto_msg.{name}().begin(), proto_msg.{name}().end(),' + ) + lines.append(f'{ind} std::back_inserter(ros_msg.{name}),') + lines.append(f'{ind} [](const auto & v) {{ return static_cast(v); }});') else: - lines.append(f"{ind}std::copy(proto_msg.{name}().begin(), proto_msg.{name}().end(),") - lines.append(f"{ind} std::back_inserter(ros_msg.{name}));") + lines.append(f'{ind}std::copy(proto_msg.{name}().begin(), proto_msg.{name}().end(),') + lines.append(f'{ind} std::back_inserter(ros_msg.{name}));') + return lines if is_p2opt: if field.type == FD.TYPE_MESSAGE: lines += [ - f"{ind}if (proto_msg.has_{name}()) {{", - f"{ind} ros_msg.{name} = {{fromProto(proto_msg.{name}())}};", - f"{ind}}}", + f'{ind}if (proto_msg.has_{name}()) {{', + f'{ind} ros_msg.{name} = {{fromProto(proto_msg.{name}())}};', + f'{ind}}}', ] elif field.type == FD.TYPE_ENUM: lines += [ - f"{ind}if (proto_msg.has_{name}()) {{", - f"{ind} ros_msg.{name} = {{static_cast(proto_msg.{name}())}};", - f"{ind}}}", + f'{ind}if (proto_msg.has_{name}()) {{', + f'{ind} ros_msg.{name} = {{static_cast(proto_msg.{name}())}};', + f'{ind}}}', ] else: lines += [ - f"{ind}if (proto_msg.has_{name}()) {{", - f"{ind} ros_msg.{name} = {{proto_msg.{name}()}};", - f"{ind}}}", + f'{ind}if (proto_msg.has_{name}()) {{', + f'{ind} ros_msg.{name} = {{proto_msg.{name}()}};', + f'{ind}}}', ] + return lines # Singular non-optional (proto2 required or proto3 default) if field.type == FD.TYPE_MESSAGE: if proto2: - lines.append(f"{ind}ros_msg.{name} = fromProto(proto_msg.{name}());") + lines.append(f'{ind}ros_msg.{name} = fromProto(proto_msg.{name}());') else: # proto3 singular message — emit has_ sentinel lines += [ - f"{ind}if (proto_msg.has_{name}()) {{", - f"{ind} ros_msg.has_{name} = true;", - f"{ind} ros_msg.{name} = fromProto(proto_msg.{name}());", - f"{ind}}}", + f'{ind}if (proto_msg.has_{name}()) {{', + f'{ind} ros_msg.{HAS_FIELD_PREFIX}{name} = true;', + f'{ind} ros_msg.{name} = fromProto(proto_msg.{name}());', + f'{ind}}}', ] elif field.type == FD.TYPE_ENUM: - lines.append(f"{ind}ros_msg.{name} = static_cast({acc(name)});") + lines.append(f'{ind}ros_msg.{name} = static_cast({acc(name)});') else: - lines.append(f"{ind}ros_msg.{name} = {acc(name)};") + lines.append(f'{ind}ros_msg.{name} = {acc(name)};') return lines @@ -345,29 +341,35 @@ def emit_passthrough( def emit_oneof( oi: int, msg: descriptor_pb2.DescriptorProto, - consumed: frozenset, + cpp_name: str, + consumed: frozenset[str], ind: str, ) -> list[str]: lines = [] oneof_name = msg.oneof_decl[oi].name arms = [f for f in msg.field - if f.HasField("oneof_index") and f.oneof_index == oi + if f.HasField('oneof_index') and f.oneof_index == oi and f.name not in consumed] - lines.append(f"{ind}ros_msg.{oneof_name}_case = static_cast(proto_msg.{oneof_name}_case());") - lines.append(f"{ind}switch (proto_msg.{oneof_name}_case()) {{") + lines.append( + f'{ind}ros_msg.{oneof_name}_case = static_cast(proto_msg.{oneof_name}_case());' + ) + lines.append(f'{ind}switch (proto_msg.{oneof_name}_case()) {{') + for f in arms: - const = f"{msg.name}::{proto_oneof_const(f.name)}" - lines.append(f"{ind} case {const}:") + const = f'{cpp_name}::{proto_oneof_const(f.name)}' + lines.append(f'{ind} case {const}:') if f.type == FD.TYPE_MESSAGE: - lines.append(f"{ind} ros_msg.{f.name} = fromProto(proto_msg.{f.name}());") + lines.append(f'{ind} ros_msg.{f.name} = fromProto(proto_msg.{f.name}());') elif f.type == FD.TYPE_ENUM: - lines.append(f"{ind} ros_msg.{f.name} = static_cast({acc(f.name)});") + lines.append(f'{ind} ros_msg.{f.name} = static_cast({acc(f.name)});') else: - lines.append(f"{ind} ros_msg.{f.name} = {acc(f.name)};") - lines.append(f"{ind} break;") - lines.append(f"{ind} default: break;") - lines.append(f"{ind}}}") + lines.append(f'{ind} ros_msg.{f.name} = {acc(f.name)};') + lines.append(f'{ind} break;') + + lines.append(f'{ind} default: break;') + lines.append(f'{ind}}}') + return lines @@ -397,194 +399,235 @@ def emit_oneof( def generate_header( - fds: list, + fds: list[descriptor_pb2.FileDescriptorProto], + sidecar: Sidecar, ros_pkg: str, proto_prefix: str, namespace: str, ) -> str: - guard = "CORE__MESSAGE_CONVERSION_GENERATED_HPP_" + skip_types = frozenset(sidecar.get('_skip_types', [])) + guard = 'CORE__MESSAGE_CONVERSION_GENERATED_HPP_' lines = [ LICENSE, - "// AUTO-GENERATED — do not edit. Re-run gen_message_conversion.py.", - f"#ifndef {guard}", - f"#define {guard}", - "", + '// AUTO-GENERATED — do not edit. Re-run gen_message_conversion.py.', + f'#ifndef {guard}', + f'#define {guard}', + '', ] # Proto pb.h includes for fd in fds: - pb_h = fd.name.replace(".proto", ".pb.h") - lines.append(f"#include <{proto_prefix}/{pb_h}>") - lines.append("") + pb_h = fd.name.replace('.proto', '.pb.h') + lines.append(f'#include <{proto_prefix}/{pb_h}>') + + lines.append('') # ROS msg includes seen_inc = set() for fd in fds: - for flat, msg in iter_messages(fd): - inc = f"{ros_pkg}/msg/{to_ros_include_name(flat)}.hpp" + for flat, _cpp_name, msg in iter_messages(fd): + if flat in skip_types: + continue + + inc = f'{ros_pkg}/msg/{convert_camel_case_to_lower_case_underscore(flat)}.hpp' if inc not in seen_inc: seen_inc.add(inc) - lines.append(f"#include <{inc}>") - lines.append("") + lines.append(f'#include <{inc}>') + + lines.append('') # Common includes lines += [ - "#include ", - "#include ", - "#include ", - "#include ", - "#include ", - "#include ", - "#include ", - "", + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '', ] - for ns in namespace.split("::"): - lines.append(f"namespace {ns}") - lines.append("{") - lines.append("") + for ns in namespace.split('::'): + lines.append(f'namespace {ns}') + lines.append('{') + + lines.append('') for fd in fds: - for flat, msg in iter_messages(fd): + for flat, cpp_name, msg in iter_messages(fd): + if flat in skip_types: + continue ros_t = ros_msg_type(flat, ros_pkg) - lines.append(f"{ros_t} fromProto(const {msg.name} & proto_msg);") + lines.append(f'{ros_t} fromProto(const {cpp_name} & proto_msg);') + + lines.append('') + for ns in reversed(namespace.split('::')): + lines.append(f'}} // namespace {ns}') - lines.append("") - for ns in reversed(namespace.split("::")): - lines.append(f"}} // namespace {ns}") - lines.append("") - lines.append(f"#endif // {guard}") - return "\n".join(lines) + "\n" + lines.append('') + lines.append(f'#endif // {guard}') + + return '\n'.join(lines) + '\n' def generate_source( - fds: list, - sidecar: dict, + fds: list[descriptor_pb2.FileDescriptorProto], + all_map_entries: frozenset[str], + sidecar: Sidecar, ros_pkg: str, namespace: str, ) -> str: lines = [ LICENSE, - "// AUTO-GENERATED — do not edit. Re-run gen_message_conversion.py.", - "", + '// AUTO-GENERATED — do not edit. Re-run gen_message_conversion.py.', + '', '#include "message_conversion_generated.hpp"', - "#include ", - "#include ", - "", + '#include ', + '#include ', + '', ] - for ns in namespace.split("::"): - lines.append(f"namespace {ns}") - lines.append("{") - lines.append("") + for ns in namespace.split('::'): + lines.append(f'namespace {ns}') + lines.append('{') + lines.append('') + + skip_types = frozenset(sidecar.get('_skip_types', [])) for fd in fds: - proto2 = fd.syntax != "proto3" - map_entries = map_entry_type_names(fd) + proto2 = fd.syntax != 'proto3' - for flat, msg in iter_messages(fd): - entry = sidecar.get(flat, {}) + for flat, cpp_name, msg in iter_messages(fd): + if flat in skip_types: + continue + + entry: MessageSidecarEntry = sidecar.get(flat, {}) consumed = sidecar_consumed(entry) - pf_map = {f.name: f for f in msg.field} + pf_map: dict[str, descriptor_pb2.FieldDescriptorProto] = {f.name: f for f in msg.field} ros_t = ros_msg_type(flat, ros_pkg) - lines.append(f"{ros_t} fromProto(const {msg.name} & proto_msg)") - lines.append("{") - lines.append(f" {ros_t} ros_msg;") + lines.append(f'{ros_t} fromProto(const {cpp_name} & proto_msg)') + lines.append('{') + lines.append(f' {ros_t} ros_msg;') # Sidecar outputs (multi-field → ROS struct) - for out in entry.get("outputs", []): - lines += emit_output(out, " ") + for out in entry.get('outputs', []): + lines += emit_output(out, ' ') # Sidecar field overrides - for pname, ann in entry.get("fields", {}).items(): + for pname, ann in entry.get('fields', {}).items(): + if ann.get('skip'): + continue + pf = pf_map.get(pname) if pf: - lines += emit_field_override(pname, ann, pf, proto2, " ") + lines += emit_field_override(pname, ann, pf, proto2, ' ') - # Passthrough — skip consumed, skip map entries - emitted_oneofs: set = set() + # Passthrough — skip consumed, error on unskipped map entries + emitted_oneofs: set[int] = set() for field in msg.field: if field.name in consumed: continue - if field.type == FD.TYPE_MESSAGE and field.type_name in map_entries: - continue - if field.HasField("oneof_index"): + + if field.type == FD.TYPE_MESSAGE and field.type_name in all_map_entries: + print( + f'{flat}.{field.name}: proto map fields have no ROS2 ' + f"equivalent and are not supported. Add a 'skip' field " + f'annotation in the sidecar for this field to drop it explicitly.', + file=sys.stderr, + ) + sys.exit(1) + + if field.HasField('oneof_index'): oi = field.oneof_index if oi not in emitted_oneofs: emitted_oneofs.add(oi) - lines += emit_oneof(oi, msg, consumed, " ") + lines += emit_oneof(oi, msg, cpp_name, consumed, ' ') continue - lines += emit_passthrough(field, proto2, " ") - lines.append(" return ros_msg;") - lines.append("}") - lines.append("") + lines += emit_passthrough(field, proto2, ' ') + + lines.append(' return ros_msg;') + lines.append('}') + lines.append('') - for ns in reversed(namespace.split("::")): - lines.append(f"}} // namespace {ns}") + for ns in reversed(namespace.split('::')): + lines.append(f'}} // namespace {ns}') - return "\n".join(lines) + "\n" + return '\n'.join(lines) + '\n' # ── Main ────────────────────────────────────────────────────────────────────── -def main(): +def main() -> None: ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("--proto-files", nargs="+", required=True) - ap.add_argument("--proto-paths", nargs="+", default=[]) - ap.add_argument("--sidecar", default=None) - ap.add_argument("--output-dir", required=True) - ap.add_argument("--proto-include-prefix", default="ssl_league_protobufs") - ap.add_argument("--ros-package", default="ssl_league_msgs") - ap.add_argument("--cpp-namespace", default="ssl_ros_bridge::message_conversion") + ap.add_argument('--proto-files', nargs='+', required=True) + ap.add_argument('--proto-paths', nargs='+', default=[]) + ap.add_argument('--sidecar', default=None) + ap.add_argument('--output-dir', required=True) + ap.add_argument('--proto-include-prefix', default='ssl_league_protobufs') + ap.add_argument('--ros-package', default='ssl_league_msgs') + ap.add_argument('--cpp-namespace', default='ssl_ros_bridge::message_conversion') + ap.add_argument( + '--protoc-path', default='protoc', + help="Path to the protoc binary to invoke (default: 'protoc' on PATH). Pass " + 'the same protoc CMake located, so this generator and the .msg generator ' + 'parse the proto set with the same protoc build/version.', + ) + args = ap.parse_args() - # Produce a FileDescriptorSet via protoc --descriptor_set_out - desc_fd, desc_path = tempfile.mkstemp(suffix=".pb") - os.close(desc_fd) + # Produce a FileDescriptorSet via protoc --descriptor_set_out, written straight to + # stdout — avoids a temp-file write/read/unlink round trip for a throwaway artifact. try: - proto_path_args = [f"--proto_path={p}" for p in args.proto_paths] + proto_path_args = [f'--proto_path={p}' for p in args.proto_paths] r = subprocess.run( - ["protoc", f"--descriptor_set_out={desc_path}", "--include_imports"] + [args.protoc_path, '--descriptor_set_out=/dev/stdout', '--include_imports'] + proto_path_args + args.proto_files, - capture_output=True, text=True, + capture_output=True, ) - if r.returncode != 0: - print(f"protoc failed:\n{r.stderr}", file=sys.stderr) - sys.exit(1) + except FileNotFoundError as e: + print(f'Could not run protoc ({args.protoc_path!r}): {e}', file=sys.stderr) + sys.exit(1) + + if r.returncode != 0: + print(f"protoc failed:\n{r.stderr.decode(errors='replace')}", file=sys.stderr) + sys.exit(1) - fds_pb = descriptor_pb2.FileDescriptorSet() - with open(desc_path, "rb") as f: - fds_pb.ParseFromString(f.read()) - finally: - os.unlink(desc_path) + fds_pb = descriptor_pb2.FileDescriptorSet() + fds_pb.ParseFromString(r.stdout) # Match requested files against descriptor requested = {Path(p).name for p in args.proto_files} target_fds = [fd for fd in fds_pb.file if Path(fd.name).name in requested] if not target_fds: - print("No matching proto files found in descriptor.", file=sys.stderr) + print('No matching proto files found in descriptor.', file=sys.stderr) sys.exit(1) sidecar = load_sidecar(args.sidecar) + all_map_entries = build_map_entry_type_names(fds_pb.file) os.makedirs(args.output_dir, exist_ok=True) - hpp = generate_header(target_fds, args.ros_package, args.proto_include_prefix, args.cpp_namespace) - cpp = generate_source(target_fds, sidecar, args.ros_package, args.cpp_namespace) + hpp = generate_header( + target_fds, sidecar, args.ros_package, args.proto_include_prefix, args.cpp_namespace, + ) + cpp = generate_source( + target_fds, all_map_entries, sidecar, args.ros_package, args.cpp_namespace, + ) - hpp_path = os.path.join(args.output_dir, "message_conversion_generated.hpp") - cpp_path = os.path.join(args.output_dir, "message_conversion_generated.cpp") + hpp_path = os.path.join(args.output_dir, 'message_conversion_generated.hpp') + cpp_path = os.path.join(args.output_dir, 'message_conversion_generated.cpp') - with open(hpp_path, "w") as f: + with open(hpp_path, 'w') as f: f.write(hpp) - with open(cpp_path, "w") as f: + with open(cpp_path, 'w') as f: f.write(cpp) - print(f"Generated {hpp_path}") - print(f"Generated {cpp_path}") + print(f'Generated {hpp_path}') + print(f'Generated {cpp_path}') -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/ssl_ros_bridge/src/core/CMakeLists.txt b/ssl_ros_bridge/src/core/CMakeLists.txt index 30abd3f..b5a400a 100644 --- a/ssl_ros_bridge/src/core/CMakeLists.txt +++ b/ssl_ros_bridge/src/core/CMakeLists.txt @@ -1,6 +1,5 @@ add_library(${PROJECT_NAME}_core SHARED get_ip_addresses.cpp - message_conversion.cpp multicast_receiver.cpp ${_CONV_OUT}/message_conversion_generated.cpp ) diff --git a/ssl_ros_bridge/src/core/message_conversion.cpp b/ssl_ros_bridge/src/core/message_conversion.cpp deleted file mode 100644 index 319f718..0000000 --- a/ssl_ros_bridge/src/core/message_conversion.cpp +++ /dev/null @@ -1,728 +0,0 @@ -// Copyright 2025 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. - -#include "message_conversion.hpp" -#include -#include -#include -#include - -#define CopyOptional(proto_msg, ros_msg, var_name) \ - if (proto_msg.has_ ## var_name() ) { \ - ros_msg.var_name = {proto_msg.var_name()}; \ - } - -#define CopyOptionalStruct(proto_msg, ros_msg, var_name) \ - if (proto_msg.has_ ## var_name() ) { \ - ros_msg.var_name = {fromProto(proto_msg.var_name())}; \ - } - -#define CopyOptionalEnum(proto_msg, ros_msg, var_name) \ - if (proto_msg.has_ ## var_name() ) { \ - ros_msg.var_name = { \ - static_cast(proto_msg.var_name()) \ - }; \ - } - -constexpr float mmTom = 1.0e-3f; -constexpr int secToNanosec = 1e9; - -namespace ssl_ros_bridge::message_conversion -{ - -geometry_msgs::msg::Point32 fromProto(const Vector2 & proto_msg) -{ - geometry_msgs::msg::Point32 ros_msg; - ros_msg.x = proto_msg.x(); - ros_msg.y = proto_msg.y(); - return ros_msg; -} -geometry_msgs::msg::Point32 fromProto(const Vector3 & proto_msg) -{ - geometry_msgs::msg::Point32 ros_msg; - ros_msg.x = proto_msg.x(); - ros_msg.y = proto_msg.y(); - ros_msg.z = proto_msg.z(); - return ros_msg; -} - -ssl_league_msgs::msg::Referee fromProto(const Referee & proto_msg) -{ - ssl_league_msgs::msg::Referee ros_msg; - CopyOptional(proto_msg, ros_msg, source_identifier); - CopyOptionalEnum(proto_msg, ros_msg, match_type); - ros_msg.timestamp = rclcpp::Time(proto_msg.packet_timestamp() * 1000); - ros_msg.stage = proto_msg.stage(); - CopyOptional(proto_msg, ros_msg, stage_time_left); - ros_msg.command = proto_msg.command(); - ros_msg.command_counter = proto_msg.command_counter(); - ros_msg.command_timestamp = rclcpp::Time(proto_msg.command_timestamp() * 1000); - ros_msg.yellow = fromProto(proto_msg.yellow()); - ros_msg.blue = fromProto(proto_msg.blue()); - if(proto_msg.has_designated_position()) { - geometry_msgs::msg::Point32 p; - p.x = proto_msg.designated_position().x() / 1e3; - p.y = proto_msg.designated_position().y() / 1e3; - ros_msg.designated_position = {p}; - } - CopyOptional(proto_msg, ros_msg, blue_team_on_positive_half); - CopyOptionalEnum(proto_msg, ros_msg, next_command); - std::transform( - proto_msg.game_events().begin(), - proto_msg.game_events().end(), - std::back_inserter(ros_msg.game_events), - [](const auto & p) {return fromProto(p);}); - std::transform( - proto_msg.game_event_proposals().begin(), - proto_msg.game_event_proposals().end(), - std::back_inserter(ros_msg.game_event_proposals), - [](const auto & p) {return fromProto(p);}); - CopyOptional(proto_msg, ros_msg, current_action_time_remaining); - CopyOptional(proto_msg, ros_msg, status_message); - return ros_msg; -} - -ssl_league_msgs::msg::TeamInfo fromProto(const Referee::TeamInfo & proto_msg) -{ - ssl_league_msgs::msg::TeamInfo ros_msg; - ros_msg.name = proto_msg.name(); - ros_msg.score = proto_msg.score(); - ros_msg.red_cards = proto_msg.red_cards(); - std::copy( - proto_msg.yellow_card_times().begin(), - proto_msg.yellow_card_times().end(), std::back_inserter(ros_msg.yellow_card_times)); - ros_msg.yellow_cards = proto_msg.yellow_cards(); - ros_msg.timeouts = proto_msg.timeouts(); - ros_msg.timeout_time = proto_msg.timeout_time(); - ros_msg.goalkeeper = proto_msg.goalkeeper(); - CopyOptional(proto_msg, ros_msg, foul_counter); - CopyOptional(proto_msg, ros_msg, ball_placement_failures); - CopyOptional(proto_msg, ros_msg, can_place_ball); - CopyOptional(proto_msg, ros_msg, max_allowed_bots); - CopyOptional(proto_msg, ros_msg, bot_substitution_intent); - CopyOptional(proto_msg, ros_msg, ball_placement_failures_reached); - CopyOptional(proto_msg, ros_msg, bot_substitution_allowed); - CopyOptional(proto_msg, ros_msg, bot_substitutions_left); - CopyOptional(proto_msg, ros_msg, bot_substitution_time_left); - return ros_msg; -} - -ssl_league_msgs::msg::GameEvent fromProto(const GameEvent & proto_msg) -{ - ssl_league_msgs::msg::GameEvent ros_msg; - ros_msg.id = proto_msg.id(); - ros_msg.type = proto_msg.type(); - std::copy( - proto_msg.origin().begin(), proto_msg.origin().end(), - std::back_inserter(ros_msg.origin)); - ros_msg.created_timestamp = proto_msg.created_timestamp(); - CopyOptionalStruct(proto_msg, ros_msg, ball_left_field_touch_line); - CopyOptionalStruct(proto_msg, ros_msg, ball_left_field_goal_line); - CopyOptionalStruct(proto_msg, ros_msg, aimless_kick); - CopyOptionalStruct(proto_msg, ros_msg, attacker_too_close_to_defense_area); - CopyOptionalStruct(proto_msg, ros_msg, defender_in_defense_area); - CopyOptionalStruct(proto_msg, ros_msg, boundary_crossing); - CopyOptionalStruct(proto_msg, ros_msg, keeper_held_ball); - CopyOptionalStruct(proto_msg, ros_msg, bot_dribbled_ball_too_far); - CopyOptionalStruct(proto_msg, ros_msg, bot_pushed_bot); - CopyOptionalStruct(proto_msg, ros_msg, bot_held_ball_deliberately); - CopyOptionalStruct(proto_msg, ros_msg, bot_tipped_over); - CopyOptionalStruct(proto_msg, ros_msg, bot_dropped_parts); - CopyOptionalStruct(proto_msg, ros_msg, attacker_touched_ball_in_defense_area); - CopyOptionalStruct(proto_msg, ros_msg, bot_kicked_ball_too_fast); - CopyOptionalStruct(proto_msg, ros_msg, bot_crash_unique); - CopyOptionalStruct(proto_msg, ros_msg, bot_crash_drawn); - CopyOptionalStruct(proto_msg, ros_msg, defender_too_close_to_kick_point); - CopyOptionalStruct(proto_msg, ros_msg, bot_too_fast_in_stop); - CopyOptionalStruct(proto_msg, ros_msg, bot_interfered_placement); - CopyOptionalStruct(proto_msg, ros_msg, possible_goal); - CopyOptionalStruct(proto_msg, ros_msg, goal); - CopyOptionalStruct(proto_msg, ros_msg, invalid_goal); - CopyOptionalStruct(proto_msg, ros_msg, attacker_double_touched_ball); - CopyOptionalStruct(proto_msg, ros_msg, placement_succeeded); - CopyOptionalStruct(proto_msg, ros_msg, penalty_kick_failed); - CopyOptionalStruct(proto_msg, ros_msg, no_progress_in_game); - CopyOptionalStruct(proto_msg, ros_msg, placement_failed); - CopyOptionalStruct(proto_msg, ros_msg, multiple_cards); - CopyOptionalStruct(proto_msg, ros_msg, multiple_fouls); - CopyOptionalStruct(proto_msg, ros_msg, bot_substitution); - CopyOptionalStruct(proto_msg, ros_msg, excessive_bot_substitution); - CopyOptionalStruct(proto_msg, ros_msg, too_many_robots); - CopyOptionalStruct(proto_msg, ros_msg, challenge_flag); - CopyOptionalStruct(proto_msg, ros_msg, challenge_flag_handled); - CopyOptionalStruct(proto_msg, ros_msg, emergency_stop); - CopyOptionalStruct(proto_msg, ros_msg, unsporting_behavior_minor); - CopyOptionalStruct(proto_msg, ros_msg, unsporting_behavior_major); - return ros_msg; -} - -ssl_league_msgs::msg::GameEventProposalGroup fromProto(const GameEventProposalGroup & proto_msg) -{ - ssl_league_msgs::msg::GameEventProposalGroup ros_msg; - CopyOptional(proto_msg, ros_msg, id); - std::transform( - proto_msg.game_events().begin(), - proto_msg.game_events().end(), - std::back_inserter(ros_msg.game_events), - [](const auto & p) {return fromProto(p);}); - ros_msg.accepted = proto_msg.accepted(); - return ros_msg; -} - -ssl_league_msgs::msg::Division fromProto(const Division & proto_msg) -{ - ssl_league_msgs::msg::Division ros_msg; - ros_msg.division = proto_msg; - return ros_msg; -} - -ssl_league_msgs::msg::RobotId fromProto(const RobotId & proto_msg) -{ - ssl_league_msgs::msg::RobotId ros_msg; - CopyOptionalStruct(proto_msg, ros_msg, team); - CopyOptional(proto_msg, ros_msg, id); - return ros_msg; -} - -ssl_league_msgs::msg::Team fromProto(const Team & proto_msg) -{ - ssl_league_msgs::msg::Team ros_msg; - ros_msg.color = proto_msg; - return ros_msg; -} - -ssl_league_msgs::msg::AimlessKick fromProto(const GameEvent_AimlessKick & proto_msg) -{ - ssl_league_msgs::msg::AimlessKick ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptionalStruct(proto_msg, ros_msg, kick_location); - return ros_msg; -} -ssl_league_msgs::msg::AttackerDoubleTouchedBall fromProto( - const GameEvent_AttackerDoubleTouchedBall & proto_msg) -{ - ssl_league_msgs::msg::AttackerDoubleTouchedBall ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - return ros_msg; -} -ssl_league_msgs::msg::AttackerTooCloseToDefenseArea fromProto( - const GameEvent_AttackerTooCloseToDefenseArea & proto_msg) -{ - ssl_league_msgs::msg::AttackerTooCloseToDefenseArea ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, distance); - CopyOptionalStruct(proto_msg, ros_msg, ball_location); - return ros_msg; -} -ssl_league_msgs::msg::AttackerTouchedBallInDefenseArea fromProto( - const GameEvent_AttackerTouchedBallInDefenseArea & proto_msg) -{ - ssl_league_msgs::msg::AttackerTouchedBallInDefenseArea ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, distance); - return ros_msg; -} -ssl_league_msgs::msg::AttackerTouchedOpponentInDefenseArea fromProto( - const GameEvent_AttackerTouchedOpponentInDefenseArea & proto_msg) -{ - ssl_league_msgs::msg::AttackerTouchedOpponentInDefenseArea ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptional(proto_msg, ros_msg, victim); - CopyOptionalStruct(proto_msg, ros_msg, location); - return ros_msg; -} -ssl_league_msgs::msg::BallLeftField fromProto(const GameEvent_BallLeftField & proto_msg) -{ - ssl_league_msgs::msg::BallLeftField ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - return ros_msg; -} -ssl_league_msgs::msg::BotCrashDrawn fromProto(const GameEvent_BotCrashDrawn & proto_msg) -{ - ssl_league_msgs::msg::BotCrashDrawn ros_msg; - CopyOptional(proto_msg, ros_msg, bot_yellow); - CopyOptional(proto_msg, ros_msg, bot_blue); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, crash_speed); - CopyOptional(proto_msg, ros_msg, speed_diff); - CopyOptional(proto_msg, ros_msg, crash_angle); - return ros_msg; -} -ssl_league_msgs::msg::BotCrashUnique fromProto(const GameEvent_BotCrashUnique & proto_msg) -{ - ssl_league_msgs::msg::BotCrashUnique ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, violator); - CopyOptional(proto_msg, ros_msg, victim); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, crash_speed); - CopyOptional(proto_msg, ros_msg, speed_diff); - CopyOptional(proto_msg, ros_msg, crash_angle); - return ros_msg; -} -ssl_league_msgs::msg::BotDribbledBallTooFar fromProto( - const GameEvent_BotDribbledBallTooFar & proto_msg) -{ - ssl_league_msgs::msg::BotDribbledBallTooFar ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, start); - CopyOptionalStruct(proto_msg, ros_msg, end); - return ros_msg; -} -ssl_league_msgs::msg::BotHeldBallDeliberately fromProto( - const GameEvent_BotHeldBallDeliberately & proto_msg) -{ - ssl_league_msgs::msg::BotHeldBallDeliberately ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, duration); - return ros_msg; -} -ssl_league_msgs::msg::BotInterferedPlacement fromProto( - const GameEvent_BotInterferedPlacement & proto_msg) -{ - ssl_league_msgs::msg::BotInterferedPlacement ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - return ros_msg; -} -ssl_league_msgs::msg::BotKickedBallTooFast fromProto( - const GameEvent_BotKickedBallTooFast & proto_msg) -{ - ssl_league_msgs::msg::BotKickedBallTooFast ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, initial_ball_speed); - CopyOptional(proto_msg, ros_msg, chipped); - return ros_msg; -} -ssl_league_msgs::msg::BotPushedBot fromProto(const GameEvent_BotPushedBot & proto_msg) -{ - ssl_league_msgs::msg::BotPushedBot ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, violator); - CopyOptional(proto_msg, ros_msg, victim); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, pushed_distance); - return ros_msg; -} -ssl_league_msgs::msg::BotSubstitution fromProto(const GameEvent_BotSubstitution & proto_msg) -{ - ssl_league_msgs::msg::BotSubstitution ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - return ros_msg; -} -ssl_league_msgs::msg::BotTippedOver fromProto(const GameEvent_BotTippedOver & proto_msg) -{ - ssl_league_msgs::msg::BotTippedOver ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptionalStruct(proto_msg, ros_msg, ball_location); - return ros_msg; -} -ssl_league_msgs::msg::BotTooFastInStop fromProto(const GameEvent_BotTooFastInStop & proto_msg) -{ - ssl_league_msgs::msg::BotTooFastInStop ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, speed); - return ros_msg; -} -ssl_league_msgs::msg::BoundaryCrossing fromProto(const GameEvent_BoundaryCrossing & proto_msg) -{ - ssl_league_msgs::msg::BoundaryCrossing ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptionalStruct(proto_msg, ros_msg, location); - return ros_msg; -} -ssl_league_msgs::msg::ChallengeFlag fromProto(const GameEvent_ChallengeFlag & proto_msg) -{ - ssl_league_msgs::msg::ChallengeFlag ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - return ros_msg; -} -ssl_league_msgs::msg::ChippedGoal fromProto(const GameEvent_ChippedGoal & proto_msg) -{ - ssl_league_msgs::msg::ChippedGoal ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptionalStruct(proto_msg, ros_msg, kick_location); - CopyOptional(proto_msg, ros_msg, max_ball_height); - return ros_msg; -} -ssl_league_msgs::msg::DefenderInDefenseArea fromProto( - const GameEvent_DefenderInDefenseArea & proto_msg) -{ - ssl_league_msgs::msg::DefenderInDefenseArea ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, distance); - return ros_msg; -} -ssl_league_msgs::msg::DefenderInDefenseAreaPartially fromProto( - const GameEvent_DefenderInDefenseAreaPartially & proto_msg) -{ - ssl_league_msgs::msg::DefenderInDefenseAreaPartially ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, distance); - CopyOptionalStruct(proto_msg, ros_msg, ball_location); - return ros_msg; -} -ssl_league_msgs::msg::DefenderTooCloseToKickPoint fromProto( - const GameEvent_DefenderTooCloseToKickPoint & proto_msg) -{ - ssl_league_msgs::msg::DefenderTooCloseToKickPoint ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, distance); - return ros_msg; -} -ssl_league_msgs::msg::EmergencyStop fromProto(const GameEvent_EmergencyStop & proto_msg) -{ - ssl_league_msgs::msg::EmergencyStop ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - return ros_msg; -} -ssl_league_msgs::msg::Goal fromProto(const GameEvent_Goal & proto_msg) -{ - ssl_league_msgs::msg::Goal ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptionalStruct(proto_msg, ros_msg, kicking_team); - CopyOptional(proto_msg, ros_msg, kicking_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptionalStruct(proto_msg, ros_msg, kick_location); - CopyOptional(proto_msg, ros_msg, max_ball_height); - CopyOptional(proto_msg, ros_msg, num_robots_by_team); - CopyOptional(proto_msg, ros_msg, last_touch_by_team); - CopyOptional(proto_msg, ros_msg, message); - return ros_msg; -} -ssl_league_msgs::msg::IndirectGoal fromProto(const GameEvent_IndirectGoal & proto_msg) -{ - ssl_league_msgs::msg::IndirectGoal ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptionalStruct(proto_msg, ros_msg, kick_location); - return ros_msg; -} -ssl_league_msgs::msg::KeeperHeldBall fromProto(const GameEvent_KeeperHeldBall & proto_msg) -{ - ssl_league_msgs::msg::KeeperHeldBall ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, duration); - return ros_msg; -} -ssl_league_msgs::msg::KickTimeout fromProto(const GameEvent_KickTimeout & proto_msg) -{ - ssl_league_msgs::msg::KickTimeout ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, time); - return ros_msg; -} -ssl_league_msgs::msg::MultipleCards fromProto(const GameEvent_MultipleCards & proto_msg) -{ - ssl_league_msgs::msg::MultipleCards ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - return ros_msg; -} -ssl_league_msgs::msg::MultipleFouls fromProto(const GameEvent_MultipleFouls & proto_msg) -{ - ssl_league_msgs::msg::MultipleFouls ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - return ros_msg; -} -ssl_league_msgs::msg::MultiplePlacementFailures fromProto( - const GameEvent_MultiplePlacementFailures & proto_msg) -{ - ssl_league_msgs::msg::MultiplePlacementFailures ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - return ros_msg; -} -ssl_league_msgs::msg::NoProgressInGame fromProto(const GameEvent_NoProgressInGame & proto_msg) -{ - ssl_league_msgs::msg::NoProgressInGame ros_msg; - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptional(proto_msg, ros_msg, time); - return ros_msg; -} -ssl_league_msgs::msg::PenaltyKickFailed fromProto(const GameEvent_PenaltyKickFailed & proto_msg) -{ - ssl_league_msgs::msg::PenaltyKickFailed ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptionalStruct(proto_msg, ros_msg, location); - return ros_msg; -} -ssl_league_msgs::msg::PlacementFailed fromProto(const GameEvent_PlacementFailed & proto_msg) -{ - ssl_league_msgs::msg::PlacementFailed ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, remaining_distance); - return ros_msg; -} -ssl_league_msgs::msg::PlacementSucceeded fromProto(const GameEvent_PlacementSucceeded & proto_msg) -{ - ssl_league_msgs::msg::PlacementSucceeded ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, time_taken); - CopyOptional(proto_msg, ros_msg, precision); - CopyOptional(proto_msg, ros_msg, distance); - return ros_msg; -} -ssl_league_msgs::msg::Prepared fromProto(const GameEvent_Prepared & proto_msg) -{ - ssl_league_msgs::msg::Prepared ros_msg; - CopyOptional(proto_msg, ros_msg, time_taken); - return ros_msg; -} -ssl_league_msgs::msg::TooManyRobots fromProto(const GameEvent_TooManyRobots & proto_msg) -{ - ssl_league_msgs::msg::TooManyRobots ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, num_robots_allowed); - CopyOptional(proto_msg, ros_msg, num_robots_on_field); - CopyOptionalStruct(proto_msg, ros_msg, ball_location); - return ros_msg; -} -ssl_league_msgs::msg::UnsportingBehaviorMajor fromProto( - const GameEvent_UnsportingBehaviorMajor & proto_msg) -{ - ssl_league_msgs::msg::UnsportingBehaviorMajor ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - ros_msg.reason = proto_msg.reason(); - return ros_msg; -} -ssl_league_msgs::msg::UnsportingBehaviorMinor fromProto( - const GameEvent_UnsportingBehaviorMinor & proto_msg) -{ - ssl_league_msgs::msg::UnsportingBehaviorMinor ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - ros_msg.reason = proto_msg.reason(); - return ros_msg; -} -ssl_league_msgs::msg::BotDroppedParts fromProto(const GameEvent_BotDroppedParts & proto_msg) -{ - ssl_league_msgs::msg::BotDroppedParts ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - CopyOptional(proto_msg, ros_msg, by_bot); - CopyOptionalStruct(proto_msg, ros_msg, location); - CopyOptionalStruct(proto_msg, ros_msg, ball_location); - return ros_msg; -} -ssl_league_msgs::msg::ChallengeFlagHandled fromProto( - const GameEvent_ChallengeFlagHandled & proto_msg) -{ - ssl_league_msgs::msg::ChallengeFlagHandled ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - ros_msg.accepted = proto_msg.accepted(); - return ros_msg; -} -ssl_league_msgs::msg::ExcessiveBotSubstitution fromProto( - const GameEvent_ExcessiveBotSubstitution & proto_msg) -{ - ssl_league_msgs::msg::ExcessiveBotSubstitution ros_msg; - ros_msg.by_team = fromProto(proto_msg.by_team()); - return ros_msg; -} -ssl_league_msgs::msg::VisionDetectionBall fromProto(const SSL_DetectionBall & proto_msg) -{ - ssl_league_msgs::msg::VisionDetectionBall ros_msg; - ros_msg.confidence = proto_msg.confidence(); - ros_msg.area = proto_msg.area(); // assuming this is in pixels, not verified - ros_msg.pos.x = proto_msg.x() * mmTom; - ros_msg.pos.y = proto_msg.y() * mmTom; - ros_msg.pos.z = proto_msg.z() * mmTom; - ros_msg.pixel.x = proto_msg.pixel_x(); - ros_msg.pixel.y = proto_msg.pixel_y(); - - return ros_msg; -} -ssl_league_msgs::msg::VisionDetectionRobot fromProto(const SSL_DetectionRobot & proto_msg) -{ - ssl_league_msgs::msg::VisionDetectionRobot ros_msg; - ros_msg.confidence = proto_msg.confidence(); - ros_msg.robot_id = proto_msg.robot_id(); - ros_msg.pose.position.x = proto_msg.x() * mmTom; - ros_msg.pose.position.y = proto_msg.y() * mmTom; - ros_msg.pose.position.z = 0; - ros_msg.pose.orientation = - tf2::toMsg(tf2::Quaternion(tf2::Vector3(0, 0, 1), proto_msg.orientation())); - ros_msg.pixel.x = proto_msg.pixel_x(); - ros_msg.pixel.y = proto_msg.pixel_y(); - ros_msg.height = proto_msg.height(); - - return ros_msg; -} -ssl_league_msgs::msg::VisionDetectionFrame fromProto(const SSL_DetectionFrame & proto_msg) -{ - ssl_league_msgs::msg::VisionDetectionFrame ros_msg; - ros_msg.frame_number = proto_msg.frame_number(); - ros_msg.t_capture = rclcpp::Time(static_cast(proto_msg.t_capture() * secToNanosec)); - ros_msg.t_sent = rclcpp::Time(static_cast(proto_msg.t_sent() * secToNanosec)); - ros_msg.t_capture_camera = - rclcpp::Time(static_cast(proto_msg.t_capture_camera() * secToNanosec)); - ros_msg.camera_id = proto_msg.camera_id(); - std::transform( - proto_msg.balls().begin(), - proto_msg.balls().end(), - std::back_inserter(ros_msg.balls), - [](const auto & p) {return fromProto(p);}); - std::transform( - proto_msg.robots_yellow().begin(), - proto_msg.robots_yellow().end(), - std::back_inserter(ros_msg.robots_yellow), - [](const auto & p) {return fromProto(p);}); - std::transform( - proto_msg.robots_blue().begin(), - proto_msg.robots_blue().end(), - std::back_inserter(ros_msg.robots_blue), - [](const auto & p) {return fromProto(p);}); - - return ros_msg; -} - -ssl_league_msgs::msg::VisionFieldLineSegment fromProto(const SSL_FieldLineSegment & proto_msg) -{ - ssl_league_msgs::msg::VisionFieldLineSegment ros_msg; - ros_msg.name = proto_msg.name(); - ros_msg.p1.x = proto_msg.p1().x() * mmTom; - ros_msg.p1.y = proto_msg.p1().y() * mmTom; - ros_msg.p2.x = proto_msg.p2().x() * mmTom; - ros_msg.p2.y = proto_msg.p2().y() * mmTom; - ros_msg.thickness = proto_msg.thickness() * mmTom; - - return ros_msg; -} -ssl_league_msgs::msg::VisionFieldCircularArc fromProto(const SSL_FieldCircularArc & proto_msg) -{ - ssl_league_msgs::msg::VisionFieldCircularArc ros_msg; - ros_msg.name = proto_msg.name(); - ros_msg.center.x = proto_msg.center().x() * mmTom; - ros_msg.center.y = proto_msg.center().y() * mmTom; - ros_msg.radius = proto_msg.radius() * mmTom; - ros_msg.a1 = proto_msg.a1(); - ros_msg.a2 = proto_msg.a2(); - ros_msg.thickness = proto_msg.thickness() * mmTom; - - return ros_msg; -} -ssl_league_msgs::msg::VisionGeometryFieldSize fromProto(const SSL_GeometryFieldSize & proto_msg) -{ - ssl_league_msgs::msg::VisionGeometryFieldSize ros_msg; - ros_msg.field_length = proto_msg.field_length() * mmTom; - ros_msg.field_width = proto_msg.field_width() * mmTom; - ros_msg.goal_width = proto_msg.goal_width() * mmTom; - ros_msg.goal_depth = proto_msg.goal_depth() * mmTom; - ros_msg.boundary_width = proto_msg.boundary_width() * mmTom; - std::transform( - proto_msg.field_lines().begin(), - proto_msg.field_lines().end(), - std::back_inserter(ros_msg.field_lines), - [](const auto & p) {return fromProto(p);}); - std::transform( - proto_msg.field_arcs().begin(), - proto_msg.field_arcs().end(), - std::back_inserter(ros_msg.field_arcs), - [](const auto & p) {return fromProto(p);}); - - ros_msg.penalty_area_depth = proto_msg.penalty_area_depth() * mmTom; - ros_msg.penalty_area_width = proto_msg.penalty_area_width() * mmTom; - ros_msg.center_circle_radius = proto_msg.center_circle_radius() * mmTom; - ros_msg.line_thickness = proto_msg.line_thickness() * mmTom; - ros_msg.goal_center_to_penalty_mark = proto_msg.goal_center_to_penalty_mark() * mmTom; - ros_msg.goal_height = proto_msg.goal_height() * mmTom; - ros_msg.ball_radius = proto_msg.ball_radius() * mmTom; - ros_msg.max_robot_radius = proto_msg.max_robot_radius() * mmTom; - - return ros_msg; -} -ssl_league_msgs::msg::VisionGeometryCameraCalibration fromProto( - const SSL_GeometryCameraCalibration & proto_msg) -{ - ssl_league_msgs::msg::VisionGeometryCameraCalibration ros_msg; - ros_msg.camera_id = proto_msg.camera_id(); - ros_msg.focal_length = proto_msg.focal_length(); - ros_msg.principal_point.x = proto_msg.principal_point_x(); - ros_msg.principal_point.y = proto_msg.principal_point_y(); - ros_msg.distortion = proto_msg.distortion(); - ros_msg.pose.orientation.x = proto_msg.q0(); - ros_msg.pose.orientation.y = proto_msg.q1(); - ros_msg.pose.orientation.z = proto_msg.q2(); - ros_msg.pose.orientation.w = proto_msg.q3(); - ros_msg.pose.position.x = proto_msg.tx() * mmTom; - ros_msg.pose.position.y = proto_msg.ty() * mmTom; - ros_msg.pose.position.z = proto_msg.tz() * mmTom; - ros_msg.derived_camera_world_t.x = proto_msg.derived_camera_world_tx() * mmTom; - ros_msg.derived_camera_world_t.y = proto_msg.derived_camera_world_ty() * mmTom; - ros_msg.derived_camera_world_t.z = proto_msg.derived_camera_world_tz() * mmTom; - - return ros_msg; -} -ssl_league_msgs::msg::VisionGeometryData fromProto(const SSL_GeometryData & proto_msg) -{ - ssl_league_msgs::msg::VisionGeometryData ros_msg; - ros_msg.field = fromProto(proto_msg.field()); - std::transform( - proto_msg.calib().begin(), - proto_msg.calib().end(), - std::back_inserter(ros_msg.calibration), - [](const auto & p) {return fromProto(p);}); - - return ros_msg; -} - -ssl_league_msgs::msg::VisionWrapper fromProto(const SSL_WrapperPacket & proto_msg) -{ - ssl_league_msgs::msg::VisionWrapper ros_msg; - if (proto_msg.has_detection()) { - ros_msg.detection.push_back(fromProto(proto_msg.detection())); - } - if (proto_msg.has_geometry()) { - ros_msg.geometry.push_back(fromProto(proto_msg.geometry())); - } - - return ros_msg; -} - -} // namespace ssl_ros_bridge::message_conversion diff --git a/ssl_ros_bridge/src/core/message_conversion.hpp b/ssl_ros_bridge/src/core/message_conversion.hpp deleted file mode 100644 index 629a846..0000000 --- a/ssl_ros_bridge/src/core/message_conversion.hpp +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2025 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 CORE__MESSAGE_CONVERSION_HPP_ -#define CORE__MESSAGE_CONVERSION_HPP_ - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace ssl_ros_bridge::message_conversion -{ - -geometry_msgs::msg::Point32 fromProto(const Vector2 & proto_msg); -geometry_msgs::msg::Point32 fromProto(const Vector3 & proto_msg); - -ssl_league_msgs::msg::Referee fromProto(const Referee & proto_msg); -ssl_league_msgs::msg::TeamInfo fromProto(const Referee::TeamInfo & proto_msg); -ssl_league_msgs::msg::GameEvent fromProto(const GameEvent & proto_msg); -ssl_league_msgs::msg::GameEventProposalGroup fromProto(const GameEventProposalGroup & proto_msg); - -ssl_league_msgs::msg::Division fromProto(const Division & proto_msg); -ssl_league_msgs::msg::RobotId fromProto(const RobotId & proto_msg); -ssl_league_msgs::msg::Team fromProto(const Team & proto_msg); - -ssl_league_msgs::msg::AimlessKick fromProto(const GameEvent_AimlessKick & proto_msg); -ssl_league_msgs::msg::AttackerDoubleTouchedBall fromProto( - const GameEvent_AttackerDoubleTouchedBall & proto_msg); -ssl_league_msgs::msg::AttackerTooCloseToDefenseArea fromProto( - const GameEvent_AttackerTooCloseToDefenseArea & proto_msg); -ssl_league_msgs::msg::AttackerTouchedBallInDefenseArea fromProto( - const GameEvent_AttackerTouchedBallInDefenseArea & proto_msg); -ssl_league_msgs::msg::AttackerTouchedOpponentInDefenseArea fromProto( - const GameEvent_AttackerTouchedOpponentInDefenseArea & proto_msg); -ssl_league_msgs::msg::BallLeftField fromProto(const GameEvent_BallLeftField & proto_msg); -ssl_league_msgs::msg::BotCrashDrawn fromProto(const GameEvent_BotCrashDrawn & proto_msg); -ssl_league_msgs::msg::BotCrashUnique fromProto(const GameEvent_BotCrashUnique & proto_msg); -ssl_league_msgs::msg::BotDribbledBallTooFar fromProto( - const GameEvent_BotDribbledBallTooFar & proto_msg); -ssl_league_msgs::msg::BotHeldBallDeliberately fromProto( - const GameEvent_BotHeldBallDeliberately & proto_msg); -ssl_league_msgs::msg::BotInterferedPlacement fromProto( - const GameEvent_BotInterferedPlacement & proto_msg); -ssl_league_msgs::msg::BotKickedBallTooFast fromProto( - const GameEvent_BotKickedBallTooFast & proto_msg); -ssl_league_msgs::msg::BotPushedBot fromProto(const GameEvent_BotPushedBot & proto_msg); -ssl_league_msgs::msg::BotSubstitution fromProto(const GameEvent_BotSubstitution & proto_msg); -ssl_league_msgs::msg::BotTippedOver fromProto(const GameEvent_BotTippedOver & proto_msg); -ssl_league_msgs::msg::BotTooFastInStop fromProto(const GameEvent_BotTooFastInStop & proto_msg); -ssl_league_msgs::msg::BoundaryCrossing fromProto(const GameEvent_BoundaryCrossing & proto_msg); -ssl_league_msgs::msg::ChallengeFlag fromProto(const GameEvent_ChallengeFlag & proto_msg); -ssl_league_msgs::msg::ChippedGoal fromProto(const GameEvent_ChippedGoal & proto_msg); -ssl_league_msgs::msg::DefenderInDefenseArea fromProto( - const GameEvent_DefenderInDefenseArea & proto_msg); -ssl_league_msgs::msg::DefenderInDefenseAreaPartially fromProto( - const GameEvent_DefenderInDefenseAreaPartially & proto_msg); -ssl_league_msgs::msg::DefenderTooCloseToKickPoint fromProto( - const GameEvent_DefenderTooCloseToKickPoint & proto_msg); -ssl_league_msgs::msg::EmergencyStop fromProto(const GameEvent_EmergencyStop & proto_msg); -ssl_league_msgs::msg::Goal fromProto(const GameEvent_Goal & proto_msg); -ssl_league_msgs::msg::IndirectGoal fromProto(const GameEvent_IndirectGoal & proto_msg); -ssl_league_msgs::msg::KeeperHeldBall fromProto(const GameEvent_KeeperHeldBall & proto_msg); -ssl_league_msgs::msg::KickTimeout fromProto(const GameEvent_KickTimeout & proto_msg); -ssl_league_msgs::msg::MultipleCards fromProto(const GameEvent_MultipleCards & proto_msg); -ssl_league_msgs::msg::MultipleFouls fromProto(const GameEvent_MultipleFouls & proto_msg); -ssl_league_msgs::msg::MultiplePlacementFailures fromProto( - const GameEvent_MultiplePlacementFailures & proto_msg); -ssl_league_msgs::msg::NoProgressInGame fromProto(const GameEvent_NoProgressInGame & proto_msg); -ssl_league_msgs::msg::PenaltyKickFailed fromProto(const GameEvent_PenaltyKickFailed & proto_msg); -ssl_league_msgs::msg::PlacementFailed fromProto(const GameEvent_PlacementFailed & proto_msg); -ssl_league_msgs::msg::PlacementSucceeded fromProto(const GameEvent_PlacementSucceeded & proto_msg); -ssl_league_msgs::msg::Prepared fromProto(const GameEvent_Prepared & proto_msg); -ssl_league_msgs::msg::TooManyRobots fromProto(const GameEvent_TooManyRobots & proto_msg); -ssl_league_msgs::msg::UnsportingBehaviorMajor fromProto( - const GameEvent_UnsportingBehaviorMajor & proto_msg); -ssl_league_msgs::msg::UnsportingBehaviorMinor fromProto( - const GameEvent_UnsportingBehaviorMinor & proto_msg); -ssl_league_msgs::msg::BotDroppedParts fromProto(const GameEvent_BotDroppedParts & proto_msg); -ssl_league_msgs::msg::ChallengeFlagHandled fromProto( - const GameEvent_ChallengeFlagHandled & proto_msg); -ssl_league_msgs::msg::ExcessiveBotSubstitution fromProto( - const GameEvent_ExcessiveBotSubstitution & proto_msg); - -ssl_league_msgs::msg::VisionDetectionBall fromProto(const SSL_DetectionBall & proto_msg); -ssl_league_msgs::msg::VisionDetectionRobot fromProto(const SSL_DetectionRobot & proto_msg); -ssl_league_msgs::msg::VisionDetectionFrame fromProto(const SSL_DetectionFrame & proto_msg); - -ssl_league_msgs::msg::VisionFieldLineSegment fromProto(const SSL_FieldLineSegment & proto_msg); -ssl_league_msgs::msg::VisionFieldCircularArc fromProto(const SSL_FieldCircularArc & proto_msg); -ssl_league_msgs::msg::VisionGeometryFieldSize fromProto(const SSL_GeometryFieldSize & proto_msg); -ssl_league_msgs::msg::VisionGeometryCameraCalibration fromProto( - const SSL_GeometryCameraCalibration & proto_msg); -ssl_league_msgs::msg::VisionGeometryData fromProto(const SSL_GeometryData & proto_msg); - -ssl_league_msgs::msg::VisionWrapper fromProto(const SSL_WrapperPacket & proto_msg); - -} // namespace ssl_ros_bridge::message_conversion - -#endif // CORE__MESSAGE_CONVERSION_HPP_ diff --git a/ssl_ros_bridge/src/game_controller_bridge/gc_multicast_bridge_node.cpp b/ssl_ros_bridge/src/game_controller_bridge/gc_multicast_bridge_node.cpp index b9336c1..e6a74cb 100644 --- a/ssl_ros_bridge/src/game_controller_bridge/gc_multicast_bridge_node.cpp +++ b/ssl_ros_bridge/src/game_controller_bridge/gc_multicast_bridge_node.cpp @@ -22,7 +22,7 @@ #include #include #include -#include "core/message_conversion.hpp" +#include "message_conversion_generated.hpp" #include "core/multicast_receiver.hpp" #include "core/protobuf_logging.hpp" #include diff --git a/ssl_ros_bridge/src/log2bag/log2bag.cpp b/ssl_ros_bridge/src/log2bag/log2bag.cpp index 17e65a8..1465324 100644 --- a/ssl_ros_bridge/src/log2bag/log2bag.cpp +++ b/ssl_ros_bridge/src/log2bag/log2bag.cpp @@ -21,10 +21,10 @@ #include #include #include -#include +#include #include #include -#include "core/message_conversion.hpp" +#include "message_conversion_generated.hpp" #include "log_reader.hpp" template @@ -95,14 +95,14 @@ int main(int argc, char ** argv) writer->open(log_path.stem().string()); rclcpp::Serialization referee_serialization; - rclcpp::Serialization vision_serialization; + rclcpp::Serialization vision_serialization; while(const auto entry = reader.GetNextMessage()) { RenderProgressBar(stream.tellg(), log_file_size); WriteMessageIfExists(*entry, "/referee_messages", "ssl_league_msgs/msg/Referee", *writer, referee_serialization); - WriteMessageIfExists(*entry, - "/vision_messages", "ssl_league_msgs/msg/VisionWrapper", *writer, vision_serialization); + WriteMessageIfExists(*entry, + "/vision_messages", "ssl_league_msgs/msg/WrapperPacket", *writer, vision_serialization); } std::cout << "\n\n"; diff --git a/ssl_ros_bridge/src/vision_bridge/ssl_vision_bridge_node.cpp b/ssl_ros_bridge/src/vision_bridge/ssl_vision_bridge_node.cpp index 58c0b7d..bd8c3a7 100644 --- a/ssl_ros_bridge/src/vision_bridge/ssl_vision_bridge_node.cpp +++ b/ssl_ros_bridge/src/vision_bridge/ssl_vision_bridge_node.cpp @@ -26,10 +26,10 @@ #include #include -#include "core/message_conversion.hpp" +#include "message_conversion_generated.hpp" #include "core/multicast_receiver.hpp" #include "core/protobuf_logging.hpp" -#include +#include namespace ssl_ros_bridge::vision_bridge { @@ -39,7 +39,7 @@ class SSLVisionBridgeNode : public rclcpp::Node public: explicit SSLVisionBridgeNode(const rclcpp::NodeOptions & options) : rclcpp::Node("ssl_vision_bridge", options), - vision_publisher_(create_publisher("~/vision_messages", + vision_publisher_(create_publisher("~/vision_messages", rclcpp::SystemDefaultsQoS())), multicast_receiver_( declare_parameter("ssl_vision_ip", "224.5.23.2"), @@ -55,7 +55,7 @@ class SSLVisionBridgeNode : public rclcpp::Node } private: - rclcpp::Publisher::SharedPtr vision_publisher_; + rclcpp::Publisher::SharedPtr vision_publisher_; core::MulticastReceiver multicast_receiver_; void multicastCallback(uint8_t * buffer, size_t bytes_received) From 8bf1316a9c6d6eb9c4622aca227db363118d4964 Mon Sep 17 00:00:00 2001 From: Will Stuckey Date: Sat, 15 Aug 2026 13:29:44 -0400 Subject: [PATCH 5/7] updated to submdoule, updated docs, pathing fixes on submodule --- .gitmodules | 3 + ARCHITECTURE.md | 249 ++++++++ README.md | 12 +- ssl_league_msgs/CMakeLists.txt | 28 +- ssl_league_msgs/cmake/ateam_proto_shared.py | 52 +- ssl_league_msgs/cmake/protoc_gen_ros2msg.py | 132 ++-- ssl_league_msgs/package.xml | 15 +- ssl_league_msgs/test/conftest.py | 6 + .../test/test_ateam_proto_shared.py | 105 +++ .../test/test_protoc_gen_ros2msg.py | 99 +++ ssl_league_protobufs/CMakeLists.txt | 104 ++- ssl_league_protobufs/proto/ssl_gc_api.proto | 56 -- .../proto/ssl_gc_change.proto | 200 ------ ssl_league_protobufs/proto/ssl_gc_ci.proto | 26 - .../proto/ssl_gc_common.proto | 28 - .../proto/ssl_gc_engine.proto | 150 ----- .../proto/ssl_gc_engine_config.proto | 55 -- .../proto/ssl_gc_game_event.proto | 596 ------------------ .../proto/ssl_gc_geometry.proto | 16 - ssl_league_protobufs/proto/ssl_gc_rcon.proto | 38 -- .../proto/ssl_gc_rcon_autoref.proto | 32 - .../proto/ssl_gc_rcon_remotecontrol.proto | 117 ---- .../proto/ssl_gc_rcon_team.proto | 56 -- .../proto/ssl_gc_referee_message.proto | 238 ------- ssl_league_protobufs/proto/ssl_gc_state.proto | 133 ---- .../proto/ssl_simulation_config.proto | 77 --- .../proto/ssl_simulation_control.proto | 90 --- .../proto/ssl_simulation_error.proto | 10 - .../proto/ssl_simulation_robot_control.proto | 66 -- .../proto/ssl_simulation_robot_feedback.proto | 23 - .../proto/ssl_simulation_synchronous.proto | 25 - .../proto/ssl_vision_detection.proto | 57 -- .../proto/ssl_vision_detection_tracked.proto | 88 --- .../proto/ssl_vision_geometry.proto | 151 ----- .../proto/ssl_vision_wrapper.proto | 9 - .../proto/ssl_vision_wrapper_tracked.proto | 14 - ssl_league_protobufs/ssl-protocol-defs | 1 + ssl_ros_bridge/CMakeLists.txt | 38 +- .../cmake/gen_message_conversion.py | 109 ++-- ssl_ros_bridge/package.xml | 14 +- ssl_ros_bridge/src/log2bag/log_reader.hpp | 4 +- .../src/team_client/team_client.hpp | 2 +- .../vision_bridge/ssl_vision_bridge_node.cpp | 2 +- 43 files changed, 766 insertions(+), 2560 deletions(-) create mode 100644 .gitmodules create mode 100644 ARCHITECTURE.md create mode 100644 ssl_league_msgs/test/conftest.py create mode 100644 ssl_league_msgs/test/test_ateam_proto_shared.py create mode 100644 ssl_league_msgs/test/test_protoc_gen_ros2msg.py delete mode 100644 ssl_league_protobufs/proto/ssl_gc_api.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_change.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_ci.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_common.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_engine.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_engine_config.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_game_event.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_geometry.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_rcon.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_rcon_autoref.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_rcon_remotecontrol.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_rcon_team.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_referee_message.proto delete mode 100644 ssl_league_protobufs/proto/ssl_gc_state.proto delete mode 100644 ssl_league_protobufs/proto/ssl_simulation_config.proto delete mode 100644 ssl_league_protobufs/proto/ssl_simulation_control.proto delete mode 100644 ssl_league_protobufs/proto/ssl_simulation_error.proto delete mode 100644 ssl_league_protobufs/proto/ssl_simulation_robot_control.proto delete mode 100644 ssl_league_protobufs/proto/ssl_simulation_robot_feedback.proto delete mode 100644 ssl_league_protobufs/proto/ssl_simulation_synchronous.proto delete mode 100644 ssl_league_protobufs/proto/ssl_vision_detection.proto delete mode 100644 ssl_league_protobufs/proto/ssl_vision_detection_tracked.proto delete mode 100644 ssl_league_protobufs/proto/ssl_vision_geometry.proto delete mode 100644 ssl_league_protobufs/proto/ssl_vision_wrapper.proto delete mode 100644 ssl_league_protobufs/proto/ssl_vision_wrapper_tracked.proto create mode 160000 ssl_league_protobufs/ssl-protocol-defs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8c0de94 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ssl_league_protobufs/ssl-protocol-defs"] + path = ssl_league_protobufs/ssl-protocol-defs + url = https://github.com/RoboCup-SSL/ssl-protocol-defs.git diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..073bd1a --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,249 @@ +# Architecture + +This document describes how `ssl_ros_bridge` is built and how it runs. It is +aimed at contributors working on the packages themselves, not at teams +consuming them — for that, see [README.md](README.md). + +## Overview + +The repository solves two related but distinct problems: + +1. **Translating the SSL league's protobuf definitions into ROS types.** + `ssl_league_protobufs` and `ssl_league_msgs` do this at build time, via + code generation. Nothing here runs at runtime. +2. **Bridging live league network traffic into a running ROS system.** + `ssl_ros_bridge` does this at runtime, via a small set of nodes that + listen for multicast/TCP traffic and republish it as ROS topics and + services, using the types and conversion functions produced by (1). + +`ssl_ros_bridge_msgs` sits outside both of these: it is a small, +hand-written package of ROS messages/services for `ssl_ros_bridge`'s own +`team_client` node, which has no protobuf equivalent to generate from. + +## Package dependency graph + +```mermaid +flowchart TD + proto["ssl-protocol-defs\n(git submodule)"] + protobufs["ssl_league_protobufs\nC++ protobuf library"] + leaguemsgs["ssl_league_msgs\nROS .msg definitions"] + bridgemsgs["ssl_ros_bridge_msgs\nhand-written .msg/.srv"] + bridge["ssl_ros_bridge\nruntime nodes + log2bag"] + + proto --> protobufs + proto --> leaguemsgs + protobufs --> bridge + leaguemsgs --> bridge + bridgemsgs --> bridge +``` + +`ssl_league_msgs` and `ssl_league_protobufs` both read the same submodule +independently — one compiles the `.proto` files into C++ protobuf classes, +the other generates ROS `.msg` files from them. `ssl_ros_bridge` depends on +both, plus its own generator that produces the glue between them. + +## Code generation pipeline + +This is the part of the codebase most contributors will actually touch. Two +independent generators consume the same proto set and the same annotation +file, but produce different output for different consumers. + +```mermaid +flowchart LR + subgraph submodule["ssl-protocol-defs submodule"] + protos["*.proto files\n(gc/, vision/, simulation/)"] + end + + annotations["ssl_ros_annotations.json\n(sidecar/annotation file)"] + + subgraph msgs["ssl_league_msgs"] + plugin["protoc_gen_ros2msg.py\n(protoc plugin)"] + msgfiles[".msg files"] + rosidl["rosidl_generate_interfaces()"] + rostypes["ssl_league_msgs::msg::*\nC++/Python ROS types"] + end + + subgraph bridgegen["ssl_ros_bridge"] + convgen["gen_message_conversion.py\n(direct protoc invocation)"] + convsrc["message_conversion_generated\n.hpp / .cpp"] + fromproto["fromProto() functions"] + end + + shared["ateam_proto_shared.py\n(shared naming/shape/annotation logic)"] + + protos --> plugin + protos --> convgen + annotations --> plugin + annotations --> convgen + shared --- plugin + shared --- convgen + plugin --> msgfiles --> rosidl --> rostypes + convgen --> convsrc --> fromproto + rostypes -.type target.-> fromproto +``` + +Both generators independently invoke `protoc` — `protoc_gen_ros2msg.py` runs +*as* a protoc plugin (protoc calls it, feeding it a `CodeGeneratorRequest` +over stdin), while `gen_message_conversion.py` invokes `protoc` itself as a +subprocess to get a `FileDescriptorSet`, then walks that with Python's +`google.protobuf.descriptor_pb2` API. Both approaches end up working from +the same protobuf descriptor model; they just get there differently because +one has to conform to the protoc plugin ABI and the other doesn't. + +`ateam_proto_shared.py` exists so the two generators can't drift on shared +concerns: flattening a proto type name into a ROS-legal one, classifying a +field's shape (repeated / oneof member / proto2-optional), and parsing the +annotation file's `fields`/`outputs`/`_skip_types` structure. + +### The annotation file + +`ssl_ros_annotations.json` (referred to as "the annotation file" or +"sidecar" in code comments) tells both generators to deviate from their +default field-by-field translation for specific messages. It exists because +proto has constructs with no direct ROS equivalent: + +- `google.protobuf.Any` fields have no fixed schema — unsupported by + default; the annotation file must explicitly `"skip": true` them. +- `map` fields have no ROS message equivalent — same treatment. +- Self-referential message types (directly or through other messages) + cannot exist in ROS's fixed-layout structs — `find_type_cycles()` in + `protoc_gen_ros2msg.py` detects these up front and fails the build with + the concrete reference chain, unless a `skip` annotation breaks the cycle. +- Several proto fields collapsing into one ROS field (e.g. `x`/`y`/`z` into + a `geometry_msgs/Point32`) via an `outputs` entry. +- Renaming a field, rescaling a value, or mapping a proto field to + `builtin_interfaces/Time`/`Duration`. + +The annotation format itself is documented in `protoc_gen_ros2msg.py`'s +module docstring — that's the canonical reference, not this file. + +### Type translation rules worth knowing + +- Proto's `optional` (proto2) and `repeated` are both modeled as ROS arrays + — proto2-optional as a 0-or-1-element array, `repeated` as an N-element + array. This is why the README calls out that optional and array fields + look identical in the generated `.msg` files. +- Proto3 has no `optional`/presence tracking on message-type fields by + default; a non-oneof message-type field gets a `bool has_` + sentinel emitted alongside it (`optional_submsg=has_field`, the default + plugin option — `error` is available to force the schema author to + address it explicitly instead). +- Enums become `uint8` constants — ROS has no native enum type. A field of + enum type is likewise `uint8`. +- A `oneof` becomes a `uint8 _case` discriminant, `uint8` constants + for each arm (named after the arm's field number, stable across + reordering), and all arms' fields present as regular fields. +- ROS2 message type names must match `^[A-Z][A-Za-z0-9]*$` — no + underscores. Nested proto messages (`Foo.Bar`) are flattened to + concatenated names (`FooBar`), unlike protobuf's own generated C++ class + names, which join nested names with `_` (`Foo_Bar`). + +### CMake orchestration + +```mermaid +flowchart TD + common["AteamProtoGenCommon.cmake\n(shared: run generator, fail loudly)"] + r2m["Ros2MsgGen.cmake\ngenerate_ros2_msgs()"] + mcg["MsgConversionGen.cmake\ngenerate_message_conversion()"] + chk["CheckGeneratedMsgList.cmake\n(staleness check, cmake -P script)"] + + common --> r2m + common --> mcg + r2m --> chk +``` + +`generate_ros2_msgs()` runs `protoc_gen_ros2msg.py` **twice**, for a +specific reason: `rosidl_generate_interfaces()` needs the full `.msg` file +*list* at CMake configure time (it's a static argument, not something it +can discover later), so the plugin runs once at configure time purely to +learn that list. It's then wired into a build-time `add_custom_command` too, +so that `ninja`/`make` alone regenerates *content* when a `.proto` file or +the plugin script changes — no reconfigure needed for content-only changes. +If the message *list* itself changes (a message added, removed, or +renamed), that does still need a reconfigure; `CheckGeneratedMsgList.cmake` +compares the current generation against a snapshot taken at configure time +and fails the build loudly, telling you to reconfigure, rather than silently +building against a stale file list. + +`generate_message_conversion()` (in `ssl_ros_bridge`) doesn't have this +two-phase requirement — nothing downstream needs its output file list known +statically — so it's a plain build-time `add_custom_command`. + +### `ssl_league_protobufs`: the odd one out + +Unlike the other two, `ssl_league_protobufs` doesn't run one of our Python +generators — it compiles the submodule's `.proto` files straight to C++ via +`protoc --cpp_out`, invoked directly from CMake rather than through the +CMake-bundled `protobuf_generate_cpp()` macro. That macro computes each +generated file's output path relative to `CMAKE_CURRENT_SOURCE_DIR`, but the +submodule's proto files live one directory deeper and cross-import each +other relative to *their own* root — the two roots disagree, and +compilation fails on the cross-includes. Invoking `protoc` directly with one +consistent `--proto_path` avoids the mismatch. See the comments in +`ssl_league_protobufs/CMakeLists.txt` for the full detail; this is a real +CMake/protobuf limitation, not a design choice. + +## Runtime architecture + +```mermaid +flowchart LR + subgraph net["SSL network"] + visionmc["Vision multicast\n(ssl_vision_wrapper)"] + gcmc["Referee multicast\n(ssl_gc_referee_message)"] + gctcp["Game Controller\nTCP (team client protocol)"] + end + + subgraph nodes["ssl_ros_bridge nodes"] + visionnode["SSLVisionBridgeNode"] + gcnode["GCMulticastBridgeNode"] + teamnode["TeamClientNode"] + end + + mcreceiver["core::MulticastReceiver\n(boost::asio, shared utility)"] + + visionmc --> mcreceiver --> visionnode + gcmc --> mcreceiver --> gcnode + gctcp <--> teamnode + + visionnode -->|"~/vision_messages"| visiontopic["ssl_league_msgs/WrapperPacket"] + gcnode -->|"~/referee_messages"| reftopic["ssl_league_msgs/Referee"] + teamnode -->|"~/connection_status"| statustopic["ssl_ros_bridge_msgs/TeamClientConnectionStatus"] + teamnode -->|services| teamsrv["SetDesiredKeeper, SubstituteBot,\nReconnectTeamClient,\nSetTeamAdvantageChoice"] + statustopic -.subscribed by.-> gcnode +``` + +Each bridge node follows the same shape: receive raw bytes off the network, +parse them as a protobuf message, convert with the generated `fromProto()`, +publish the resulting ROS message. `core::MulticastReceiver` is the shared +piece — a small `boost::asio`-based UDP multicast listener used by both +`SSLVisionBridgeNode` and `GCMulticastBridgeNode`, so neither reimplements +socket handling. + +`TeamClientNode` is different in shape: it's a long-lived TCP client to the +Game Controller's automated team-control protocol, wrapped in ROS services +rather than a topic publisher, since the interactions it exposes +(substitute a bot, set the desired keeper, reconnect) are request/response, +not a stream. + +`log2bag` is not a node — it's a standalone executable that reads an SSL +game log file, runs each record through the same generated `fromProto()` +conversion functions the live nodes use, and writes the result to a rosbag2 +bag. It shares conversion code with the runtime bridge but has no ROS graph +presence of its own. + +## Testing + +`ssl_league_msgs` has a small `pytest` suite (`ssl_league_msgs/test/`) +covering the pure logic in `ateam_proto_shared.py` and +`protoc_gen_ros2msg.py` — name flattening, field-shape classification, +annotation-file parsing, and `find_type_cycles()`'s cycle detection — using +hand-built `descriptor_pb2` messages rather than real `.proto` files, so +these run in milliseconds with no protoc invocation. Everything else is +covered by the standard `ament_lint_auto` suite (`flake8`, `pep257`, +`cpplint`, `cppcheck`, `uncrustify`, `lint_cmake`, `xmllint`, `copyright`) +run via `colcon test`. + +`ssl_league_protobufs` and (for two generator scripts) `ssl_ros_bridge` +deliberately don't enforce `ament_copyright` on their own generator +scripts/CMake helpers — see the comments in each package's `CMakeLists.txt` +and `package.xml` for why, and which specific files are excluded. diff --git a/README.md b/README.md index 9466475..1f0f470 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ This repository provides ROS 2 packages containing utilities for connecting your This repo is designed to be built in a colcon workspace. You can clone this repo alongside your own code or as a submodule within your own repository. +This repo itself contains a git submodule (the league protobuf definitions), so clone with `--recurse-submodules`, or run `git submodule update --init --recursive` afterward. + ## Usage ### Bridge Nodes @@ -46,11 +48,11 @@ ros2 run ssl_ros_bridge log2bag /path/to/game/log.log ### ssl_league_protobufs -This package includes the league-defined protobuf files and builds them into a library available for other packages. +This package includes the league-defined protobuf files (via the [ssl-protocol-defs](https://github.com/RoboCup-SSL/ssl-protocol-defs) git submodule) and builds them into a library available for other packages. ### ssl_league_msgs -This package defines ROS messages which closely mirror the league protobufs. +This package defines ROS messages which closely mirror the league protobufs. These messages are generated automatically from the protobuf definitions at build time rather than hand-written — see [ARCHITECTURE.md](ARCHITECTURE.md) for how. #### Optional Fields @@ -79,7 +81,7 @@ This node listens for the multicast vision messages sent by ssl-vision and publi ##### Published Topics * ~/vision_messages - * Type: [ssl_league_msgs/msg/VisionWrapper](ssl_league_msgs/msg/vision/VisionWrapper.msg) + * Type: `ssl_league_msgs/msg/WrapperPacket` * Contains vision data including robot detections, ball detections, and field geometry. ##### Parameters @@ -108,7 +110,7 @@ The team client node needs to know the IP address fo the game controller server ##### Published Topics * ~/referee_messages - * Type: [ssl_league_msgs/msg/Referee](ssl_league_msgs/msg/game_controller/Referee.msg) + * Type: `ssl_league_msgs/msg/Referee` * Contains the latest information from the game controller. ##### Subscribed Topics @@ -187,4 +189,4 @@ _Note_: Encrypted connections are not currently supported. ## Contributing -See [our contributing guidelines](CONTRIBUTING.md). +See [our contributing guidelines](CONTRIBUTING.md). For how these packages are built and how the bridge works internally, see [ARCHITECTURE.md](ARCHITECTURE.md). diff --git a/ssl_league_msgs/CMakeLists.txt b/ssl_league_msgs/CMakeLists.txt index 7255100..dc76422 100644 --- a/ssl_league_msgs/CMakeLists.txt +++ b/ssl_league_msgs/CMakeLists.txt @@ -8,21 +8,21 @@ find_package(geometry_msgs REQUIRED) include(cmake/Ros2MsgGen.cmake) -set(_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_protobufs/proto") +set(_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_protobufs/ssl-protocol-defs/proto") generate_ros2_msgs( PROTO_FILES - ${_PROTO_DIR}/ssl_gc_common.proto - ${_PROTO_DIR}/ssl_gc_geometry.proto - ${_PROTO_DIR}/ssl_gc_game_event.proto - ${_PROTO_DIR}/ssl_gc_referee_message.proto - ${_PROTO_DIR}/ssl_gc_rcon.proto - ${_PROTO_DIR}/ssl_vision_detection.proto - ${_PROTO_DIR}/ssl_vision_geometry.proto - ${_PROTO_DIR}/ssl_vision_wrapper.proto - ${_PROTO_DIR}/ssl_simulation_config.proto - ${_PROTO_DIR}/ssl_simulation_control.proto - ${_PROTO_DIR}/ssl_simulation_error.proto + ${_PROTO_DIR}/gc/ssl_gc_common.proto + ${_PROTO_DIR}/gc/ssl_gc_geometry.proto + ${_PROTO_DIR}/gc/ssl_gc_game_event.proto + ${_PROTO_DIR}/gc/ssl_gc_referee_message.proto + ${_PROTO_DIR}/gc/ssl_gc_rcon.proto + ${_PROTO_DIR}/vision/ssl_vision_detection.proto + ${_PROTO_DIR}/vision/ssl_vision_geometry.proto + ${_PROTO_DIR}/vision/ssl_vision_wrapper.proto + ${_PROTO_DIR}/simulation/ssl_simulation_config.proto + ${_PROTO_DIR}/simulation/ssl_simulation_control.proto + ${_PROTO_DIR}/simulation/ssl_simulation_error.proto PROTO_PATHS ${_PROTO_DIR} SIDECAR @@ -39,6 +39,10 @@ ament_export_dependencies(rosidl_default_runtime) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) ament_lint_auto_find_test_dependencies() + + find_package(ament_cmake_pytest REQUIRED) + ament_add_pytest_test(test_ateam_proto_shared test/test_ateam_proto_shared.py) + ament_add_pytest_test(test_protoc_gen_ros2msg test/test_protoc_gen_ros2msg.py) endif() ament_package() diff --git a/ssl_league_msgs/cmake/ateam_proto_shared.py b/ssl_league_msgs/cmake/ateam_proto_shared.py index e1168ab..bbaf54f 100644 --- a/ssl_league_msgs/cmake/ateam_proto_shared.py +++ b/ssl_league_msgs/cmake/ateam_proto_shared.py @@ -3,16 +3,16 @@ protoc_gen_ros2msg.py (ssl_league_msgs) is the .msg generator; gen_message_conversion.py (ssl_ros_bridge) is the C++ proto<->ROS bridge -generator. Both must agree on naming, map-entry detection, and sidecar +generator. Both must agree on naming, map-entry detection, and annotation semantics; that logic lives here once instead of two copies. Import with: from ateam_proto_shared import ( parse_options, flatten_type_name, strip_package, build_map_entry_type_names, iter_messages, - load_sidecar, sidecar_consumed, output_proto_fields, - field_shape, FieldShape, - FieldAnnotation, OutputEntry, MessageSidecarEntry, Sidecar, + load_annotations, consumed_annotation_fields, output_proto_fields, + classify_field_shape, FieldShape, + FieldAnnotation, OutputEntry, MessageAnnotationEntry, Annotations, HAS_FIELD_PREFIX, ) """ @@ -25,13 +25,13 @@ FD = descriptor_pb2.FieldDescriptorProto # --------------------------------------------------------------------------- -# Sidecar JSON shapes +# Annotation JSON shapes # --------------------------------------------------------------------------- # # Typed as TypedDict, not a dataclass: this is JSON config with no behavior, # and every call site already uses plain dict access (.get(...), [...]). # TypedDict types that shape and catches a typo'd key at type-check time -# without changing the runtime value. See the sidecar annotation format in +# without changing the runtime value. See the annotation file format in # protoc_gen_ros2msg.py's module docstring. # # OutputEntry uses the functional TypedDict form because one of its keys is @@ -41,7 +41,7 @@ class FieldAnnotation(TypedDict, total=False): - """One entry under a message's sidecar 'fields' map.""" + """One entry under a message's annotation entry 'fields' map.""" ros_type: str ros_field: str @@ -73,20 +73,20 @@ class FieldAnnotation(TypedDict, total=False): ) -class MessageSidecarEntry(TypedDict, total=False): - """The sidecar entry for one message, keyed by the message's flat_name.""" +class MessageAnnotationEntry(TypedDict, total=False): + """The annotation entry for one message, keyed by the message's flat_name.""" fields: dict[str, FieldAnnotation] outputs: list[OutputEntry] -# The sidecar's top level mixes message-name keys (-> MessageSidecarEntry) -# with reserved keys ("_skip_types": list[str], documentation-only -# "_comment"/"_schema") that aren't message entries. TypedDict cannot -# express "arbitrary keys of type A except these keys of type B", so the -# top level stays loosely typed; only entries returned by -# sidecar.get(flat_name, {}) are typed as MessageSidecarEntry. -Sidecar = dict[str, Any] +# The annotation file's top level mixes message-name keys (-> +# MessageAnnotationEntry) with reserved keys ("_skip_types": list[str], +# documentation-only "_comment"/"_schema") that aren't message entries. +# TypedDict cannot express "arbitrary keys of type A except these keys of +# type B", so the top level stays loosely typed; only entries returned by +# annotations.get(flat_name, {}) are typed as MessageAnnotationEntry. +Annotations = dict[str, Any] # Prefix for the ROS-side presence-sentinel bool emitted alongside a # non-oneof message-type field under optional_submsg=has_field (see @@ -193,7 +193,7 @@ class FieldShape(NamedTuple): One of: repeated (genuinely `repeated` in the proto), in a oneof, or a proto2 `optional` scalar/message (modeled as a 0/1-element ROS array; - see ros2_field_type). This 3-line computation was duplicated across + see map_field_to_ros2_type). This 3-line computation was duplicated across both generators; one typed helper replaces it. """ @@ -202,7 +202,7 @@ class FieldShape(NamedTuple): is_proto2_optional: bool -def field_shape(field: descriptor_pb2.FieldDescriptorProto, proto2: bool) -> FieldShape: +def classify_field_shape(field: descriptor_pb2.FieldDescriptorProto, proto2: bool) -> FieldShape: in_oneof = field.HasField('oneof_index') return FieldShape( is_repeated=field.label == FD.LABEL_REPEATED, @@ -212,15 +212,15 @@ def field_shape(field: descriptor_pb2.FieldDescriptorProto, proto2: bool) -> Fie # --------------------------------------------------------------------------- -# Sidecar helpers +# Annotation file helpers # --------------------------------------------------------------------------- # -# Sidecar annotation format is documented in protoc_gen_ros2msg.py's module +# Annotation file format is documented in protoc_gen_ros2msg.py's module # docstring (gen_message_conversion.py's docstring points there rather than # duplicating it). Both generators consume the same JSON file and must # agree on what a "consumed" field is, so that logic lives here once. -def load_sidecar(path: str | None) -> Sidecar: +def load_annotations(path: str | None) -> Annotations: if not path: return {} with open(path) as f: @@ -228,7 +228,7 @@ def load_sidecar(path: str | None) -> Sidecar: def output_proto_fields(out: OutputEntry) -> Iterator[str]: - """Yield proto field names consumed by one sidecar 'outputs' entry.""" + """Yield proto field names consumed by one annotation 'outputs' entry.""" for v in out.get('from', {}).values(): yield v for sub in ('position', 'orientation'): @@ -237,14 +237,14 @@ def output_proto_fields(out: OutputEntry) -> Iterator[str]: yield v -def sidecar_consumed(sidecar_entry: MessageSidecarEntry) -> frozenset[str]: +def consumed_annotation_fields(annotation_entry: MessageAnnotationEntry) -> frozenset[str]: """ - Return the proto field names consumed by a message's sidecar entry. + Return the proto field names consumed by a message's annotation entry. Right-hand side of all 'from' maps, plus all 'fields' keys. Excluded from passthrough emission by both generators. """ - consumed = set(sidecar_entry.get('fields', {}).keys()) - for out in sidecar_entry.get('outputs', []): + consumed = set(annotation_entry.get('fields', {}).keys()) + for out in annotation_entry.get('outputs', []): consumed.update(output_proto_fields(out)) return frozenset(consumed) diff --git a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py index 4b9bf65..594b563 100755 --- a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py +++ b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py @@ -22,10 +22,10 @@ optional_submsg=error Reject any non-oneof message-type field as a build error; forces schema authors to be explicit. sidecar= Path to a JSON annotation file. Fields listed in - the sidecar are consumed and emitted as annotated - ROS types; remaining fields pass through normally. + it are consumed and emitted as annotated ROS + types; remaining fields pass through normally. -Sidecar annotation format (per message): +Annotation file format (per message): "MsgName": { "fields": { "proto_field": { "ros_type": "...", "ros_field": "...", "scale": ... }, @@ -54,7 +54,7 @@ Some proto constructs have no sane ROS translation (e.g. google.protobuf.Any has no fixed schema to map to a ROS type). The generator fails by default on - these; the sidecar can opt in to dropping them: + these; the annotation file can opt in to dropping them: - A "skip": true field annotation (must be the only key) omits that field from the generated .msg, like any other consumed field. - A top-level "_skip_types" list omits an entire message (no .msg file @@ -85,19 +85,19 @@ # Must follow the sys.path.insert() above, so this can't sort before the # google.protobuf imports the way import-order linting wants. from ateam_proto_shared import ( # noqa: E402, I100 + Annotations, build_map_entry_type_names, - field_shape, + classify_field_shape, + consumed_annotation_fields, FieldAnnotation, flatten_type_name, HAS_FIELD_PREFIX, iter_messages, - load_sidecar, - MessageSidecarEntry, + load_annotations, + MessageAnnotationEntry, output_proto_fields, OutputEntry, parse_options, - Sidecar, - sidecar_consumed, ) FD = descriptor_pb2.FieldDescriptorProto @@ -132,11 +132,11 @@ 'builtin_interfaces/Duration', }) -# Populated by main() from sidecar "_skip_types" before generation runs. +# Populated by main() from the annotation file's "_skip_types" before generation runs. _SKIP_TYPES: frozenset = frozenset() -def ros2_field_type(field: descriptor_pb2.FieldDescriptorProto) -> str: +def map_field_to_ros2_type(field: descriptor_pb2.FieldDescriptorProto) -> str: if field.type in SCALAR_TYPE_MAP: return SCALAR_TYPE_MAP[field.type] @@ -147,14 +147,14 @@ def ros2_field_type(field: descriptor_pb2.FieldDescriptorProto) -> str: if field.type_name == '.google.protobuf.Any': raise ValueError( "Fields with type 'Any' are not supported (no fixed schema to map " - "to a ROS type). Add a 'skip' field annotation in the sidecar." + "to a ROS type). Add a 'skip' field annotation in the annotation file." ) flat = flatten_type_name(field.type_name).replace('SSL_', '') if flat in _SKIP_TYPES: raise ValueError( - f"references type '{flat}', which is skipped via sidecar " - f"'_skip_types'. Add a 'skip' field annotation for this field, " + f"references type '{flat}', which is skipped via the annotation " + f"file's '_skip_types'. Add a 'skip' field annotation for this field, " f"or remove '{flat}' from '_skip_types'." ) @@ -164,23 +164,23 @@ def ros2_field_type(field: descriptor_pb2.FieldDescriptorProto) -> str: # --------------------------------------------------------------------------- -# Sidecar validation (error accumulation is specific to this script's -# protoc-plugin response.error mechanism, so this stays local; -# load_sidecar/sidecar_consumed/output_proto_fields are shared — see -# ateam_proto_shared.py) +# Annotation entry validation (error accumulation is specific to this +# script's protoc-plugin response.error mechanism, so this stays local; +# load_annotations/consumed_annotation_fields/output_proto_fields are +# shared — see ateam_proto_shared.py) # --------------------------------------------------------------------------- -def _validate_sidecar_entry( - sidecar_entry: MessageSidecarEntry, +def _validate_annotation_entry( + annotation_entry: MessageAnnotationEntry, proto_field_map: dict[str, descriptor_pb2.FieldDescriptorProto], flat_name: str, errors: list[str], ) -> None: - for proto_name, ann in sidecar_entry.get('fields', {}).items(): + for proto_name, ann in annotation_entry.get('fields', {}).items(): if proto_name not in proto_field_map: errors.append( - f"{flat_name}: sidecar 'fields' references unknown proto field '{proto_name}'" + f"{flat_name}: annotation 'fields' references unknown proto field '{proto_name}'" ) continue @@ -212,12 +212,12 @@ def _validate_sidecar_entry( f'stand in for a whole list.' ) - for out in sidecar_entry.get('outputs', []): + for out in annotation_entry.get('outputs', []): ros_field = out.get('ros_field', '') for proto_name in output_proto_fields(out): if proto_name not in proto_field_map: errors.append( - f"{flat_name}: sidecar output '{ros_field}' references unknown " + f"{flat_name}: annotation output '{ros_field}' references unknown " f"proto field '{proto_name}'" ) @@ -228,7 +228,7 @@ def _validate_sidecar_entry( ) -def _message_type_edges( +def _iter_message_type_edges( msg: descriptor_pb2.DescriptorProto, consumed: frozenset[str], map_entry_type_names: frozenset[str], @@ -237,7 +237,7 @@ def _message_type_edges( Yield (proto_field_name, target_flat_type_name) for msg's message-type fields. Only fields that will survive into the generated .msg — not consumed by - the sidecar, not a map entry, not Any, not a skipped type. + an annotation, not a map entry, not Any, not a skipped type. """ for field in msg.field: if field.name in consumed: @@ -257,19 +257,19 @@ def _message_type_edges( def find_type_cycles( all_files: dict[str, descriptor_pb2.FileDescriptorProto], file_to_generate_names: Iterable[str], - sidecar: Sidecar, + annotations: Annotations, map_entry_type_names: frozenset[str], ) -> list[str]: """ Detect cycles in the message-type reference graph across every generated message. - Considers the graph post sidecar consumption/skip. ROS2 .msg files are + Considers the graph post annotation consumption/skip. ROS2 .msg files are fixed-layout structs and cannot be self-referential, even indirectly — a cycle always breaks generation downstream (rosidl_generator_type_description has a latent bug: it crashes with an opaque KeyError instead of a clean error; see calculate_type_hash's double-delete of a cycled type's 'default_value' key after deepcopy aliasing). Detect it here instead, - with the concrete chain, so the sidecar can 'skip' a field to break it. + with the concrete chain, so the annotation file can 'skip' a field to break it. """ graph: dict[str, list[tuple[str, str]]] = {} for file_name in file_to_generate_names: @@ -278,8 +278,8 @@ def find_type_cycles( if flat_name in _SKIP_TYPES: continue - consumed = sidecar_consumed(sidecar.get(flat_name, {})) - graph[flat_name] = list(_message_type_edges(msg, consumed, map_entry_type_names)) + consumed = consumed_annotation_fields(annotations.get(flat_name, {})) + graph[flat_name] = list(_iter_message_type_edges(msg, consumed, map_entry_type_names)) found: set[tuple[tuple[str, ...], tuple[str, ...]]] = set() # canonicalized (nodes, fields) @@ -313,19 +313,19 @@ def dfs(node: str, path: list[str], path_fields: list[str]) -> None: f'Recursive message type reference detected: {chain}. ROS2 ' f'messages are fixed-layout structs and cannot be ' f"self-referential. Add a 'skip' field annotation in the " - f'sidecar on one of the fields in the cycle to break it.' + f'annotation file on one of the fields in the cycle to break it.' ) return errors def _emit_one_output(out: OutputEntry, lines: list[str]) -> None: - """Emit the single ROS struct field for one sidecar 'outputs' entry.""" + """Emit the single ROS struct field for one annotation 'outputs' entry.""" lines.append(f"{out['ros_type']} {out['ros_field']}") -def _default_literal(value: bool | str | int | float) -> str: - """Format a sidecar 'default' JSON value as a ROS2 .msg default-value literal.""" +def _format_default_literal(value: bool | str | int | float) -> str: + """Format an annotation 'default' JSON value as a ROS2 .msg default-value literal.""" if isinstance(value, bool): return 'true' if value else 'false' @@ -336,7 +336,7 @@ def _default_literal(value: bool | str | int | float) -> str: return repr(value) -def _emit_one_sidecar_field( +def _emit_annotation_field_override( proto_name: str, ann: FieldAnnotation, pf: descriptor_pb2.FieldDescriptorProto, @@ -366,13 +366,13 @@ def _emit_one_sidecar_field( return else: try: - ros_type = ros2_field_type(pf) + ros_type = map_field_to_ros2_type(pf) except ValueError as e: errors.append(f'{flat_name}.{proto_name}: {e}') return - shape = field_shape(pf, proto2) + shape = classify_field_shape(pf, proto2) if 'default' not in ann: if shape.is_repeated or shape.is_proto2_optional: @@ -385,10 +385,10 @@ def _emit_one_sidecar_field( # 'default' was already rejected at validation time for genuinely repeated # fields (a single value cannot stand in for a whole list). A proto2 # "optional" scalar is modeled as a 0/1-element ROS array (see - # ros2_field_type), so its default is a 1-element array literal, matching + # map_field_to_ros2_type), so its default is a 1-element array literal, matching # the presence convention: unset proto field -> empty array, default ROS # field -> that one value present. - default_lit = _default_literal(ann['default']) + default_lit = _format_default_literal(ann['default']) if shape.is_proto2_optional: lines.append(f'{ros_type}[] {ros_name} [{default_lit}]') else: @@ -399,7 +399,7 @@ def _emit_one_sidecar_field( # Main message generator # --------------------------------------------------------------------------- -def generate_message_msg( +def generate_message_definition( msg: descriptor_pb2.DescriptorProto, flat_name: str, source_file: str, @@ -407,10 +407,10 @@ def generate_message_msg( errors: list[str], map_entry_type_names: frozenset[str], proto2: bool = False, - sidecar_entry: MessageSidecarEntry | None = None, + annotation_entry: MessageAnnotationEntry | None = None, ) -> str: - if sidecar_entry is None: - sidecar_entry = {} + if annotation_entry is None: + annotation_entry = {} lines = [ f'# Generated from proto message {flat_name}', @@ -420,11 +420,11 @@ def generate_message_msg( proto_field_map = {f.name: f for f in msg.field} - # Validate sidecar references before emitting anything. - _validate_sidecar_entry(sidecar_entry, proto_field_map, flat_name, errors) + # Validate annotation references before emitting anything. + _validate_annotation_entry(annotation_entry, proto_field_map, flat_name, errors) - consumed = sidecar_consumed(sidecar_entry) - field_overrides = sidecar_entry.get('fields', {}) + consumed = consumed_annotation_fields(annotation_entry) + field_overrides = annotation_entry.get('fields', {}) # 1. Enum value constants, upfront per ROS2 convention — one blank line # after each enum's cluster. @@ -438,15 +438,15 @@ def generate_message_msg( lines.append('') # 2. Fields, walked in proto declaration order — this holds even for - # sidecar-annotated fields/outputs, so the .msg reads in the same order - # as the .proto. A sidecar 'outputs' entry (several proto fields + # annotated fields/outputs, so the .msg reads in the same order + # as the .proto. An annotation 'outputs' entry (several proto fields # collapsing into one ROS field, e.g. x/y/z -> a Point32) is emitted once, # at the position of the first proto field it consumes; the entry's other # consumed fields are skipped where they would otherwise fall, rather # than splitting the composite field's data across multiple positions. pending_outputs: list[tuple[OutputEntry, frozenset[str]]] = [ (out, frozenset(output_proto_fields(out))) - for out in sidecar_entry.get('outputs', []) + for out in annotation_entry.get('outputs', []) ] output_trigger: dict[str, OutputEntry] = {} for field in msg.field: @@ -466,7 +466,7 @@ def generate_message_msg( if field.name in consumed: if field.name in field_overrides: - _emit_one_sidecar_field( + _emit_annotation_field_override( field.name, field_overrides[field.name], field, lines, proto2, errors, flat_name, ) @@ -478,11 +478,11 @@ def generate_message_msg( errors.append( f'{flat_name}.{field.name}: proto map fields have no ROS2 ' f"equivalent and are not supported. Add a 'skip' field annotation " - f'in the sidecar for this field to drop it explicitly.' + f'in the annotation file for this field to drop it explicitly.' ) continue - shape = field_shape(field, proto2) + shape = classify_field_shape(field, proto2) is_repeated, in_oneof, is_proto2_optional = shape if in_oneof: @@ -517,14 +517,14 @@ def generate_message_msg( lines.append(f'uint8 {oneof_name}_case') for of in oneof_fields: try: - lines.append(f'{ros2_field_type(of)} {of.name}') + lines.append(f'{map_field_to_ros2_type(of)} {of.name}') except ValueError as e: errors.append(f'{flat_name}.{of.name}: {e}') continue try: - ros2_type = ros2_field_type(field) + ros2_type = map_field_to_ros2_type(field) except ValueError as e: errors.append(f'{flat_name}.{field.name}: {e}') continue @@ -550,14 +550,14 @@ def generate_message_msg( return '\n'.join(lines) + '\n' -def generate_enum_msg( +def generate_enum_definition( enum: descriptor_pb2.EnumDescriptorProto, flat_name: str, source_file: str, ) -> str: """ Constants-only .msg for a top-level proto enum. Nested enums are handled separately, inline in their containing message - (see the 'Enum value constants' step in generate_message_msg) — this + (see the 'Enum value constants' step in generate_message_definition) — this covers only enums declared at file scope, which have no containing message to attach constants to. """ @@ -592,11 +592,11 @@ def main() -> None: return - sidecar_path = opts.get('sidecar', None) - sidecar: Sidecar = load_sidecar(sidecar_path) + annotations_path = opts.get('sidecar', None) + annotations: Annotations = load_annotations(annotations_path) global _SKIP_TYPES - _SKIP_TYPES = frozenset(sidecar.get('_skip_types', [])) + _SKIP_TYPES = frozenset(annotations.get('_skip_types', [])) seen_skip_types: set[str] = set() map_entry_type_names = build_map_entry_type_names(request.proto_file) @@ -606,7 +606,7 @@ def main() -> None: errors: list[str] = [] errors.extend(find_type_cycles( - all_files, request.file_to_generate, sidecar, map_entry_type_names + all_files, request.file_to_generate, annotations, map_entry_type_names )) emitted_names: set[str] = set() @@ -628,9 +628,9 @@ def main() -> None: emitted_names.add(flat_name) out = response.file.add() out.name = f'{flat_name}.msg' - out.content = generate_message_msg( + out.content = generate_message_definition( msg, flat_name, source_file, optional_submsg, errors, map_entry_type_names, - proto2, sidecar_entry=sidecar.get(flat_name, {}) + proto2, annotation_entry=annotations.get(flat_name, {}) ) for enum in fd.enum_type: @@ -648,12 +648,12 @@ def main() -> None: emitted_names.add(flat_name) out = response.file.add() out.name = f'{flat_name}.msg' - out.content = generate_enum_msg(enum, flat_name, source_file) + out.content = generate_enum_definition(enum, flat_name, source_file) unused_skip_types = _SKIP_TYPES - seen_skip_types if unused_skip_types: errors.append( - f"sidecar '_skip_types' references message(s) never encountered " + f"annotation file '_skip_types' references message(s) never encountered " f'during generation: {sorted(unused_skip_types)}' ) diff --git a/ssl_league_msgs/package.xml b/ssl_league_msgs/package.xml index 998c4b3..988d884 100644 --- a/ssl_league_msgs/package.xml +++ b/ssl_league_msgs/package.xml @@ -20,7 +20,20 @@ geometry_msgs ament_lint_auto - ament_lint_common + + ament_cmake_cppcheck + ament_cmake_cpplint + ament_cmake_flake8 + ament_cmake_lint_cmake + ament_cmake_pep257 + ament_cmake_uncrustify + ament_cmake_xmllint + ament_cmake_pytest + python3-pytest rosidl_interface_packages diff --git a/ssl_league_msgs/test/conftest.py b/ssl_league_msgs/test/conftest.py new file mode 100644 index 0000000..b5ca827 --- /dev/null +++ b/ssl_league_msgs/test/conftest.py @@ -0,0 +1,6 @@ +"""Make the cmake/ generator scripts importable without installing them as a package.""" + +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'cmake')) diff --git a/ssl_league_msgs/test/test_ateam_proto_shared.py b/ssl_league_msgs/test/test_ateam_proto_shared.py new file mode 100644 index 0000000..cc462a6 --- /dev/null +++ b/ssl_league_msgs/test/test_ateam_proto_shared.py @@ -0,0 +1,105 @@ +"""Unit tests for the pure helper functions in ateam_proto_shared.py.""" + +from ateam_proto_shared import ( + build_map_entry_type_names, + classify_field_shape, + consumed_annotation_fields, + flatten_type_name, + iter_messages, + output_proto_fields, + parse_options, +) +from google.protobuf import descriptor_pb2 + +FD = descriptor_pb2.FieldDescriptorProto +DP = descriptor_pb2.DescriptorProto + + +def test_parse_options_empty(): + assert parse_options('') == {} + + +def test_parse_options_multiple_keys(): + assert parse_options('a=1,b=2') == {'a': '1', 'b': '2'} + + +def test_parse_options_ignores_entries_without_equals(): + assert parse_options('a=1,noeq,b=2') == {'a': '1', 'b': '2'} + + +def test_flatten_type_name_examples(): + assert flatten_type_name('.ateam.BasicControl') == 'BasicControl' + assert flatten_type_name('.GameEvent.BallLeftField') == 'GameEventBallLeftField' + assert flatten_type_name('.ateam_test.OuterMessage.Inner') == 'OuterMessageInner' + + +def test_classify_field_shape_repeated(): + field = FD(name='items', label=FD.LABEL_REPEATED) + shape = classify_field_shape(field, proto2=False) + assert shape.is_repeated + assert not shape.in_oneof + assert not shape.is_proto2_optional + + +def test_classify_field_shape_oneof_member(): + field = FD(name='choice', label=FD.LABEL_OPTIONAL, oneof_index=0) + shape = classify_field_shape(field, proto2=True) + assert not shape.is_repeated + assert shape.in_oneof + # A oneof member is never treated as a proto2-optional array, even + # though the label matches — presence is already modeled by the case enum. + assert not shape.is_proto2_optional + + +def test_classify_field_shape_proto2_optional(): + field = FD(name='maybe', label=FD.LABEL_OPTIONAL) + shape = classify_field_shape(field, proto2=True) + assert not shape.is_repeated + assert not shape.in_oneof + assert shape.is_proto2_optional + + +def test_classify_field_shape_proto3_singular_is_not_proto2_optional(): + # Same descriptor shape as the proto2-optional case, but proto2=False — + # the caller's syntax flag gates this, not the label alone. + field = FD(name='maybe', label=FD.LABEL_OPTIONAL) + shape = classify_field_shape(field, proto2=False) + assert not shape.is_proto2_optional + + +def test_output_proto_fields_from_and_components(): + out = { + 'from': {'x': 'proto_x'}, + 'position': {'from': {'x': 'tx', 'y': 'ty'}}, + 'orientation': {'from': {'x': 'q0'}}, + } + assert set(output_proto_fields(out)) == {'proto_x', 'tx', 'ty', 'q0'} + + +def test_consumed_annotation_fields_combines_fields_and_outputs(): + entry = { + 'fields': {'renamed_field': {'ros_field': 'renamed'}}, + 'outputs': [{'ros_field': 'point', 'from': {'x': 'px', 'y': 'py'}}], + } + assert consumed_annotation_fields(entry) == {'renamed_field', 'px', 'py'} + + +def test_build_map_entry_type_names_finds_nested_map_entry(): + map_entry = DP(name='FooEntry') + map_entry.options.map_entry = True + msg = DP(name='Foo', nested_type=[map_entry]) + fd = descriptor_pb2.FileDescriptorProto(package='testpkg', message_type=[msg]) + + result = build_map_entry_type_names([fd]) + + assert result == frozenset({'.testpkg.Foo.FooEntry'}) + + +def test_iter_messages_flattens_nested_names_and_strips_ssl_prefix(): + inner = DP(name='Inner') + outer = DP(name='SSL_Outer', nested_type=[inner]) + fd = descriptor_pb2.FileDescriptorProto(package='testpkg', message_type=[outer]) + + results = {flat: cpp for flat, cpp, _msg in iter_messages(fd)} + + assert results == {'Outer': 'SSL_Outer', 'OuterInner': 'SSL_Outer_Inner'} diff --git a/ssl_league_msgs/test/test_protoc_gen_ros2msg.py b/ssl_league_msgs/test/test_protoc_gen_ros2msg.py new file mode 100644 index 0000000..197077b --- /dev/null +++ b/ssl_league_msgs/test/test_protoc_gen_ros2msg.py @@ -0,0 +1,99 @@ +""" +Unit tests for protoc_gen_ros2msg.py's recursion detection and type mapping. + +find_type_cycles is the highest-value target here: a real recursive proto +(GameEvent -> MultipleFouls -> GameEvent) previously had to be caught by +hand-comparing generated .msg output against the old hand-written messages. +These tests build small fake descriptors directly (no protoc invocation +needed) to pin down that behavior. +""" + +from google.protobuf import descriptor_pb2 +import protoc_gen_ros2msg as gen +import pytest + +FD = descriptor_pb2.FieldDescriptorProto +DP = descriptor_pb2.DescriptorProto +FDP = descriptor_pb2.FileDescriptorProto + + +def _message_field(name, type_name): + return FD( + name=name, number=1, label=FD.LABEL_OPTIONAL, type=FD.TYPE_MESSAGE, + type_name=type_name, + ) + + +def _two_message_file(a_fields=(), b_fields=()): + """Build a FileDescriptorProto with two top-level messages, A and B, in package testpkg.""" + a = DP(name='A', field=list(a_fields)) + b = DP(name='B', field=list(b_fields)) + return FDP(name='test.proto', package='testpkg', message_type=[a, b]) + + +def test_find_type_cycles_no_cycle(): + fd = _two_message_file(a_fields=[_message_field('b', '.testpkg.B')]) + all_files = {'test.proto': fd} + + errors = gen.find_type_cycles(all_files, ['test.proto'], {}, frozenset()) + + assert errors == [] + + +def test_find_type_cycles_direct_self_reference(): + fd = _two_message_file(a_fields=[_message_field('self_ref', '.testpkg.A')]) + all_files = {'test.proto': fd} + + errors = gen.find_type_cycles(all_files, ['test.proto'], {}, frozenset()) + + assert len(errors) == 1 + assert 'A --[self_ref]--> A' in errors[0] + + +def test_find_type_cycles_indirect_cycle(): + fd = _two_message_file( + a_fields=[_message_field('b', '.testpkg.B')], + b_fields=[_message_field('a', '.testpkg.A')], + ) + all_files = {'test.proto': fd} + + errors = gen.find_type_cycles(all_files, ['test.proto'], {}, frozenset()) + + assert len(errors) == 1 + assert 'A --[b]--> B --[a]--> A' in errors[0] + + +def test_find_type_cycles_skip_annotation_breaks_the_cycle(): + fd = _two_message_file( + a_fields=[_message_field('b', '.testpkg.B')], + b_fields=[_message_field('a', '.testpkg.A')], + ) + all_files = {'test.proto': fd} + annotations = {'A': {'fields': {'b': {'skip': True}}}} + + errors = gen.find_type_cycles(all_files, ['test.proto'], annotations, frozenset()) + + assert errors == [] + + +def test_map_field_to_ros2_type_scalar(): + field = FD(name='n', type=FD.TYPE_INT32) + assert gen.map_field_to_ros2_type(field) == 'int32' + + +def test_map_field_to_ros2_type_enum_is_uint8(): + field = FD(name='e', type=FD.TYPE_ENUM) + assert gen.map_field_to_ros2_type(field) == 'uint8' + + +def test_map_field_to_ros2_type_rejects_any(): + field = FD(name='a', type=FD.TYPE_MESSAGE, type_name='.google.protobuf.Any') + with pytest.raises(ValueError, match='Any'): + gen.map_field_to_ros2_type(field) + + +def test_map_field_to_ros2_type_rejects_skipped_type(monkeypatch): + monkeypatch.setattr(gen, '_SKIP_TYPES', frozenset({'Dropped'})) + field = FD(name='d', type=FD.TYPE_MESSAGE, type_name='.testpkg.Dropped') + with pytest.raises(ValueError, match='_skip_types'): + gen.map_field_to_ros2_type(field) diff --git a/ssl_league_protobufs/CMakeLists.txt b/ssl_league_protobufs/CMakeLists.txt index 29b6eb5..3001fdf 100644 --- a/ssl_league_protobufs/CMakeLists.txt +++ b/ssl_league_protobufs/CMakeLists.txt @@ -4,37 +4,60 @@ project(ssl_league_protobufs) find_package(ament_cmake REQUIRED) find_package(Protobuf REQUIRED) -protobuf_generate_cpp( - PROTOBUF_SRCS - PROTOBUF_HDRS - proto/ssl_gc_api.proto - proto/ssl_gc_api.proto - proto/ssl_gc_change.proto - proto/ssl_gc_ci.proto - proto/ssl_gc_common.proto - proto/ssl_gc_engine_config.proto - proto/ssl_gc_engine.proto - proto/ssl_gc_game_event.proto - proto/ssl_gc_geometry.proto - proto/ssl_gc_rcon_autoref.proto - proto/ssl_gc_rcon_remotecontrol.proto - proto/ssl_gc_rcon_team.proto - proto/ssl_gc_rcon.proto - proto/ssl_gc_referee_message.proto - proto/ssl_gc_state.proto - proto/ssl_simulation_config.proto - proto/ssl_simulation_control.proto - proto/ssl_simulation_error.proto - proto/ssl_simulation_robot_control.proto - proto/ssl_simulation_robot_feedback.proto - proto/ssl_simulation_synchronous.proto - proto/ssl_vision_detection_tracked.proto - proto/ssl_vision_detection.proto - proto/ssl_vision_geometry.proto - proto/ssl_vision_wrapper_tracked.proto - proto/ssl_vision_wrapper.proto +# ssl-protocol-defs/ is the RoboCup-SSL/ssl-protocol-defs submodule. Its proto +# files import each other relative to their own proto/ root (for example, +# "gc/ssl_gc_common.proto"), and protoc mirrors that same relative path into +# its generated #include lines and its --cpp_out layout. CMake's +# protobuf_generate_cpp() macro instead places each output file relative to +# CMAKE_CURRENT_SOURCE_DIR, one directory above the submodule's proto/ root. +# The two roots disagree, so compilation fails with "no such file" on the +# cross-includes. Invoking protoc directly with one consistent --proto_path +# root avoids that mismatch. +set(_PROTO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/ssl-protocol-defs/proto") + +set(_PROTO_FILES + gc/ssl_gc_common.proto + gc/ssl_gc_game_event.proto + gc/ssl_gc_geometry.proto + gc/ssl_gc_rcon_autoref.proto + gc/ssl_gc_rcon_remotecontrol.proto + gc/ssl_gc_rcon_team.proto + gc/ssl_gc_rcon.proto + gc/ssl_gc_referee_message.proto + simulation/ssl_simulation_config.proto + simulation/ssl_simulation_control.proto + simulation/ssl_simulation_error.proto + simulation/ssl_simulation_robot_control.proto + simulation/ssl_simulation_robot_feedback.proto + simulation/ssl_simulation_synchronous.proto + vision/ssl_vision_detection_tracked.proto + vision/ssl_vision_detection.proto + vision/ssl_vision_geometry.proto + vision/ssl_vision_wrapper_tracked.proto + vision/ssl_vision_wrapper.proto ) +set(PROTOBUF_SRCS) +set(PROTOBUF_HDRS) +set(_abs_proto_files) +foreach(_rel ${_PROTO_FILES}) + get_filename_component(_stem "${_rel}" NAME_WLE) + get_filename_component(_dir "${_rel}" DIRECTORY) + list(APPEND PROTOBUF_SRCS "${CMAKE_CURRENT_BINARY_DIR}/${_dir}/${_stem}.pb.cc") + list(APPEND PROTOBUF_HDRS "${CMAKE_CURRENT_BINARY_DIR}/${_dir}/${_stem}.pb.h") + list(APPEND _abs_proto_files "${_PROTO_ROOT}/${_rel}") +endforeach() + +add_custom_command( + OUTPUT ${PROTOBUF_SRCS} ${PROTOBUF_HDRS} + COMMAND protobuf::protoc + ARGS --cpp_out=${CMAKE_CURRENT_BINARY_DIR} --proto_path=${_PROTO_ROOT} ${_abs_proto_files} + DEPENDS ${_abs_proto_files} protobuf::protoc + COMMENT "Running protoc on ssl-protocol-defs protos" + VERBATIM +) +set_source_files_properties(${PROTOBUF_SRCS} ${PROTOBUF_HDRS} PROPERTIES GENERATED TRUE) + add_library(${PROJECT_NAME} SHARED ${PROTOBUF_HDRS} ${PROTOBUF_SRCS} @@ -43,9 +66,19 @@ target_link_libraries(${PROJECT_NAME} protobuf::libprotobuf ) target_include_directories(${PROJECT_NAME} - INTERFACE + PUBLIC + # Generated headers live under subdirectories (gc/, vision/, simulation/) + # that mirror ssl-protocol-defs' own layout, and they include each other + # by path from that root (for example, "gc/ssl_gc_common.pb.h"). PUBLIC, + # not INTERFACE, because this package's own compilation needs that root + # too, not just external consumers, to resolve those same cross-includes. + # Two include roots are exported: the normal + # root every consumer's top-level include expects, and this package's + # own generated-header root, needed to resolve the cross-includes at + # both build and install time. $ $ + $ ) # Disable noisy deprecation warnings we can't control from league protobufs target_compile_options(${PROJECT_NAME} PRIVATE -Wno-deprecated-declarations) @@ -65,6 +98,17 @@ install(TARGETS ${PROJECT_NAME} INCLUDES DESTINATION include) if(BUILD_TESTING) + # ssl-protocol-defs/ is a vendored third-party submodule. Its files are + # not ours to lint or copyright-check. AMENT_LINT_AUTO_FILE_EXCLUDE + # correctly excludes it for flake8, cpplint, cppcheck, and uncrustify. + # ament_cmake_pep257's hook has no exclude support at all; it always scans + # every *.py file recursively. ament_copyright's exclude only matches + # exact file paths, not a directory prefix. This package has no Python or + # copyright-checked source of its own, so both linters are skipped + # entirely rather than left to flag the submodule's files. + set(AMENT_LINT_AUTO_FILE_EXCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/ssl-protocol-defs") + set(AMENT_LINT_AUTO_EXCLUDE ament_cmake_pep257 ament_cmake_copyright) + find_package(ament_lint_auto REQUIRED) ament_lint_auto_find_test_dependencies() endif() diff --git a/ssl_league_protobufs/proto/ssl_gc_api.proto b/ssl_league_protobufs/proto/ssl_gc_api.proto deleted file mode 100644 index 1ef2c6d..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_api.proto +++ /dev/null @@ -1,56 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/api"; - -import "ssl_gc_state.proto"; -import "ssl_gc_change.proto"; -import "ssl_gc_engine.proto"; -import "ssl_gc_engine_config.proto"; - -import "google/protobuf/duration.proto"; - -// Message format that is pushed from the GC to the client -message Output { - // The current match state - optional State match_state = 1; - // The current GC state - optional GcState gc_state = 2; - // The protocol - optional Protocol protocol = 3; - // The engine config - optional Config config = 4; -} - -// The game protocol -message Protocol { - // Is this a delta only? - // Entries that were already sent are not sent again, because the protocol is immutable anyway. - // But if the game is reset, the whole protocol must be replaced. That's what this flag is for. - optional bool delta = 1; - // The (delta) list of entries - repeated ProtocolEntry entry = 2; -} - -// A protocol entry of a change -message ProtocolEntry { - // Id of the entry - optional int32 id = 1; - // The change that was made - optional Change change = 2; - // The match time elapsed when this change was made - optional google.protobuf.Duration match_time_elapsed = 3; - // The stage time elapsed when this change was made - optional google.protobuf.Duration stage_time_elapsed = 4; -} - -// Message format that can be send from the client to the GC -message Input { - // A change to be enqueued into the GC engine - optional Change change = 1; - // Reset the match - optional bool reset_match = 2; - // An updated config delta - optional Config config_delta = 3; - // Continue with action - optional ContinueAction continue_action = 4; -} diff --git a/ssl_league_protobufs/proto/ssl_gc_change.proto b/ssl_league_protobufs/proto/ssl_gc_change.proto deleted file mode 100644 index 35c4c30..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_change.proto +++ /dev/null @@ -1,200 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/statemachine"; - -import "ssl_gc_state.proto"; -import "ssl_gc_common.proto"; -import "ssl_gc_geometry.proto"; -import "ssl_gc_game_event.proto"; -import "ssl_gc_referee_message.proto"; - -import "google/protobuf/timestamp.proto"; -import "google/protobuf/wrappers.proto"; - -// A state change -message StateChange { - // A unique increasing id - optional int32 id = 1; - // The previous state - optional State state_pre = 2; - // The state after the change was applied - optional State state = 3; - // The change itself - optional Change change = 4; - // The timestamp when the change was triggered - optional google.protobuf.Timestamp timestamp = 5; -} - -// A certain change -message Change { - // An identifier of the origin that triggered the change - optional string origin = 1; - // Is this change revertible? - optional bool revertible = 16; - - oneof change { - NewCommand new_command_change = 2; - ChangeStage change_stage_change = 3; - SetBallPlacementPos set_ball_placement_pos_change = 4; - AddYellowCard add_yellow_card_change = 5; - AddRedCard add_red_card_change = 6; - YellowCardOver yellow_card_over_change = 7; - AddGameEvent add_game_event_change = 8; - AddPassiveGameEvent add_passive_game_event_change = 19; - AddProposal add_proposal_change = 9; - UpdateConfig update_config_change = 12; - UpdateTeamState update_team_state_change = 13; - SwitchColors switch_colors_change = 14; - Revert revert_change = 15; - NewGameState new_game_state_change = 17; - AcceptProposalGroup accept_proposal_group_change = 18; - SetStatusMessage set_status_message_change = 20; - } - - // New referee command - message NewCommand { - // The command - optional Command command = 1; - } - - // Switch to a new stage - message ChangeStage { - // The new stage - optional Referee.Stage new_stage = 1; - } - - // Set the ball placement pos - message SetBallPlacementPos { - // The position in [m] - optional Vector2 pos = 1; - } - - // Add a new yellow card - message AddYellowCard { - // The team that the card is for - optional Team for_team = 1; - // The game event that caused the card - optional GameEvent caused_by_game_event = 2; - } - - // Add a new red card - message AddRedCard { - // The team that the card is for - optional Team for_team = 1; - // The game event that caused the card - optional GameEvent caused_by_game_event = 2; - } - - // Trigger when a yellow card timed out - message YellowCardOver { - // The team that the card was for - optional Team for_team = 1; - } - - // Add a new game event - message AddGameEvent { - // The game event - optional GameEvent game_event = 1; - } - - // Add a new passive game event (that is only logged, but does not automatically trigger anything) - message AddPassiveGameEvent { - // The game event - optional GameEvent game_event = 1; - } - - // Add a new proposal (i.e. from an auto referee for majority voting) - message AddProposal { - // The proposal - optional Proposal proposal = 1; - } - - // Accept a proposal group (that contain one or more proposals of the same type) - message AcceptProposalGroup { - // The id of the group - optional string group_id = 3; - // An identifier of the acceptor - optional string accepted_by = 2; - } - - // Update some configuration - message UpdateConfig { - // The division to play with - optional Division division = 1; - // the team that does/did the first kick off - optional Team first_kickoff_team = 2; - reserved 3; // auto_continue moved to gcState - // The match type - optional MatchType match_type = 4; - // The number of robots per team - optional google.protobuf.Int32Value max_robots_per_team = 5; - } - - // Update the current state of a team (all fields that should be updated are set) - message UpdateTeamState { - // The team - optional Team for_team = 1; - - // Change the name of the team - optional google.protobuf.StringValue team_name = 2; - // Change the number of goals that the teams has at the moment - optional google.protobuf.Int32Value goals = 3; - // The id of the goal keeper - optional google.protobuf.Int32Value goalkeeper = 4; - // The number of timeouts that the team has left - optional google.protobuf.Int32Value timeouts_left = 5; - // The timeout time that the team has left - optional google.protobuf.StringValue timeout_time_left = 6; - // Does the team play on the positive or the negative half (in ssl-vision coordinates)? - optional google.protobuf.BoolValue on_positive_half = 7; - // The number of ball placement failures - optional google.protobuf.Int32Value ball_placement_failures = 8; - // Can the team place the ball, or is ball placement for this team disabled and should be skipped? - optional google.protobuf.BoolValue can_place_ball = 9; - // The number of challenge flags that the team has left - optional google.protobuf.Int32Value challenge_flags_left = 21; - // The number of bot substitutions left by the team in this halftime - optional google.protobuf.Int32Value bot_substitutions_left = 22; - // Does the team want to substitute a robot in the next possible situation? - optional google.protobuf.BoolValue requests_bot_substitution = 10; - // Does the team want to take a timeout in the next possible situation? - optional google.protobuf.BoolValue requests_timeout = 17; - // Does the team want to challenge a recent decision of the referee? - optional google.protobuf.BoolValue requests_challenge = 18; - // Does the team want to request an emergency stop? - optional google.protobuf.BoolValue requests_emergency_stop = 19; - // Update a certain yellow card of the team - optional YellowCard yellow_card = 20; - // Update a certain red card of the team - optional RedCard red_card = 12; - // Update a certain foul of the team - optional Foul foul = 13; - // Remove the yellow card with this id - optional google.protobuf.UInt32Value remove_yellow_card = 14; - // Remove the red card with this id - optional google.protobuf.UInt32Value remove_red_card = 15; - // Remove the foul with this id - optional google.protobuf.UInt32Value remove_foul = 16; - } - - // Switch the team colors - message SwitchColors { - } - - // Revert a certain change - message Revert { - // The id of the change - optional int32 change_id = 1; - } - - // Change the current game state - message NewGameState { - // The new game state - optional GameState game_state = 1; - } - - message SetStatusMessage { - // The new status message - optional string status_message = 1; - } -} diff --git a/ssl_league_protobufs/proto/ssl_gc_ci.proto b/ssl_league_protobufs/proto/ssl_gc_ci.proto deleted file mode 100644 index 0eddc73..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_ci.proto +++ /dev/null @@ -1,26 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/ci"; - -import "ssl_vision_wrapper_tracked.proto"; -import "ssl_gc_api.proto"; -import "ssl_gc_referee_message.proto"; -import "ssl_vision_geometry.proto"; - -// The input format to the GC -message CiInput { - // New unix timestamp in [ns] for the GC - optional int64 timestamp = 1; - // New tracker packet with ball and robot data - optional TrackerWrapperPacket tracker_packet = 2; - // (UI) API input - repeated Input api_inputs = 3; - // Update geometry - optional SSL_GeometryData geometry = 4; -} - -// The output format of the GC response -message CiOutput { - // Latest referee message - optional Referee referee_msg = 1; -} diff --git a/ssl_league_protobufs/proto/ssl_gc_common.proto b/ssl_league_protobufs/proto/ssl_gc_common.proto deleted file mode 100644 index 796e2d3..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_common.proto +++ /dev/null @@ -1,28 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/state"; - -// Team is either blue or yellow -enum Team { - // team not set - UNKNOWN = 0; - // yellow team - YELLOW = 1; - // blue team - BLUE = 2; -} - -// RobotId is the combination of a team and a robot id -message RobotId { - // the robot number - optional uint32 id = 1; - // the team that the robot belongs to - optional Team team = 2; -} - -// Division denotes the current division, which influences some rules -enum Division { - DIV_UNKNOWN = 0; - DIV_A = 1; - DIV_B = 2; -} diff --git a/ssl_league_protobufs/proto/ssl_gc_engine.proto b/ssl_league_protobufs/proto/ssl_gc_engine.proto deleted file mode 100644 index 6e58206..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_engine.proto +++ /dev/null @@ -1,150 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/engine"; - -import "ssl_gc_geometry.proto"; -import "ssl_gc_common.proto"; - -import "google/protobuf/timestamp.proto"; - -// The GC state contains settings and state independent of the match state -message GcState { - // the state of each team - map team_state = 1; - - // the states of the auto referees - map auto_ref_state = 2; - - // the attached trackers (uuid -> source_name) - map trackers = 3; - - // the next actions that can be executed when continuing - repeated ContinueAction continue_actions = 4; - - // the next actions that can be executed when continuing - repeated ContinueHint continue_hints = 5; -} - -// The GC state for a single team -message GcStateTeam { - // true: The team is connected - optional bool connected = 1; - - // true: The team connected via TLS with a verified certificate - optional bool connection_verified = 2; - - // true: The remote control for the team is connected - optional bool remote_control_connected = 3; - - // true: The remote control for the team connected via TLS with a verified certificate - optional bool remote_control_connection_verified = 4; - - // the advantage choice of the team - optional TeamAdvantageChoice advantage_choice = 5; -} - -// The choice from a team regarding the advantage rule -message TeamAdvantageChoice { - // the choice of the team - optional AdvantageChoice choice = 1; - - // possible advantage choices - enum AdvantageChoice { - // stop the game - STOP = 0; - // keep the match running - CONTINUE = 1; - } -} - -// The GC state of an auto referee -message GcStateAutoRef { - // true: The autoRef connected via TLS with a verified certificate - optional bool connection_verified = 1; -} - -// GC state of a tracker -message GcStateTracker { - // Name of the source - optional string source_name = 1; - - // UUID of the source - optional string uuid = 4; - - // Current ball - optional Ball ball = 2; - - // Current robots - repeated Robot robots = 3; -} - -// The ball state -message Ball { - // ball position [m] - optional Vector3 pos = 1; - - // ball velocity [m/s] - optional Vector3 vel = 2; -} - -// The robot state -message Robot { - // robot id and team - optional RobotId id = 1; - - // robot position [m] - optional Vector2 pos = 2; -} - -message ContinueAction { - // type of action that will be performed next - required Type type = 1; - - // for which team (if team specific) - required Team for_team = 2; - - // list of issues that hinders the game from continuing - repeated string continuation_issues = 3; - - // timestamp at which the action will be ready (to give some preparation time) - optional google.protobuf.Timestamp ready_at = 4; - - // state of the action - optional State state = 5; - - enum Type { - TYPE_UNKNOWN = 0; - HALT = 1; - RESUME_FROM_HALT = 10; - STOP_GAME = 2; - FORCE_START = 11; - FREE_KICK = 17; - NEXT_COMMAND = 3; - BALL_PLACEMENT_START = 4; - BALL_PLACEMENT_CANCEL = 9; - BALL_PLACEMENT_COMPLETE = 14; - BALL_PLACEMENT_FAIL = 15; - TIMEOUT_START = 5; - TIMEOUT_STOP = 6; - BOT_SUBSTITUTION = 7; - NEXT_STAGE = 8; - END_GAME = 16; - ACCEPT_GOAL = 12; - NORMAL_START = 13; - CHALLENGE_ACCEPT = 18; - CHALLENGE_REJECT = 19; - } - - enum State { - STATE_UNKNOWN = 0; - BLOCKED = 1; - WAITING = 2; - READY_AUTO = 3; - READY_MANUAL = 4; - DISABLED = 5; - } -} - -message ContinueHint { - required string message = 1; -} diff --git a/ssl_league_protobufs/proto/ssl_gc_engine_config.proto b/ssl_league_protobufs/proto/ssl_gc_engine_config.proto deleted file mode 100644 index b523840..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_engine_config.proto +++ /dev/null @@ -1,55 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/engine"; - -// The engine config -message Config { - // The behavior for each game event - map game_event_behavior = 1; - - // The config for each auto referee - map auto_ref_configs = 2; - - // The selected tracker source - optional string active_tracker_source = 3; - - // The list of available teams - repeated string teams = 4; - - // Enable or disable auto continuation - optional bool auto_continue = 5; - - // Behaviors for each game event - enum Behavior { - // Not set or unknown - BEHAVIOR_UNKNOWN = 0; - // Always accept the game event - BEHAVIOR_ACCEPT = 1; - // Accept the game event if was reported by a majority - BEHAVIOR_ACCEPT_MAJORITY = 2; - // Only propose the game event (can be accepted in the UI by a human) - BEHAVIOR_PROPOSE_ONLY = 3; - // Only log the game event to the protocol - BEHAVIOR_LOG = 4; - // Silently ignore the game event - BEHAVIOR_IGNORE = 5; - } -} - -// The config for an auto referee -message AutoRefConfig { - // The game event behaviors for this auto referee - map game_event_behavior = 1; - - // Behaviors for the game events reported by this auto referee - enum Behavior { - // Not set or unknown - BEHAVIOR_UNKNOWN = 0; - // Accept the game event - BEHAVIOR_ACCEPT = 1; - // Log the game event - BEHAVIOR_LOG = 2; - // Silently ignore the game event - BEHAVIOR_IGNORE = 3; - } -} diff --git a/ssl_league_protobufs/proto/ssl_gc_game_event.proto b/ssl_league_protobufs/proto/ssl_gc_game_event.proto deleted file mode 100644 index 78b68cc..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_game_event.proto +++ /dev/null @@ -1,596 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/state"; - -import "ssl_gc_common.proto"; -import "ssl_gc_geometry.proto"; - -// GameEvent contains exactly one game event -// Each game event has optional and required fields. The required fields are mandatory to process the event. -// Some optional fields are only used for visualization, others are required to determine the ball placement position. -// If fields are missing that are required for the ball placement position, no ball placement command will be issued. -// Fields are marked optional to make testing and extending of the protocol easier. -// An autoRef should ideally set all fields, except if there are good reasons to not do so. -message GameEvent { - - // A globally unique id of the game event. - optional string id = 50; - - // The type of the game event. - optional Type type = 40; - - // The origins of this game event. - // Empty, if it originates from game controller. - // Contains autoRef name(s), if it originates from one or more autoRefs. - // Ignored if sent by autoRef to game controller. - repeated string origin = 41; - - // Unix timestamp in microseconds when the event was created. - optional uint64 created_timestamp = 49; - - // the event that occurred - oneof event { - - // Ball out of field events (stopping) - - BallLeftField ball_left_field_touch_line = 6; - BallLeftField ball_left_field_goal_line = 7; - AimlessKick aimless_kick = 11; - - // Stopping Fouls - - AttackerTooCloseToDefenseArea attacker_too_close_to_defense_area = 19; - DefenderInDefenseArea defender_in_defense_area = 31; - BoundaryCrossing boundary_crossing = 43; - KeeperHeldBall keeper_held_ball = 13; - BotDribbledBallTooFar bot_dribbled_ball_too_far = 17; - - BotPushedBot bot_pushed_bot = 24; - BotHeldBallDeliberately bot_held_ball_deliberately = 26; - BotTippedOver bot_tipped_over = 27; - BotDroppedParts bot_dropped_parts = 51; - - // Non-Stopping Fouls - - AttackerTouchedBallInDefenseArea attacker_touched_ball_in_defense_area = 15; - BotKickedBallTooFast bot_kicked_ball_too_fast = 18; - BotCrashUnique bot_crash_unique = 22; - BotCrashDrawn bot_crash_drawn = 21; - - // Fouls while ball out of play - - DefenderTooCloseToKickPoint defender_too_close_to_kick_point = 29; - BotTooFastInStop bot_too_fast_in_stop = 28; - BotInterferedPlacement bot_interfered_placement = 20; - - // Scoring goals - - Goal possible_goal = 39; - Goal goal = 8; - Goal invalid_goal = 44; - - // Other events - - AttackerDoubleTouchedBall attacker_double_touched_ball = 14; - PlacementSucceeded placement_succeeded = 5; - PenaltyKickFailed penalty_kick_failed = 45; - - NoProgressInGame no_progress_in_game = 2; - PlacementFailed placement_failed = 3; - MultipleCards multiple_cards = 32; - MultipleFouls multiple_fouls = 34; - BotSubstitution bot_substitution = 37; - ExcessiveBotSubstitution excessive_bot_substitution = 52; - TooManyRobots too_many_robots = 38; - ChallengeFlag challenge_flag = 46; - ChallengeFlagHandled challenge_flag_handled = 48; - EmergencyStop emergency_stop = 47; - - UnsportingBehaviorMinor unsporting_behavior_minor = 35; - UnsportingBehaviorMajor unsporting_behavior_major = 36; - - // Deprecated events - - // replaced by ready_to_continue flag - Prepared prepared = 1 [deprecated = true]; - // obsolete - IndirectGoal indirect_goal = 9 [deprecated = true]; - // replaced by the meta-information in the possible_goal event - ChippedGoal chipped_goal = 10 [deprecated = true]; - // obsolete - KickTimeout kick_timeout = 12 [deprecated = true]; - // rule removed - AttackerTouchedOpponentInDefenseArea attacker_touched_opponent_in_defense_area = 16 [deprecated = true]; - // obsolete - AttackerTouchedOpponentInDefenseArea attacker_touched_opponent_in_defense_area_skipped = 42 [deprecated = true]; - // obsolete - BotCrashUnique bot_crash_unique_skipped = 23 [deprecated = true]; - // can not be used as long as autoRefs do not judge pushing - BotPushedBot bot_pushed_bot_skipped = 25 [deprecated = true]; - // rule removed - DefenderInDefenseAreaPartially defender_in_defense_area_partially = 30 [deprecated = true]; - // the referee msg already indicates this - MultiplePlacementFailures multiple_placement_failures = 33 [deprecated = true]; - } - - // the ball left the field normally - message BallLeftField { - // the team that last touched the ball - required Team by_team = 1; - // the bot that last touched the ball - optional uint32 by_bot = 2; - // the location where the ball left the field [m] - optional Vector2 location = 3; - } - // the ball left the field via goal line and a team committed an aimless kick - message AimlessKick { - // the team that last touched the ball - required Team by_team = 1; - // the bot that last touched the ball - optional uint32 by_bot = 2; - // the location where the ball left the field [m] - optional Vector2 location = 3; - // the location where the ball was last touched [m] - optional Vector2 kick_location = 4; - } - // a team shot a goal - message Goal { - // the team that scored the goal - required Team by_team = 1; - // the team that shot the goal (different from by_team for own goals) - optional Team kicking_team = 6; - // the bot that shot the goal - optional uint32 kicking_bot = 2; - // the location where the ball entered the goal [m] - optional Vector2 location = 3; - // the location where the ball was kicked (for deciding if this was a valid goal) [m] - optional Vector2 kick_location = 4; - // the maximum height the ball reached during the goal kick (for deciding if this was a valid goal) [m] - optional float max_ball_height = 5; - // number of robots of scoring team when the ball entered the goal (for deciding if this was a valid goal) - optional uint32 num_robots_by_team = 7; - // The UNIX timestamp [μs] when the scoring team last touched the ball - optional uint64 last_touch_by_team = 8; - // An additional message with e.g. a reason for invalid goals - optional string message = 9; - } - // the ball entered the goal directly during an indirect free kick - message IndirectGoal { - // the team that tried to shoot the goal - required Team by_team = 1; - // the bot that kicked the ball - at least the team must be set - optional uint32 by_bot = 2; - // the location where the ball entered the goal [m] - optional Vector2 location = 3; - // the location where the ball was kicked [m] - optional Vector2 kick_location = 4; - } - // the ball entered the goal, but was initially chipped - message ChippedGoal { - // the team that tried to shoot the goal - required Team by_team = 1; - // the bot that kicked the ball - optional uint32 by_bot = 2; - // the location where the ball entered the goal [m] - optional Vector2 location = 3; - // the location where the ball was kicked [m] - optional Vector2 kick_location = 4; - // the maximum height [m] of the ball, before it entered the goal and since the last kick [m] - optional float max_ball_height = 5; - } - // a bot moved too fast while the game was stopped - message BotTooFastInStop { - // the team that found guilty - required Team by_team = 1; - // the bot that was too fast - optional uint32 by_bot = 2; - // the location of the bot [m] - optional Vector2 location = 3; - // the bot speed [m/s] - optional float speed = 4; - } - // a bot of the defending team got too close to the kick point during a free kick - message DefenderTooCloseToKickPoint { - // the team that was found guilty - required Team by_team = 1; - // the bot that violates the distance to the kick point - optional uint32 by_bot = 2; - // the location of the bot [m] - optional Vector2 location = 3; - // the distance [m] from bot to the kick point (including the minimum radius) - optional float distance = 4; - } - // two robots crashed into each other with similar speeds - message BotCrashDrawn { - // the bot of the yellow team - optional uint32 bot_yellow = 1; - // the bot of the blue team - optional uint32 bot_blue = 2; - // the location of the crash (center between both bots) [m] - optional Vector2 location = 3; - // the calculated crash speed [m/s] of the two bots - optional float crash_speed = 4; - // the difference [m/s] of the velocity of the two bots - optional float speed_diff = 5; - // the angle [rad] in the range [0, π] of the bot velocity vectors - // an angle of 0 rad ( 0°) means, the bots barely touched each other - // an angle of π rad (180°) means, the bots crashed frontal into each other - optional float crash_angle = 6; - } - // two robots crashed into each other and one team was found guilty to due significant speed difference - message BotCrashUnique { - // the team that caused the crash - required Team by_team = 1; - // the bot that caused the crash - optional uint32 violator = 2; - // the bot of the opposite team that was involved in the crash - optional uint32 victim = 3; - // the location of the crash (center between both bots) [m] - optional Vector2 location = 4; - // the calculated crash speed vector [m/s] of the two bots - optional float crash_speed = 5; - // the difference [m/s] of the velocity of the two bots - optional float speed_diff = 6; - // the angle [rad] in the range [0, π] of the bot velocity vectors - // an angle of 0 rad ( 0°) means, the bots barely touched each other - // an angle of π rad (180°) means, the bots crashed frontal into each other - optional float crash_angle = 7; - } - // a bot pushed another bot over a significant distance - message BotPushedBot { - // the team that pushed the other team - required Team by_team = 1; - // the bot that pushed the other bot - optional uint32 violator = 2; - // the bot of the opposite team that was pushed - optional uint32 victim = 3; - // the location of the push (center between both bots) [m] - optional Vector2 location = 4; - // the pushed distance [m] - optional float pushed_distance = 5; - } - // a bot tipped over - message BotTippedOver { - // the team that found guilty - required Team by_team = 1; - // the bot that tipped over - optional uint32 by_bot = 2; - // the location of the bot [m] - optional Vector2 location = 3; - // the location of the ball at the moment when this foul occurred [m] - optional Vector2 ball_location = 4; - } - // a bot dropped parts - message BotDroppedParts { - // the team that found guilty - required Team by_team = 1; - // the bot that dropped the parts - optional uint32 by_bot = 2; - // the location where the parts were dropped [m] - optional Vector2 location = 3; - // the location of the ball at the moment when this foul occurred [m] - optional Vector2 ball_location = 4; - } - // a defender other than the keeper was fully located inside its own defense and touched the ball - message DefenderInDefenseArea { - // the team that found guilty - required Team by_team = 1; - // the bot that is inside the penalty area - optional uint32 by_bot = 2; - // the location of the bot [m] - optional Vector2 location = 3; - // the distance [m] from bot case to the nearest point outside the defense area - optional float distance = 4; - } - // a defender other than the keeper was partially located inside its own defense area and touched the ball - message DefenderInDefenseAreaPartially { - // the team that found guilty - required Team by_team = 1; - // the bot that is partially inside the penalty area - optional uint32 by_bot = 2; - // the location of the bot - optional Vector2 location = 3; - // the distance [m] that the bot is inside the penalty area - optional float distance = 4; - // the location of the ball at the moment when this foul occurred [m] - optional Vector2 ball_location = 5; - } - // an attacker touched the ball inside the opponent defense area - message AttackerTouchedBallInDefenseArea { - // the team that found guilty - required Team by_team = 1; - // the bot that is inside the penalty area - optional uint32 by_bot = 2; - // the location of the bot [m] - optional Vector2 location = 3; - // the distance [m] that the bot is inside the penalty area - optional float distance = 4; - } - // a bot kicked the ball too fast - message BotKickedBallTooFast { - // the team that found guilty - required Team by_team = 1; - // the bot that kicked too fast - optional uint32 by_bot = 2; - // the location of the ball at the time of the highest speed [m] - optional Vector2 location = 3; - // the absolute initial ball speed (kick speed) [m/s] - optional float initial_ball_speed = 4; - // was the ball chipped? - optional bool chipped = 5; - } - // a bot dribbled to ball too far - message BotDribbledBallTooFar { - // the team that found guilty - required Team by_team = 1; - // the bot that dribbled too far - optional uint32 by_bot = 2; - // the location where the dribbling started [m] - optional Vector2 start = 3; - // the location where the maximum dribbling distance was reached [m] - optional Vector2 end = 4; - } - // an attacker touched the opponent robot inside defense area - message AttackerTouchedOpponentInDefenseArea { - // the team that found guilty - required Team by_team = 1; - // the bot that touched the opponent robot - optional uint32 by_bot = 2; - // the bot of the opposite team that was touched - optional uint32 victim = 4; - // the location of the contact point between both bots [m] - optional Vector2 location = 3; - } - // an attacker touched the ball multiple times when it was not allowed to - message AttackerDoubleTouchedBall { - // the team that found guilty - required Team by_team = 1; - // the bot that touched the ball twice - optional uint32 by_bot = 2; - // the location of the ball when it was first touched [m] - optional Vector2 location = 3; - } - // an attacker was located too near to the opponent defense area during stop or free kick - message AttackerTooCloseToDefenseArea { - // the team that found guilty - required Team by_team = 1; - // the bot that is too close to the defense area - optional uint32 by_bot = 2; - // the location of the bot [m] - optional Vector2 location = 3; - // the distance [m] of the bot to the penalty area - optional float distance = 4; - // the location of the ball at the moment when this foul occurred [m] - optional Vector2 ball_location = 5; - } - // a bot held the ball for too long - message BotHeldBallDeliberately { - // the team that found guilty - required Team by_team = 1; - // the bot that holds the ball - optional uint32 by_bot = 2; - // the location of the ball [m] - optional Vector2 location = 3; - // the duration [s] that the bot hold the ball - optional float duration = 4; - } - // a bot interfered the ball placement of the other team - message BotInterferedPlacement { - // the team that found guilty - required Team by_team = 1; - // the bot that interfered the placement - optional uint32 by_bot = 2; - // the location of the bot [m] - optional Vector2 location = 3; - } - // a team collected multiple yellow cards - message MultipleCards { - // the team that received multiple yellow cards - required Team by_team = 1; - } - // a team collected multiple fouls, which results in a yellow card - message MultipleFouls { - // the team that collected multiple fouls - required Team by_team = 1; - // the list of game events that caused the multiple fouls - repeated GameEvent caused_game_events = 2; - } - // a team failed to place the ball multiple times in a row - message MultiplePlacementFailures { - // the team that failed multiple times - required Team by_team = 1; - } - // timeout waiting for the attacking team to perform the free kick - message KickTimeout { - // the team that that should have kicked - required Team by_team = 1; - // the location of the ball [m] - optional Vector2 location = 2; - // the time [s] that was waited - optional float time = 3; - } - // game was stuck - message NoProgressInGame { - // the location of the ball - optional Vector2 location = 1; - // the time [s] that was waited - optional float time = 2; - } - // ball placement failed - message PlacementFailed { - // the team that failed - required Team by_team = 1; - // the remaining distance [m] from ball to placement position - optional float remaining_distance = 2; - // the distance [m] of the nearest own robot to the ball - optional float nearest_own_bot_distance = 3; - } - // a team was found guilty for minor unsporting behavior - message UnsportingBehaviorMinor { - // the team that found guilty - required Team by_team = 1; - // an explanation of the situation and decision - required string reason = 2; - } - // a team was found guilty for major unsporting behavior - message UnsportingBehaviorMajor { - // the team that found guilty - required Team by_team = 1; - // an explanation of the situation and decision - required string reason = 2; - } - // a keeper held the ball in its defense area for too long - message KeeperHeldBall { - // the team that found guilty - required Team by_team = 1; - // the location of the ball [m] - optional Vector2 location = 2; - // the duration [s] that the keeper hold the ball - optional float duration = 3; - } - // a team successfully placed the ball - message PlacementSucceeded { - // the team that did the placement - required Team by_team = 1; - // the time [s] taken for placing the ball - optional float time_taken = 2; - // the distance [m] between placement location and actual ball position - optional float precision = 3; - // the distance [m] between the initial ball location and the placement position - optional float distance = 4; - } - // both teams are prepared - all conditions are met to continue (with kickoff or penalty kick) - message Prepared { - // the time [s] taken for preparing - optional float time_taken = 1; - } - // bots are being substituted by a team - message BotSubstitution { - // the team that substitutes robots - required Team by_team = 1; - } - // A foul for excessive bot substitutions - message ExcessiveBotSubstitution { - // the team that substitutes robots - required Team by_team = 1; - } - // A challenge flag, requested by a team previously, is flagged - message ChallengeFlag { - // the team that requested the challenge flag - required Team by_team = 1; - } - // A challenge, flagged recently, has been handled by the referee - message ChallengeFlagHandled { - // the team that requested the challenge flag - required Team by_team = 1; - // the challenge was accepted by the referee - required bool accepted = 2; - } - // An emergency stop, requested by team previously, occurred - message EmergencyStop { - // the team that substitutes robots - required Team by_team = 1; - } - // a team has too many robots on the field - message TooManyRobots { - // the team that has too many robots - required Team by_team = 1; - // number of robots allowed at the moment - optional int32 num_robots_allowed = 2; - // number of robots currently on the field - optional int32 num_robots_on_field = 3; - // the location of the ball at the moment when this foul occurred [m] - optional Vector2 ball_location = 4; - } - // a robot chipped the ball over the field boundary out of the playing surface - message BoundaryCrossing { - // the team that has too many robots - required Team by_team = 1; - // the location of the ball [m] - optional Vector2 location = 2; - } - // the penalty kick failed (by time or by keeper) - message PenaltyKickFailed { - // the team that last touched the ball - required Team by_team = 1; - // the location of the ball at the moment of this event [m] - optional Vector2 location = 2; - // an explanation of the failure - optional string reason = 3; - } - - enum Type { - UNKNOWN_GAME_EVENT_TYPE = 0; - - // Ball out of field events (stopping) - - BALL_LEFT_FIELD_TOUCH_LINE = 6; // triggered by autoRef - BALL_LEFT_FIELD_GOAL_LINE = 7; // triggered by autoRef - AIMLESS_KICK = 11; // triggered by autoRef - - // Stopping Fouls - - ATTACKER_TOO_CLOSE_TO_DEFENSE_AREA = 19; // triggered by autoRef - DEFENDER_IN_DEFENSE_AREA = 31; // triggered by autoRef - BOUNDARY_CROSSING = 41; // triggered by autoRef - KEEPER_HELD_BALL = 13; // triggered by GC - BOT_DRIBBLED_BALL_TOO_FAR = 17; // triggered by autoRef - - BOT_PUSHED_BOT = 24; // triggered by human ref - BOT_HELD_BALL_DELIBERATELY = 26; // triggered by human ref - BOT_TIPPED_OVER = 27; // triggered by human ref - BOT_DROPPED_PARTS = 47; // triggered by human ref - - // Non-Stopping Fouls - - ATTACKER_TOUCHED_BALL_IN_DEFENSE_AREA = 15; // triggered by autoRef - BOT_KICKED_BALL_TOO_FAST = 18; // triggered by autoRef - BOT_CRASH_UNIQUE = 22; // triggered by autoRef - BOT_CRASH_DRAWN = 21; // triggered by autoRef - - // Fouls while ball out of play - - DEFENDER_TOO_CLOSE_TO_KICK_POINT = 29; // triggered by autoRef - BOT_TOO_FAST_IN_STOP = 28; // triggered by autoRef - BOT_INTERFERED_PLACEMENT = 20; // triggered by autoRef - EXCESSIVE_BOT_SUBSTITUTION = 48; // triggered by GC - - // Scoring goals - - POSSIBLE_GOAL = 39; // triggered by autoRef - GOAL = 8; // triggered by GC - INVALID_GOAL = 42; // triggered by GC - - // Other events - - ATTACKER_DOUBLE_TOUCHED_BALL = 14; // triggered by autoRef - PLACEMENT_SUCCEEDED = 5; // triggered by autoRef - PENALTY_KICK_FAILED = 43; // triggered by GC and autoRef - - NO_PROGRESS_IN_GAME = 2; // triggered by GC - PLACEMENT_FAILED = 3; // triggered by GC - MULTIPLE_CARDS = 32; // triggered by GC - MULTIPLE_FOULS = 34; // triggered by GC - BOT_SUBSTITUTION = 37; // triggered by GC - TOO_MANY_ROBOTS = 38; // triggered by GC - CHALLENGE_FLAG = 44; // triggered by GC - CHALLENGE_FLAG_HANDLED = 46; // triggered by GC - EMERGENCY_STOP = 45; // triggered by GC - - UNSPORTING_BEHAVIOR_MINOR = 35; // triggered by human ref - UNSPORTING_BEHAVIOR_MAJOR = 36; // triggered by human ref - - // Deprecated events - - PREPARED = 1 [deprecated = true]; - INDIRECT_GOAL = 9 [deprecated = true]; - CHIPPED_GOAL = 10 [deprecated = true]; - KICK_TIMEOUT = 12 [deprecated = true]; - ATTACKER_TOUCHED_OPPONENT_IN_DEFENSE_AREA = 16 [deprecated = true]; - ATTACKER_TOUCHED_OPPONENT_IN_DEFENSE_AREA_SKIPPED = 40 [deprecated = true]; - BOT_CRASH_UNIQUE_SKIPPED = 23 [deprecated = true]; - BOT_PUSHED_BOT_SKIPPED = 25 [deprecated = true]; - DEFENDER_IN_DEFENSE_AREA_PARTIALLY = 30 [deprecated = true]; - MULTIPLE_PLACEMENT_FAILURES = 33 [deprecated = true]; - } -} diff --git a/ssl_league_protobufs/proto/ssl_gc_geometry.proto b/ssl_league_protobufs/proto/ssl_gc_geometry.proto deleted file mode 100644 index 47f04f6..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_geometry.proto +++ /dev/null @@ -1,16 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/geom"; - -// A vector with two dimensions -message Vector2 { - required float x = 1; - required float y = 2; -} - -// A vector with three dimensions -message Vector3 { - required float x = 1; - required float y = 2; - required float z = 3; -} diff --git a/ssl_league_protobufs/proto/ssl_gc_rcon.proto b/ssl_league_protobufs/proto/ssl_gc_rcon.proto deleted file mode 100644 index 91d9b5c..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_rcon.proto +++ /dev/null @@ -1,38 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/rcon"; - -// a reply that is sent by the controller for each request from teams or autoRefs -message ControllerReply { - // status_code is an optional code that indicates the result of the last request - optional StatusCode status_code = 1; - // reason is an optional explanation of the status code - optional string reason = 2; - // next_token must be send with the next request, if secure communication is used - // the token is used to avoid replay attacks - // the token is always present in the very first message before the registration starts - // the token is not present, if secure communication is not used - optional string next_token = 3; - // verification indicates if the last request could be verified (secure communication) - optional Verification verification = 4; - - enum StatusCode { - UNKNOWN_STATUS_CODE = 0; - OK = 1; - REJECTED = 2; - } - - enum Verification { - UNKNOWN_VERIFICATION = 0; - VERIFIED = 1; - UNVERIFIED = 2; - } -} - -// Signature can be added to a request to let it be verfied by the controller -message Signature { - // the token that was received with the last controller reply - required string token = 1; - // the PKCS1v15 signature of this message - required bytes pkcs1v15 = 2; -} diff --git a/ssl_league_protobufs/proto/ssl_gc_rcon_autoref.proto b/ssl_league_protobufs/proto/ssl_gc_rcon_autoref.proto deleted file mode 100644 index 56d2e7a..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_rcon_autoref.proto +++ /dev/null @@ -1,32 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/rcon"; - -import "ssl_gc_game_event.proto"; -import "ssl_gc_rcon.proto"; - -// AutoRefRegistration is the first message that a client must send to the controller to identify itself -message AutoRefRegistration { - // identifier is a unique name of the client - required string identifier = 1; - // signature can optionally be specified to enable secure communication - optional Signature signature = 2; -} - -// AutoRefToController is the wrapper message for all subsequent messages from the autoRef to the controller -message AutoRefToController { - // reserve fields for removed fields - reserved 3, 4; - // signature can optionally be specified to enable secure communication - optional Signature signature = 1; - // game_event is an optional event that the autoRef detected during the game - optional GameEvent game_event = 2; -} - -// ControllerToAutoRef is the wrapper message for all messages from controller to autoRef -message ControllerToAutoRef { - oneof msg { - // a reply from the controller - ControllerReply controller_reply = 1; - } -} \ No newline at end of file diff --git a/ssl_league_protobufs/proto/ssl_gc_rcon_remotecontrol.proto b/ssl_league_protobufs/proto/ssl_gc_rcon_remotecontrol.proto deleted file mode 100644 index 4e2717e..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_rcon_remotecontrol.proto +++ /dev/null @@ -1,117 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/rcon"; - -import "ssl_gc_common.proto"; -import "ssl_gc_rcon.proto"; - -// a registration that must be send by the remote control to the controller as the very first message -message RemoteControlRegistration { - // the team to be controlled - required Team team = 1; - // signature can optionally be specified to enable secure communication - optional Signature signature = 2; -} - -// wrapper for all messages from the remote control to the controller -message RemoteControlToController { - // signature can optionally be specified to enable secure communication - optional Signature signature = 1; - - oneof msg { - // send a ping to the GC to test if the connection is still open. - // the value is ignored and a reply is sent back - Request request = 2; - - // request a new desired keeper id - int32 desired_keeper = 3; - - // true: request to substitute a robot at the next possibility - // false: cancel request - bool request_robot_substitution = 4; - - // true: request a timeout with the next stoppage - // false: cancel the request - bool request_timeout = 5; - - // true: initiate an emergency stop - // false: cancel the request - bool request_emergency_stop = 6; - } - - enum Request { - UNKNOWN = 0; - // Ping the GC to test the connection. The GC will respond with OK and the current team state - PING = 1; - // Raise a challenge flag (this is not revocable) - CHALLENGE_FLAG = 2; - // Stop an ongoing timeout - STOP_TIMEOUT = 3; - } -} - -// wrapper for all messages from controller to a team's computer -message ControllerToRemoteControl { - // a reply from the controller - optional ControllerReply controller_reply = 1; - - // current team state - optional RemoteControlTeamState state = 2; -} - -// Current team state from Controller for remote control -message RemoteControlTeamState { - // the team that is controlled - optional Team team = 12; - - // list of all currently available request types that can be made - repeated RemoteControlRequestType available_requests = 1; - - // list of all currently active request types that are pending - repeated RemoteControlRequestType active_requests = 2; - - // currently set keeper id - optional int32 keeper_id = 3; - - // number of seconds till emergency stop is executed - // zero, if no emergency stop requested - optional float emergency_stop_in = 4; - - // number of timeouts left for the team - optional int32 timeouts_left = 5; - - // number of seconds left for timeout for the team - optional float timeout_time_left = 10; - - // number of challenge flags left for the team - optional int32 challenge_flags_left = 6; - - // max number of robots currently allowed - optional int32 max_robots = 7; - - // current number of robots visible on field - optional int32 robots_on_field = 9; - - // list of due times for each active yellow card (in seconds) - repeated float yellow_cards_due = 8; - - // if true, team is allowed to substitute robots - optional bool can_substitute_robot = 11; - - // number of bot substitutions left by the team in this halftime - optional uint32 bot_substitutions_left = 13; - - // number of seconds left for current bot substitution - optional float bot_substitution_time_left = 14; -} - -// All possible request types that the remote control can make -enum RemoteControlRequestType { - UNKNOWN_REQUEST_TYPE = 0; - EMERGENCY_STOP = 1; - ROBOT_SUBSTITUTION = 2; - TIMEOUT = 3; - CHALLENGE_FLAG = 4; - CHANGE_KEEPER_ID = 5; - STOP_TIMEOUT = 6; -} diff --git a/ssl_league_protobufs/proto/ssl_gc_rcon_team.proto b/ssl_league_protobufs/proto/ssl_gc_rcon_team.proto deleted file mode 100644 index a3f0772..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_rcon_team.proto +++ /dev/null @@ -1,56 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/rcon"; - -import "ssl_gc_rcon.proto"; -import "ssl_gc_common.proto"; - -// a registration that must be send by teams to the controller as the very first message -message TeamRegistration { - // the exact team name as published by the game-controller - required string team_name = 1; - // signature can optionally be specified to enable secure communication - optional Signature signature = 2; - // the team (relevant only if a team plays against itself) - optional Team team = 3; -} - -// wrapper for all messages from a team's computer to the controller -message TeamToController { - // signature can optionally be specified to enable secure communication - optional Signature signature = 1; - - oneof msg { - // request a new desired keeper id - int32 desired_keeper = 2; - // response to an advantage choice request - AdvantageChoice advantage_choice = 3; - // request to substitute a robot at the next possibility - bool substitute_bot = 4; - // send a ping to the GC to test if the connection is still open. - // the value is ignored and a reply is sent back - bool ping = 5; - } -} - -// the current advantage choice of the team -// the choice is valid until another choice is received -// if the team disconnects, the choice is reset to its default (STOP) -// teams may either send their current choice continuously or only on change -enum AdvantageChoice { - // stop the game - STOP = 0; - // keep the game running - CONTINUE = 1; -} - -// wrapper for all messages from controller to a team's computer -message ControllerToTeam { - // reserve obsolete field ids - reserved 2; - - oneof msg { - // a reply from the controller - ControllerReply controller_reply = 1; - } -} diff --git a/ssl_league_protobufs/proto/ssl_gc_referee_message.proto b/ssl_league_protobufs/proto/ssl_gc_referee_message.proto deleted file mode 100644 index d71a4b1..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_referee_message.proto +++ /dev/null @@ -1,238 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/state"; - -import "ssl_gc_game_event.proto"; - -// Each UDP packet contains one of these messages. -message Referee { - // A random UUID of the source that is kept constant at the source while running - // If multiple sources are broadcasting to the same network, this id can be used to identify individual sources - optional string source_identifier = 18; - - // The match type is a meta information about the current match that helps to process the logs after a competition - optional MatchType match_type = 19 [default = UNKNOWN_MATCH]; - - // The UNIX timestamp when the packet was sent, in microseconds. - // Divide by 1,000,000 to get a time_t. - required uint64 packet_timestamp = 1; - - // These are the "coarse" stages of the game. - enum Stage { - // The first half is about to start. - // A kickoff is called within this stage. - // This stage ends with the NORMAL_START. - NORMAL_FIRST_HALF_PRE = 0; - // The first half of the normal game, before half time. - NORMAL_FIRST_HALF = 1; - // Half time between first and second halves. - NORMAL_HALF_TIME = 2; - // The second half is about to start. - // A kickoff is called within this stage. - // This stage ends with the NORMAL_START. - NORMAL_SECOND_HALF_PRE = 3; - // The second half of the normal game, after half time. - NORMAL_SECOND_HALF = 4; - // The break before extra time. - EXTRA_TIME_BREAK = 5; - // The first half of extra time is about to start. - // A kickoff is called within this stage. - // This stage ends with the NORMAL_START. - EXTRA_FIRST_HALF_PRE = 6; - // The first half of extra time. - EXTRA_FIRST_HALF = 7; - // Half time between first and second extra halves. - EXTRA_HALF_TIME = 8; - // The second half of extra time is about to start. - // A kickoff is called within this stage. - // This stage ends with the NORMAL_START. - EXTRA_SECOND_HALF_PRE = 9; - // The second half of extra time. - EXTRA_SECOND_HALF = 10; - // The break before penalty shootout. - PENALTY_SHOOTOUT_BREAK = 11; - // The penalty shootout. - PENALTY_SHOOTOUT = 12; - // The game is over. - POST_GAME = 13; - } - required Stage stage = 2; - - // The number of microseconds left in the stage. - // The following stages have this value; the rest do not: - // NORMAL_FIRST_HALF - // NORMAL_HALF_TIME - // NORMAL_SECOND_HALF - // EXTRA_TIME_BREAK - // EXTRA_FIRST_HALF - // EXTRA_HALF_TIME - // EXTRA_SECOND_HALF - // PENALTY_SHOOTOUT_BREAK - // - // If the stage runs over its specified time, this value - // becomes negative. - optional sint64 stage_time_left = 3; - - // These are the "fine" states of play on the field. - enum Command { - // All robots should completely stop moving. - HALT = 0; - // Robots must keep 50 cm from the ball. - STOP = 1; - // A prepared kickoff or penalty may now be taken. - NORMAL_START = 2; - // The ball is dropped and free for either team. - FORCE_START = 3; - // The yellow team may move into kickoff position. - PREPARE_KICKOFF_YELLOW = 4; - // The blue team may move into kickoff position. - PREPARE_KICKOFF_BLUE = 5; - // The yellow team may move into penalty position. - PREPARE_PENALTY_YELLOW = 6; - // The blue team may move into penalty position. - PREPARE_PENALTY_BLUE = 7; - // The yellow team may take a direct free kick. - DIRECT_FREE_YELLOW = 8; - // The blue team may take a direct free kick. - DIRECT_FREE_BLUE = 9; - // The yellow team may take an indirect free kick. - INDIRECT_FREE_YELLOW = 10 [deprecated = true]; - // The blue team may take an indirect free kick. - INDIRECT_FREE_BLUE = 11 [deprecated = true]; - // The yellow team is currently in a timeout. - TIMEOUT_YELLOW = 12; - // The blue team is currently in a timeout. - TIMEOUT_BLUE = 13; - // The yellow team just scored a goal. - // For information only. - // Deprecated: Use the score field from the team infos instead. That way, you can also detect revoked goals. - GOAL_YELLOW = 14 [deprecated = true]; - // The blue team just scored a goal. See also GOAL_YELLOW. - GOAL_BLUE = 15 [deprecated = true]; - // Equivalent to STOP, but the yellow team must pick up the ball and - // drop it in the Designated Position. - BALL_PLACEMENT_YELLOW = 16; - // Equivalent to STOP, but the blue team must pick up the ball and drop - // it in the Designated Position. - BALL_PLACEMENT_BLUE = 17; - } - required Command command = 4; - - // The number of commands issued since startup (mod 2^32). - required uint32 command_counter = 5; - - // The UNIX timestamp when the command was issued, in microseconds. - // This value changes only when a new command is issued, not on each packet. - required uint64 command_timestamp = 6; - - // Information about a single team. - message TeamInfo { - // The team's name (empty string if operator has not typed anything). - required string name = 1; - // The number of goals scored by the team during normal play and overtime. - required uint32 score = 2; - // The number of red cards issued to the team since the beginning of the game. - required uint32 red_cards = 3; - // The amount of time (in microseconds) left on each yellow card issued to the team. - // If no yellow cards are issued, this array has no elements. - // Otherwise, times are ordered from smallest to largest. - repeated uint32 yellow_card_times = 4 [packed = true]; - // The total number of yellow cards ever issued to the team. - required uint32 yellow_cards = 5; - // The number of timeouts this team can still call. - // If in a timeout right now, that timeout is excluded. - required uint32 timeouts = 6; - // The number of microseconds of timeout this team can use. - required uint32 timeout_time = 7; - // The pattern number of this team's goalkeeper. - required uint32 goalkeeper = 8; - // The total number of countable fouls that act towards yellow cards - optional uint32 foul_counter = 9; - // The number of consecutive ball placement failures of this team - optional uint32 ball_placement_failures = 10; - // Indicate if the team is able and allowed to place the ball - optional bool can_place_ball = 12; - // The maximum number of bots allowed on the field based on division and cards - optional uint32 max_allowed_bots = 13; - // The team has submitted an intent to substitute one or more robots at the next chance - optional bool bot_substitution_intent = 14; - // Indicate if the team reached the maximum allowed ball placement failures and is thus not allowed to place the ball anymore - optional bool ball_placement_failures_reached = 15; - // The team is allowed to substitute one or more robots currently - optional bool bot_substitution_allowed = 16; - // The number of bot substitutions left by the team in this halftime - optional uint32 bot_substitutions_left = 17; - // The number of microseconds left for current bot substitution - optional uint32 bot_substitution_time_left = 18; - } - - // Information about the two teams. - required TeamInfo yellow = 7; - required TeamInfo blue = 8; - - // The coordinates of the Designated Position. These are measured in - // millimetres and correspond to SSL-Vision coordinates. These fields are - // always either both present (in the case of a ball placement command) or - // both absent (in the case of any other command). - message Point { - required float x = 1; - required float y = 2; - } - optional Point designated_position = 9; - - // Information about the direction of play. - // True, if the blue team will have it's goal on the positive x-axis of the ssl-vision coordinate system. - // Obviously, the yellow team will play on the opposite half. - optional bool blue_team_on_positive_half = 10; - - // The game event that caused the referee command. - // deprecated in favor of game_events. - // optional Game_Event game_event = 11 [deprecated = true]; - reserved 11; - - // The command that will be issued after the current stoppage and ball placement to continue the game. - optional Command next_command = 12; - - // All game events that were detected since the last RUNNING state. - // Will be cleared as soon as the game is continued. - reserved 13; - repeated GameEvent game_events = 16; - - // All proposed game events that were detected since the last RUNNING state. - reserved 14; - repeated GameEventProposalGroup game_event_proposals = 17; - - // The time in microseconds that is remaining until the current action times out - // The time will not be reset. It can get negative. - // An autoRef would raise an appropriate event, if the time gets negative. - // Possible actions where this time is relevant: - // * free kicks - // * kickoff, penalty kick, force start - // * ball placement - optional int64 current_action_time_remaining = 15; - - // A message that can be displayed to the spectators, like a reason for a stoppage. - optional string status_message = 20; -} - -// List of matching proposals -message GameEventProposalGroup { - // Unique ID of this group - optional string id = 3; - // The proposed game events - repeated GameEvent game_events = 1; - // Whether the proposal group was accepted - optional bool accepted = 2; -} - -// MatchType is a meta information about the current match for easier log processing -enum MatchType { - // not set - UNKNOWN_MATCH = 0; - // match is part of the group phase - GROUP_PHASE = 1; - // match is part of the elimination phase - ELIMINATION_PHASE = 2; - // a friendly match, not part of a tournament - FRIENDLY = 3; -} diff --git a/ssl_league_protobufs/proto/ssl_gc_state.proto b/ssl_league_protobufs/proto/ssl_gc_state.proto deleted file mode 100644 index 7a2a289..0000000 --- a/ssl_league_protobufs/proto/ssl_gc_state.proto +++ /dev/null @@ -1,133 +0,0 @@ -syntax = "proto2"; - -option go_package = "github.com/RoboCup-SSL/ssl-game-controller/internal/app/state"; - -import "ssl_gc_common.proto"; -import "ssl_gc_geometry.proto"; -import "ssl_gc_game_event.proto"; -import "ssl_gc_referee_message.proto"; - -import "google/protobuf/duration.proto"; -import "google/protobuf/timestamp.proto"; - -message YellowCard { - optional uint32 id = 1; - optional GameEvent caused_by_game_event = 2; - optional google.protobuf.Duration time_remaining = 3; -} - -message RedCard { - optional uint32 id = 1; - optional GameEvent caused_by_game_event = 2; -} - -message Foul { - optional uint32 id = 1; - optional GameEvent caused_by_game_event = 2; - optional google.protobuf.Timestamp timestamp = 3; -} - -message Command { - required Type type = 1; - required Team for_team = 2; - - enum Type { - UNKNOWN = 0; - HALT = 1; - STOP = 2; - NORMAL_START = 3; - FORCE_START = 4; - DIRECT = 5; - reserved 6; // INDIRECT - KICKOFF = 7; - PENALTY = 8; - TIMEOUT = 9; - BALL_PLACEMENT = 10; - } -} - -message GameState { - required Type type = 1; - optional Team for_team = 2; - - enum Type { - UNKNOWN = 0; - HALT = 1; - STOP = 2; - RUNNING = 3; - FREE_KICK = 4; - KICKOFF = 5; - PENALTY = 6; - TIMEOUT = 7; - BALL_PLACEMENT = 8; - } -} - -message Proposal { - // The timestamp when the game event proposal occurred - optional google.protobuf.Timestamp timestamp = 1; - // The proposed game event. - optional GameEvent game_event = 2; -} - -message ProposalGroup { - // Unique ID of this group - optional string id = 4; - // The proposals in this group - repeated Proposal proposals = 1; - // Whether the proposal group was accepted - optional bool accepted = 2; - reserved 3; // uint32 id -} - -message TeamInfo { - optional string name = 1; - optional int32 goals = 2; - optional int32 goalkeeper = 3; - repeated YellowCard yellow_cards = 4; - repeated RedCard red_cards = 5; - optional int32 timeouts_left = 6; - optional google.protobuf.Duration timeout_time_left = 7; - optional bool on_positive_half = 8; - repeated Foul fouls = 9; - optional int32 ball_placement_failures = 10; - optional bool ball_placement_failures_reached = 11; - optional bool can_place_ball = 12; - optional int32 max_allowed_bots = 13; - optional google.protobuf.Timestamp requests_bot_substitution_since = 14; - optional google.protobuf.Timestamp requests_timeout_since = 15; - optional google.protobuf.Timestamp requests_emergency_stop_since = 16; - optional int32 challenge_flags = 17; - optional bool bot_substitution_allowed = 18; - optional int32 bot_substitutions_left = 19; - optional google.protobuf.Duration bot_substitution_time_left = 20; -} - -message State { - optional Referee.Stage stage = 1; - optional Command command = 2; - optional GameState game_state = 19; - optional google.protobuf.Duration stage_time_elapsed = 4; - optional google.protobuf.Duration stage_time_left = 5; - optional google.protobuf.Timestamp match_time_start = 6; - map team_state = 8; - optional Vector2 placement_pos = 9; - optional Command next_command = 10; - optional google.protobuf.Duration current_action_time_remaining = 12; - repeated GameEvent game_events = 13; - repeated ProposalGroup proposal_groups = 14; - optional Division division = 15; - reserved 16; - optional Team first_kickoff_team = 17; - optional MatchType match_type = 18; - optional google.protobuf.Timestamp ready_continue_time = 20; - optional ShootoutState shootout_state = 21; - optional string status_message = 22; - // The maximum number of bots per team (overwrites the division config) - optional int32 max_bots_per_team = 23; -} - -message ShootoutState { - optional Team next_team = 1; - map number_of_attempts = 2; -} diff --git a/ssl_league_protobufs/proto/ssl_simulation_config.proto b/ssl_league_protobufs/proto/ssl_simulation_config.proto deleted file mode 100644 index da8b6ae..0000000 --- a/ssl_league_protobufs/proto/ssl_simulation_config.proto +++ /dev/null @@ -1,77 +0,0 @@ -syntax = "proto2"; -option go_package = "github.com/RoboCup-SSL/ssl-simulation-protocol/pkg/sim"; - -import "ssl_gc_common.proto"; -import "ssl_vision_geometry.proto"; -import "google/protobuf/any.proto"; - -// Movement limits for a robot -message RobotLimits { - // Max absolute speed-up acceleration [m/s^2] - optional float acc_speedup_absolute_max = 1; - // Max angular speed-up acceleration [rad/s^2] - optional float acc_speedup_angular_max = 2; - // Max absolute brake acceleration [m/s^2] - optional float acc_brake_absolute_max = 3; - // Max angular brake acceleration [rad/s^2] - optional float acc_brake_angular_max = 4; - // Max absolute velocity [m/s] - optional float vel_absolute_max = 5; - // Max angular velocity [rad/s] - optional float vel_angular_max = 6; -} - -// Robot wheel angle configuration -// all angles are relative to looking forward, -// all wheels / angles are clockwise -message RobotWheelAngles { - // Angle front right [rad] - required float front_right = 1; - // Angle back right [rad] - required float back_right = 2; - // Angle back left [rad] - required float back_left = 3; - // Angle front left [rad] - required float front_left = 4; -} - -// Specs of a robot -message RobotSpecs { - // Id of the robot - required RobotId id = 1; - // Robot radius [m] - optional float radius = 2 [default = 0.09]; - // Robot height [m] - optional float height = 3 [default = 0.15]; - // Robot mass [kg] - optional float mass = 4; - // Max linear kick speed [m/s] (unset = unlimited) - optional float max_linear_kick_speed = 7; - // Max chip kick speed [m/s] (unset = unlimited) - optional float max_chip_kick_speed = 8; - // Distance from robot center to dribbler [m] (implicitly defines the opening angle and dribbler width) - optional float center_to_dribbler = 9; - // Movement limits - optional RobotLimits limits = 10; - // Wheel angle configuration - optional RobotWheelAngles wheel_angles = 13; - // Custom robot spec for specific simulators (the protobuf files are managed by the simulators) - repeated google.protobuf.Any custom = 14; -} - -message RealismConfig { - // Custom config for specific simulators (the protobuf files are managed by the simulators) - repeated google.protobuf.Any custom = 1; -} - -// Change the simulator configuration -message SimulatorConfig { - // Update the geometry - optional SSL_GeometryData geometry = 1; - // Update the robot specs - repeated RobotSpecs robot_specs = 2; - // Update realism configuration - optional RealismConfig realism_config = 3; - // Change the vision publish port - optional uint32 vision_port = 4; -} \ No newline at end of file diff --git a/ssl_league_protobufs/proto/ssl_simulation_control.proto b/ssl_league_protobufs/proto/ssl_simulation_control.proto deleted file mode 100644 index 55f639e..0000000 --- a/ssl_league_protobufs/proto/ssl_simulation_control.proto +++ /dev/null @@ -1,90 +0,0 @@ -syntax = "proto2"; -option go_package = "github.com/RoboCup-SSL/ssl-simulation-protocol/pkg/sim"; - -import "ssl_gc_common.proto"; -import "ssl_simulation_config.proto"; -import "ssl_simulation_error.proto"; - -// Teleport the ball to a new location and optionally set it to some velocity -message TeleportBall { - // x-coordinate [m] - optional float x = 1; - // y-coordinate [m] - optional float y = 2; - // z-coordinate (height) [m] - optional float z = 3; - // Velocity in x-direction [m/s] - optional float vx = 4; - // Velocity in y-direction [m/s] - optional float vy = 5; - // Velocity in z-direction [m/s] - optional float vz = 6; - // Teleport the ball safely to the target, for example by - // moving robots out of the way in case of collision and set speed of robots close-by to zero - optional bool teleport_safely = 7 [default = false]; - // Adapt the angular ball velocity such that the ball is rolling - optional bool roll = 8 [default = false]; - // Instead of teleporting the ball, apply some force to make sure - // the ball reaches the required position soon (velocity is ignored if true) - // WARNING: A command with by_force stays active (the move will take some time) - // until cancled by another TeleportBall command with by_force = false. - // To avoid teleporting the ball at the end and resetting its current spin, - // do not set any of the optional fields in this message to end the force without triggering - // an additional teleportation - optional bool by_force = 9 [ default = false]; -} - -// Teleport a robot to some location and give it a velocity -message TeleportRobot { - // Robot id to teleport - required RobotId id = 1; - // x-coordinate [m] - optional float x = 2; - // y-coordinate [m] - optional float y = 3; - // Orientation [rad], measured from the x-axis counter-clockwise - optional float orientation = 4; - // Global velocity [m/s] towards x-axis - optional float v_x = 5 [default = 0]; - // Global velocity [m/s] towards y-axis - optional float v_y = 6 [default = 0]; - // Angular velocity [rad/s] - optional float v_angular = 7 [default = 0]; - // Robot should be present on the field? - // true -> robot will be added, if it does not exist yet - // false -> robot will be removed, if it is present - optional bool present = 8; - // Instead of teleporting, apply some force to make sure - // the robot reaches the required position soon (velocity is ignored if true) - // WARNING: A command with by_force stays active (the move will take some time) - // until cancled by another TeleportRobot command for the same bot with by_force = false. - // To avoid teleporting at the end, - // do not set any of the optional fields in this message - // to end the force without triggering - // an additional teleportation - optional bool by_force = 9 [ default = false]; -} - -// Control the simulation -message SimulatorControl { - // Teleport the ball - optional TeleportBall teleport_ball = 1; - // Teleport robots - repeated TeleportRobot teleport_robot = 2; - // Change the simulation speed - optional float simulation_speed = 3; -} - -// Command from the connected client to the simulator -message SimulatorCommand { - // Control the simulation - optional SimulatorControl control = 1; - // Configure the simulation - optional SimulatorConfig config = 2; -} - -// Response of the simulator to the connected client -message SimulatorResponse { - // List of errors, like using unsupported features - repeated SimulatorError errors = 1; -} diff --git a/ssl_league_protobufs/proto/ssl_simulation_error.proto b/ssl_league_protobufs/proto/ssl_simulation_error.proto deleted file mode 100644 index 92c732f..0000000 --- a/ssl_league_protobufs/proto/ssl_simulation_error.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -option go_package = "github.com/RoboCup-SSL/ssl-simulation-protocol/pkg/sim"; - -// Errors in the simulator -message SimulatorError { - // Unique code of the error for automatic handling on client side - optional string code = 1; - // Human readable description of the error - optional string message = 2; -} diff --git a/ssl_league_protobufs/proto/ssl_simulation_robot_control.proto b/ssl_league_protobufs/proto/ssl_simulation_robot_control.proto deleted file mode 100644 index 981b843..0000000 --- a/ssl_league_protobufs/proto/ssl_simulation_robot_control.proto +++ /dev/null @@ -1,66 +0,0 @@ -syntax = "proto2"; -option go_package = "github.com/RoboCup-SSL/ssl-simulation-protocol/pkg/sim"; - -// Full command for a single robot -message RobotCommand { - // Id of the robot - required uint32 id = 1; - // Movement command - optional RobotMoveCommand move_command = 2; - // Absolute (3 dimensional) kick speed [m/s] - optional float kick_speed = 3; - // Kick angle [degree] (defaults to 0 degrees for a straight kick) - optional float kick_angle = 4 [default = 0]; - // Dribbler speed in rounds per minute [rpm] - optional float dribbler_speed = 5; -} - -// Wrapper for different kinds of movement commands -message RobotMoveCommand { - oneof command { - // Move with wheel velocities - MoveWheelVelocity wheel_velocity = 1; - // Move with local velocity - MoveLocalVelocity local_velocity = 2; - // Move with global velocity - MoveGlobalVelocity global_velocity = 3; - } -} - -// Move robot with wheel velocities -message MoveWheelVelocity { - // Velocity [m/s] of front right wheel - required float front_right = 1; - // Velocity [m/s] of back right wheel - required float back_right = 2; - // Velocity [m/s] of back left wheel - required float back_left = 3; - // Velocity [m/s] of front left wheel - required float front_left = 4; -} - -// Move robot with local velocity -message MoveLocalVelocity { - // Velocity forward [m/s] (towards the dribbler) - required float forward = 1; - // Velocity to the left [m/s] - required float left = 2; - // Angular velocity counter-clockwise [rad/s] - required float angular = 3; -} - -// Move robot with global velocity -message MoveGlobalVelocity { - // Velocity on x-axis of the field [m/s] - required float x = 1; - // Velocity on y-axis of the field [m/s] - required float y = 2; - // Angular velocity counter-clockwise [rad/s] - required float angular = 3; -} - -// Command from the connected client to the simulator -message RobotControl { - // Control the robots - repeated RobotCommand robot_commands = 1; -} diff --git a/ssl_league_protobufs/proto/ssl_simulation_robot_feedback.proto b/ssl_league_protobufs/proto/ssl_simulation_robot_feedback.proto deleted file mode 100644 index ddbf214..0000000 --- a/ssl_league_protobufs/proto/ssl_simulation_robot_feedback.proto +++ /dev/null @@ -1,23 +0,0 @@ -syntax = "proto2"; -option go_package = "github.com/RoboCup-SSL/ssl-simulation-protocol/pkg/sim"; - -import "ssl_simulation_error.proto"; -import "google/protobuf/any.proto"; - -// Feedback from a robot -message RobotFeedback { - // Id of the robot - required uint32 id = 1; - // Has the dribbler contact to the ball right now - optional bool dribbler_ball_contact = 2; - // Custom robot feedback for specific simulators (the protobuf files are managed by the simulators) - optional google.protobuf.Any custom = 3; -} - -// Response to RobotControl from the simulator to the connected client -message RobotControlResponse { - // List of errors, like using unsupported features - repeated SimulatorError errors = 1; - // Feedback of the robots - repeated RobotFeedback feedback = 2; -} diff --git a/ssl_league_protobufs/proto/ssl_simulation_synchronous.proto b/ssl_league_protobufs/proto/ssl_simulation_synchronous.proto deleted file mode 100644 index b8dde5d..0000000 --- a/ssl_league_protobufs/proto/ssl_simulation_synchronous.proto +++ /dev/null @@ -1,25 +0,0 @@ -syntax = "proto2"; -option go_package = "github.com/RoboCup-SSL/ssl-simulation-protocol/pkg/sim"; - -import "ssl_vision_detection.proto"; -import "ssl_simulation_robot_feedback.proto"; -import "ssl_simulation_robot_control.proto"; -import "ssl_simulation_control.proto"; - -// Request from the team to the simulator -message SimulationSyncRequest { - // The simulation step [s] to perform - optional float sim_step = 1; - // An optional simulator command - optional SimulatorCommand simulator_command = 2; - // An optional robot control command - optional RobotControl robot_control = 3; -} - -// Response to last SimulationSyncRequest -message SimulationSyncResponse { - // List of detection frames for all cameras with the state after the simulation step in the request was performed - repeated SSL_DetectionFrame detection = 1; - // An optional robot control response - optional RobotControlResponse robot_control_response = 2; -} diff --git a/ssl_league_protobufs/proto/ssl_vision_detection.proto b/ssl_league_protobufs/proto/ssl_vision_detection.proto deleted file mode 100644 index 090bb64..0000000 --- a/ssl_league_protobufs/proto/ssl_vision_detection.proto +++ /dev/null @@ -1,57 +0,0 @@ -syntax = "proto2"; - -message SSL_DetectionBall { - // Confidence in [0-1] of the detection - required float confidence = 1; - optional uint32 area = 2; - // X-coordinate in [mm] in global ssl-vision coordinate system - required float x = 3; - // Y-coordinate in [mm] in global ssl-vision coordinate system - required float y = 4; - // Z-coordinate in [mm] in global ssl-vision coordinate system - // Not supported by ssl-vision, but might be set by simulators - optional float z = 5; - // X-coordinate in [pixel] in the image - required float pixel_x = 6; - // Y-coordinate in [pixel] in the image - required float pixel_y = 7; -} - -message SSL_DetectionRobot { - // Confidence in [0-1] of the detection - required float confidence = 1; - // Id of the robot - optional uint32 robot_id = 2; - // X-coordinate in [mm] in global ssl-vision coordinate system - required float x = 3; - // Y-coordinate in [mm] in global ssl-vision coordinate system - required float y = 4; - // Orientation in [rad] - optional float orientation = 5; - // X-coordinate in [pixel] in the image - required float pixel_x = 6; - // Y-coordinate in [pixel] in the image - required float pixel_y = 7; - // Height, as configured in ssl-vision for the respective team - optional float height = 8; -} - -message SSL_DetectionFrame { - // monotonously increasing frame number - required uint32 frame_number = 1; - // Unix timestamp in [seconds] at which the image has been received by ssl-vision - required double t_capture = 2; - // Unix timestamp in [seconds] at which this message has been sent to the network - required double t_sent = 3; - // Camera timestamp in [seconds] as reported by the camera, if supported - // This is not necessarily a unix timestamp - optional double t_capture_camera = 8; - // Identifier of the camera - required uint32 camera_id = 4; - // Detected balls - repeated SSL_DetectionBall balls = 5; - // Detected yellow robots - repeated SSL_DetectionRobot robots_yellow = 6; - // Detected blue robots - repeated SSL_DetectionRobot robots_blue = 7; -} diff --git a/ssl_league_protobufs/proto/ssl_vision_detection_tracked.proto b/ssl_league_protobufs/proto/ssl_vision_detection_tracked.proto deleted file mode 100644 index 4d73f4d..0000000 --- a/ssl_league_protobufs/proto/ssl_vision_detection_tracked.proto +++ /dev/null @@ -1,88 +0,0 @@ -syntax = "proto2"; - -import "ssl_gc_common.proto"; -import "ssl_gc_geometry.proto"; - -// Default network address: 224.5.23.2:10010 - -// Capabilities that a source implementation can have -enum Capability { - CAPABILITY_UNKNOWN = 0; - CAPABILITY_DETECT_FLYING_BALLS = 1; - CAPABILITY_DETECT_MULTIPLE_BALLS = 2; - CAPABILITY_DETECT_KICKED_BALLS = 3; -} - -// A single tracked ball -message TrackedBall { - // The position (x, y, height) [m] in the ssl-vision coordinate system - required Vector3 pos = 1; - - // The velocity [m/s] in the ssl-vision coordinate system - optional Vector3 vel = 2; - - // The visibility of the ball - // A value between 0 (not visible) and 1 (visible) - // The exact implementation depends on the source software - optional float visibility = 3; -} - -// A ball kicked by a robot, including predictions when the ball will come to a stop -message KickedBall { - // The initial position [m] from which the ball was kicked - required Vector2 pos = 1; - // The initial velocity [m/s] with which the ball was kicked - required Vector3 vel = 2; - // The unix timestamp [s] when the kick was performed - required double start_timestamp = 3; - - // The predicted unix timestamp [s] when the ball comes to a stop - optional double stop_timestamp = 4; - // The predicted position [m] at which the ball will come to a stop - optional Vector2 stop_pos = 5; - - // The robot that kicked the ball - optional RobotId robot_id = 6; -} - -// A single tracked robot -message TrackedRobot { - required RobotId robot_id = 1; - - // The position [m] in the ssl-vision coordinate system - required Vector2 pos = 2; - // The orientation [rad] in the ssl-vision coordinate system - required float orientation = 3; - - // The velocity [m/s] in the ssl-vision coordinate system - optional Vector2 vel = 4; - // The angular velocity [rad/s] in the ssl-vision coordinate system - optional float vel_angular = 5; - - // The visibility of the robot - // A value between 0 (not visible) and 1 (visible) - // The exact implementation depends on the source software - optional float visibility = 6; -} - -// A frame that contains all currently tracked objects on the field on all cameras -message TrackedFrame { - // A monotonous increasing frame counter - required uint32 frame_number = 1; - // The unix timestamp in [s] of the data - required double timestamp = 2; - - // The list of detected balls - // The first ball is the primary one - // Sources may add additional balls based on their capabilities - repeated TrackedBall balls = 3; - // The list of detected robots of both teams - repeated TrackedRobot robots = 4; - - // Information about a kicked ball, if the ball was kicked by a robot and is still moving - // Note: This field is optional. Some source implementations might not set this at any time - optional KickedBall kicked_ball = 5; - - // List of capabilities of the source implementation - repeated Capability capabilities = 6; -} diff --git a/ssl_league_protobufs/proto/ssl_vision_geometry.proto b/ssl_league_protobufs/proto/ssl_vision_geometry.proto deleted file mode 100644 index 3cdc756..0000000 --- a/ssl_league_protobufs/proto/ssl_vision_geometry.proto +++ /dev/null @@ -1,151 +0,0 @@ -syntax = "proto2"; -// A 2D float vector. -message Vector2f { - // X-coordinate in mm - required float x = 1; - // Y-coordinate in mm - required float y = 2; -} - -// Represents a field marking as a line segment represented by a start point p1, -// and end point p2, and a line thickness. The start and end points are along -// the center of the line, so the thickness of the line extends by thickness / 2 -// on either side of the line. -message SSL_FieldLineSegment { - // Name of this field marking. - required string name = 1; - // Start point of the line segment. - required Vector2f p1 = 2; - // End point of the line segment. - required Vector2f p2 = 3; - // Thickness of the line segment. - required float thickness = 4; - // The type of this shape - optional SSL_FieldShapeType type = 5; -} - -// Represents a field marking as a circular arc segment represented by center point, a -// start angle, an end angle, and an arc thickness. -message SSL_FieldCircularArc { - // Name of this field marking. - required string name = 1; - // Center point of the circular arc. - required Vector2f center = 2; - // Radius of the arc. - required float radius = 3; - // Start angle in counter-clockwise order. - required float a1 = 4; - // End angle in counter-clockwise order. - required float a2 = 5; - // Thickness of the arc. - required float thickness = 6; - // The type of this shape - optional SSL_FieldShapeType type = 7; -} - -message SSL_GeometryFieldSize { - // Field length (distance between goal lines) in mm - required int32 field_length = 1; - // Field width (distance between touch lines) in mm - required int32 field_width = 2; - // Goal width (distance between inner edges of goal posts) in mm - required int32 goal_width = 3; - // Goal depth (distance from outer goal line edge to inner goal back) in mm - required int32 goal_depth = 4; - // Boundary width (distance from touch/goal line centers to boundary walls) in mm - required int32 boundary_width = 5; - // Generated line segments based on the other parameters - repeated SSL_FieldLineSegment field_lines = 6; - // Generated circular arcs based on the other parameters - repeated SSL_FieldCircularArc field_arcs = 7; - // Depth of the penalty/defense area (measured between line centers) in mm - optional int32 penalty_area_depth = 8; - // Width of the penalty/defense area (measured between line centers) in mm - optional int32 penalty_area_width = 9; - // Radius of the center circle (measured between line centers) in mm - optional int32 center_circle_radius = 10; - // Thickness/width of the lines on the field in mm - optional int32 line_thickness = 11; - // Distance between the goal center and the center of the penalty mark in mm - optional int32 goal_center_to_penalty_mark = 12; - // Goal height in mm - optional int32 goal_height = 13; - // Ball radius in mm (note that this is a float type to represent sub-mm precision) - optional float ball_radius = 14; - // Max allowed robot radius in mm (note that this is a float type to represent sub-mm precision) - optional float max_robot_radius = 15; -} - -message SSL_GeometryCameraCalibration { - required uint32 camera_id = 1; - required float focal_length = 2; - required float principal_point_x = 3; - required float principal_point_y = 4; - required float distortion = 5; - required float q0 = 6; - required float q1 = 7; - required float q2 = 8; - required float q3 = 9; - required float tx = 10; - required float ty = 11; - required float tz = 12; - optional float derived_camera_world_tx = 13; - optional float derived_camera_world_ty = 14; - optional float derived_camera_world_tz = 15; - optional uint32 pixel_image_width = 16; - optional uint32 pixel_image_height = 17; -} - -// Two-Phase model for straight-kicked balls. -// There are two phases with different accelerations during the ball kicks: -// 1. Sliding -// 2. Rolling -// The full model is described in the TDP of ER-Force from 2016, which can be found here: -// https://ssl.robocup.org/wp-content/uploads/2019/01/2016_ETDP_ER-Force.pdf -message SSL_BallModelStraightTwoPhase { - // Ball sliding acceleration [m/s^2] (should be negative) - required double acc_slide = 1; - // Ball rolling acceleration [m/s^2] (should be negative) - required double acc_roll = 2; - // Fraction of the initial velocity where the ball starts to roll - required double k_switch = 3; -} - -// Fixed-Loss model for chipped balls. -// Uses fixed damping factors for xy and z direction per hop. -message SSL_BallModelChipFixedLoss { - // Chip kick velocity damping factor in XY direction for the first hop - required double damping_xy_first_hop = 1; - // Chip kick velocity damping factor in XY direction for all following hops - required double damping_xy_other_hops = 2; - // Chip kick velocity damping factor in Z direction for all hops - required double damping_z = 3; -} - -message SSL_GeometryModels { - optional SSL_BallModelStraightTwoPhase straight_two_phase = 1; - optional SSL_BallModelChipFixedLoss chip_fixed_loss = 2; -} - -message SSL_GeometryData { - required SSL_GeometryFieldSize field = 1; - repeated SSL_GeometryCameraCalibration calib = 2; - optional SSL_GeometryModels models = 3; -} - -enum SSL_FieldShapeType { - Undefined = 0; - CenterCircle = 1; - TopTouchLine = 2; - BottomTouchLine = 3; - LeftGoalLine = 4; - RightGoalLine = 5; - HalfwayLine = 6; - CenterLine = 7; - LeftPenaltyStretch = 8; - RightPenaltyStretch = 9; - LeftFieldLeftPenaltyStretch = 10; - LeftFieldRightPenaltyStretch = 11; - RightFieldLeftPenaltyStretch = 12; - RightFieldRightPenaltyStretch = 13; -} diff --git a/ssl_league_protobufs/proto/ssl_vision_wrapper.proto b/ssl_league_protobufs/proto/ssl_vision_wrapper.proto deleted file mode 100644 index e8be7fb..0000000 --- a/ssl_league_protobufs/proto/ssl_vision_wrapper.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; - -import "ssl_vision_detection.proto"; -import "ssl_vision_geometry.proto"; - -message SSL_WrapperPacket { - optional SSL_DetectionFrame detection = 1; - optional SSL_GeometryData geometry = 2; -} diff --git a/ssl_league_protobufs/proto/ssl_vision_wrapper_tracked.proto b/ssl_league_protobufs/proto/ssl_vision_wrapper_tracked.proto deleted file mode 100644 index c30c4b1..0000000 --- a/ssl_league_protobufs/proto/ssl_vision_wrapper_tracked.proto +++ /dev/null @@ -1,14 +0,0 @@ -syntax = "proto2"; -import "ssl_vision_detection_tracked.proto"; - -// A wrapper packet containing meta data of the source -// Also serves for the possibility to extend the protocol later -message TrackerWrapperPacket { - // A random UUID of the source that is kept constant at the source while running - // If multiple sources are broadcasting to the same network, this id can be used to identify individual sources - required string uuid = 1; - // The name of the source software that is producing this messages. - optional string source_name = 2; - // The tracked frame - optional TrackedFrame tracked_frame = 3; -} diff --git a/ssl_league_protobufs/ssl-protocol-defs b/ssl_league_protobufs/ssl-protocol-defs new file mode 160000 index 0000000..4d3326f --- /dev/null +++ b/ssl_league_protobufs/ssl-protocol-defs @@ -0,0 +1 @@ +Subproject commit 4d3326fa38c8f62c6fb1dbbb13cc8cea8b39b1ed diff --git a/ssl_ros_bridge/CMakeLists.txt b/ssl_ros_bridge/CMakeLists.txt index d31fb4e..de00696 100644 --- a/ssl_ros_bridge/CMakeLists.txt +++ b/ssl_ros_bridge/CMakeLists.txt @@ -20,23 +20,23 @@ find_package(ssl_ros_bridge_msgs REQUIRED) include(cmake/MsgConversionGen.cmake) -set(_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_protobufs/proto") +set(_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_protobufs/ssl-protocol-defs/proto") set(_SIDECAR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_msgs/cmake/ssl_ros_annotations.json") set(_CONV_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated_conversion") generate_message_conversion( PROTO_FILES - ${_PROTO_DIR}/ssl_gc_common.proto - ${_PROTO_DIR}/ssl_gc_geometry.proto - ${_PROTO_DIR}/ssl_gc_game_event.proto - ${_PROTO_DIR}/ssl_gc_referee_message.proto - ${_PROTO_DIR}/ssl_gc_rcon.proto - ${_PROTO_DIR}/ssl_vision_detection.proto - ${_PROTO_DIR}/ssl_vision_geometry.proto - ${_PROTO_DIR}/ssl_vision_wrapper.proto - ${_PROTO_DIR}/ssl_simulation_config.proto - ${_PROTO_DIR}/ssl_simulation_control.proto - ${_PROTO_DIR}/ssl_simulation_error.proto + ${_PROTO_DIR}/gc/ssl_gc_common.proto + ${_PROTO_DIR}/gc/ssl_gc_geometry.proto + ${_PROTO_DIR}/gc/ssl_gc_game_event.proto + ${_PROTO_DIR}/gc/ssl_gc_referee_message.proto + ${_PROTO_DIR}/gc/ssl_gc_rcon.proto + ${_PROTO_DIR}/vision/ssl_vision_detection.proto + ${_PROTO_DIR}/vision/ssl_vision_geometry.proto + ${_PROTO_DIR}/vision/ssl_vision_wrapper.proto + ${_PROTO_DIR}/simulation/ssl_simulation_config.proto + ${_PROTO_DIR}/simulation/ssl_simulation_control.proto + ${_PROTO_DIR}/simulation/ssl_simulation_error.proto PROTO_PATHS ${_PROTO_DIR} SIDECAR ${_SIDECAR} OUTPUT_DIR ${_CONV_OUT} @@ -54,8 +54,22 @@ install(DIRECTORY ) if(BUILD_TESTING) + # ament_cmake_copyright is excluded from ament_lint_auto's automatic + # per-package hook and registered manually below instead, with an + # explicit EXCLUDE for the two generator scripts (no per-file header, by + # project decision), so the rest of the package keeps full copyright + # enforcement. + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_copyright) + find_package(ament_lint_auto REQUIRED) ament_lint_auto_find_test_dependencies() + + find_package(ament_cmake_copyright REQUIRED) + ament_copyright( + EXCLUDE + cmake/gen_message_conversion.py + cmake/MsgConversionGen.cmake + ) endif() ament_package() diff --git a/ssl_ros_bridge/cmake/gen_message_conversion.py b/ssl_ros_bridge/cmake/gen_message_conversion.py index dd5ad6d..d72560e 100644 --- a/ssl_ros_bridge/cmake/gen_message_conversion.py +++ b/ssl_ros_bridge/cmake/gen_message_conversion.py @@ -2,7 +2,7 @@ r""" Generate C++ fromProto bridge functions. -Reads SSL league proto files + sidecar JSON, emits two files: +Reads SSL league proto files + an annotation JSON file, emits two files: /message_conversion_generated.hpp /message_conversion_generated.cpp @@ -16,12 +16,12 @@ [--ros-package ssl_league_msgs] \ [--cpp-namespace ssl_ros_bridge::message_conversion] -Sidecar annotation format is the same as for protoc_gen_ros2msg.py. +Annotation file format is the same as for protoc_gen_ros2msg.py. Consumed fields (from 'outputs' groups + 'fields' keys) are excluded from passthrough. All other proto fields are emitted using proto2/proto3-appropriate accessor patterns. -Intrinsic conversions (no 'conversion_func' needed in sidecar): +Intrinsic conversions (no 'conversion_func' needed in the annotation file): builtin_interfaces/Time float/double → from_seconds (×1e9 ns cast) int/uint → from_microseconds (×1000 ns cast) builtin_interfaces/Duration same rules @@ -41,16 +41,16 @@ # Must follow the sys.path.insert() above, so this can't sort before the # rosidl_pycommon import the way import-order linting wants. from ateam_proto_shared import ( # noqa: E402, I100 + Annotations, build_map_entry_type_names, - field_shape, + classify_field_shape, + consumed_annotation_fields, FieldAnnotation, HAS_FIELD_PREFIX, iter_messages, - load_sidecar, - MessageSidecarEntry, + load_annotations, + MessageAnnotationEntry, OutputEntry, - Sidecar, - sidecar_consumed, ) FD = descriptor_pb2.FieldDescriptorProto @@ -66,7 +66,7 @@ # ── Name helpers ────────────────────────────────────────────────────────────── -def ros_cpp_type(ros_type: str) -> str: +def to_ros_cpp_type(ros_type: str) -> str: """'geometry_msgs/Point32' → 'geometry_msgs::msg::Point32'.""" if '/' in ros_type: pkg, typ = ros_type.split('/', 1) @@ -74,28 +74,28 @@ def ros_cpp_type(ros_type: str) -> str: return ros_type -def ros_msg_type(flat: str, pkg: str) -> str: +def to_ros_msg_type(flat: str, pkg: str) -> str: return f'{pkg}::msg::{flat}' -def proto_oneof_const(field_name: str) -> str: +def oneof_case_const_name(field_name: str) -> str: """'aimless_kick' → 'kAimlessKick'.""" return 'k' + ''.join(p.capitalize() for p in field_name.split('_')) # ── C++ expression helpers ──────────────────────────────────────────────────── -def scale_lit(val: float) -> str: +def scale_literal(val: float) -> str: if abs(val - 1e-3) < 1e-12: return '1e-3f' return f'{val}f' -def acc(field_name: str) -> str: +def proto_accessor_expr(field_name: str) -> str: return f'proto_msg.{field_name}()' -# ── Sidecar output codegen ──────────────────────────────────────────────────── +# ── Annotation output codegen ───────────────────────────────────────────────── def _emit_component_assignments( target: str, from_map: dict[str, str], scale: float | None, ind: str, @@ -109,16 +109,16 @@ def _emit_component_assignments( """ lines = [] for ros_comp, pf in from_map.items(): - expr = acc(pf) + expr = proto_accessor_expr(pf) if scale: - expr = f'{expr} * {scale_lit(scale)}' + expr = f'{expr} * {scale_literal(scale)}' lines.append(f'{ind}{target}.{ros_comp} = {expr};') return lines -def emit_output(out: OutputEntry, ind: str) -> list[str]: +def emit_annotation_output(out: OutputEntry, ind: str) -> list[str]: """Emit C++ for one 'outputs' entry.""" ros_field = out['ros_field'] ros_type = out['ros_type'] @@ -143,7 +143,7 @@ def emit_output(out: OutputEntry, ind: str) -> list[str]: return [f"{ind}// TODO: unsupported output ros_type '{ros_type}' → '{ros_field}'"] -# ── Sidecar field override codegen ──────────────────────────────────────────── +# ── Annotation field override codegen ───────────────────────────────────────── # (ros_type, is_float_source_field) -> wrapper around a proto accessor # expression producing the intrinsic conversion. int/uint sources are in @@ -161,7 +161,7 @@ def emit_output(out: OutputEntry, ind: str) -> list[str]: } -def emit_field_override( +def emit_annotation_field_override( proto_name: str, ann: FieldAnnotation, field: descriptor_pb2.FieldDescriptorProto, @@ -173,7 +173,7 @@ def emit_field_override( ros_type = ann.get('ros_type') scale = ann.get('scale') - is_rep, _in_oneof, is_p2opt = field_shape(field, proto2) + is_rep, _in_oneof, is_p2opt = classify_field_shape(field, proto2) def wrap_optional(inner: str) -> list[str]: return [ @@ -183,7 +183,8 @@ def wrap_optional(inner: str) -> list[str]: ] if ros_type in INTRINSIC_ROS_TYPES: - expr = _INTRINSIC_WRAPPERS[(ros_type, field.type in FLOAT_TYPES)](acc(proto_name)) + wrapper = _INTRINSIC_WRAPPERS[(ros_type, field.type in FLOAT_TYPES)] + expr = wrapper(proto_accessor_expr(proto_name)) if is_p2opt: lines += wrap_optional(expr) @@ -193,7 +194,7 @@ def wrap_optional(inner: str) -> list[str]: lines.append(f'{ind}ros_msg.{ros_name} = {expr};') elif scale is not None: - expr = f'{acc(proto_name)} * {scale_lit(scale)}' + expr = f'{proto_accessor_expr(proto_name)} * {scale_literal(scale)}' if is_p2opt: lines += wrap_optional(expr) elif is_rep: @@ -202,14 +203,14 @@ def wrap_optional(inner: str) -> list[str]: f'proto_msg.{proto_name}().end(),' ) lines.append(f'{ind} std::back_inserter(ros_msg.{ros_name}),') - lines.append(f'{ind} [](const auto & v) {{ return v * {scale_lit(scale)}; }});') + lines.append(f'{ind} [](const auto & v) {{ return v * {scale_literal(scale)}; }});') else: lines.append(f'{ind}ros_msg.{ros_name} = {expr};') else: # Rename only — same type, different ros field name if field.type == FD.TYPE_MESSAGE: - inner = f'fromProto({acc(proto_name)})' + inner = f'fromProto({proto_accessor_expr(proto_name)})' if is_p2opt: lines += [ f'{ind}if (proto_msg.has_{proto_name}()) {{', @@ -224,16 +225,18 @@ def wrap_optional(inner: str) -> list[str]: lines.append(f'{ind} std::back_inserter(ros_msg.{ros_name}),') lines.append(f'{ind} [](const auto & p) {{ return fromProto(p); }});') else: - lines.append(f'{ind}ros_msg.{ros_name} = fromProto({acc(proto_name)});') + lines.append( + f'{ind}ros_msg.{ros_name} = fromProto({proto_accessor_expr(proto_name)});' + ) elif field.type == FD.TYPE_ENUM: - inner = f'static_cast({acc(proto_name)})' + inner = f'static_cast({proto_accessor_expr(proto_name)})' if is_p2opt: lines += wrap_optional(inner) else: lines.append(f'{ind}ros_msg.{ros_name} = {inner};') else: if is_p2opt: - lines += wrap_optional(acc(proto_name)) + lines += wrap_optional(proto_accessor_expr(proto_name)) elif is_rep: lines.append( f'{ind}std::copy(proto_msg.{proto_name}().begin(), ' @@ -241,7 +244,7 @@ def wrap_optional(inner: str) -> list[str]: ) lines.append(f'{ind} std::back_inserter(ros_msg.{ros_name}));') else: - lines.append(f'{ind}ros_msg.{ros_name} = {acc(proto_name)};') + lines.append(f'{ind}ros_msg.{ros_name} = {proto_accessor_expr(proto_name)};') return lines @@ -255,13 +258,13 @@ def emit_passthrough( ) -> list[str]: lines = [] name = field.name - is_rep, _in_oneof, is_p2opt = field_shape(field, proto2) + is_rep, _in_oneof, is_p2opt = classify_field_shape(field, proto2) if field.type == FD.TYPE_BYTES: if is_p2opt: lines += [ f'{ind}if (proto_msg.has_{name}()) {{', - f'{ind} auto & _b = {acc(name)};', + f'{ind} auto & _b = {proto_accessor_expr(name)};', f'{ind} ros_msg.{name} = {{std::vector(_b.begin(), _b.end())}};', f'{ind}}}', ] @@ -270,7 +273,7 @@ def emit_passthrough( else: lines += [ f'{ind}{{', - f'{ind} auto & _b = {acc(name)};', + f'{ind} auto & _b = {proto_accessor_expr(name)};', f'{ind} ros_msg.{name}.assign(_b.begin(), _b.end());', f'{ind}}}', ] @@ -331,9 +334,9 @@ def emit_passthrough( f'{ind}}}', ] elif field.type == FD.TYPE_ENUM: - lines.append(f'{ind}ros_msg.{name} = static_cast({acc(name)});') + lines.append(f'{ind}ros_msg.{name} = static_cast({proto_accessor_expr(name)});') else: - lines.append(f'{ind}ros_msg.{name} = {acc(name)};') + lines.append(f'{ind}ros_msg.{name} = {proto_accessor_expr(name)};') return lines @@ -357,14 +360,16 @@ def emit_oneof( lines.append(f'{ind}switch (proto_msg.{oneof_name}_case()) {{') for f in arms: - const = f'{cpp_name}::{proto_oneof_const(f.name)}' + const = f'{cpp_name}::{oneof_case_const_name(f.name)}' lines.append(f'{ind} case {const}:') if f.type == FD.TYPE_MESSAGE: lines.append(f'{ind} ros_msg.{f.name} = fromProto(proto_msg.{f.name}());') elif f.type == FD.TYPE_ENUM: - lines.append(f'{ind} ros_msg.{f.name} = static_cast({acc(f.name)});') + lines.append( + f'{ind} ros_msg.{f.name} = static_cast({proto_accessor_expr(f.name)});' + ) else: - lines.append(f'{ind} ros_msg.{f.name} = {acc(f.name)};') + lines.append(f'{ind} ros_msg.{f.name} = {proto_accessor_expr(f.name)};') lines.append(f'{ind} break;') lines.append(f'{ind} default: break;') @@ -400,12 +405,12 @@ def emit_oneof( def generate_header( fds: list[descriptor_pb2.FileDescriptorProto], - sidecar: Sidecar, + annotations: Annotations, ros_pkg: str, proto_prefix: str, namespace: str, ) -> str: - skip_types = frozenset(sidecar.get('_skip_types', [])) + skip_types = frozenset(annotations.get('_skip_types', [])) guard = 'CORE__MESSAGE_CONVERSION_GENERATED_HPP_' lines = [ LICENSE, @@ -458,7 +463,7 @@ def generate_header( for flat, cpp_name, msg in iter_messages(fd): if flat in skip_types: continue - ros_t = ros_msg_type(flat, ros_pkg) + ros_t = to_ros_msg_type(flat, ros_pkg) lines.append(f'{ros_t} fromProto(const {cpp_name} & proto_msg);') lines.append('') @@ -474,7 +479,7 @@ def generate_header( def generate_source( fds: list[descriptor_pb2.FileDescriptorProto], all_map_entries: frozenset[str], - sidecar: Sidecar, + annotations: Annotations, ros_pkg: str, namespace: str, ) -> str: @@ -493,7 +498,7 @@ def generate_source( lines.append('{') lines.append('') - skip_types = frozenset(sidecar.get('_skip_types', [])) + skip_types = frozenset(annotations.get('_skip_types', [])) for fd in fds: proto2 = fd.syntax != 'proto3' @@ -502,27 +507,27 @@ def generate_source( if flat in skip_types: continue - entry: MessageSidecarEntry = sidecar.get(flat, {}) - consumed = sidecar_consumed(entry) + entry: MessageAnnotationEntry = annotations.get(flat, {}) + consumed = consumed_annotation_fields(entry) pf_map: dict[str, descriptor_pb2.FieldDescriptorProto] = {f.name: f for f in msg.field} - ros_t = ros_msg_type(flat, ros_pkg) + ros_t = to_ros_msg_type(flat, ros_pkg) lines.append(f'{ros_t} fromProto(const {cpp_name} & proto_msg)') lines.append('{') lines.append(f' {ros_t} ros_msg;') - # Sidecar outputs (multi-field → ROS struct) + # Annotation outputs (multi-field → ROS struct) for out in entry.get('outputs', []): - lines += emit_output(out, ' ') + lines += emit_annotation_output(out, ' ') - # Sidecar field overrides + # Annotation field overrides for pname, ann in entry.get('fields', {}).items(): if ann.get('skip'): continue pf = pf_map.get(pname) if pf: - lines += emit_field_override(pname, ann, pf, proto2, ' ') + lines += emit_annotation_field_override(pname, ann, pf, proto2, ' ') # Passthrough — skip consumed, error on unskipped map entries emitted_oneofs: set[int] = set() @@ -534,7 +539,7 @@ def generate_source( print( f'{flat}.{field.name}: proto map fields have no ROS2 ' f"equivalent and are not supported. Add a 'skip' field " - f'annotation in the sidecar for this field to drop it explicitly.', + f'annotation in the annotation file for this field to drop it explicitly.', file=sys.stderr, ) sys.exit(1) @@ -605,16 +610,16 @@ def main() -> None: print('No matching proto files found in descriptor.', file=sys.stderr) sys.exit(1) - sidecar = load_sidecar(args.sidecar) + annotations = load_annotations(args.sidecar) all_map_entries = build_map_entry_type_names(fds_pb.file) os.makedirs(args.output_dir, exist_ok=True) hpp = generate_header( - target_fds, sidecar, args.ros_package, args.proto_include_prefix, args.cpp_namespace, + target_fds, annotations, args.ros_package, args.proto_include_prefix, args.cpp_namespace, ) cpp = generate_source( - target_fds, all_map_entries, sidecar, args.ros_package, args.cpp_namespace, + target_fds, all_map_entries, annotations, args.ros_package, args.cpp_namespace, ) hpp_path = os.path.join(args.output_dir, 'message_conversion_generated.hpp') diff --git a/ssl_ros_bridge/package.xml b/ssl_ros_bridge/package.xml index 9c377ca..3aff9bd 100644 --- a/ssl_ros_bridge/package.xml +++ b/ssl_ros_bridge/package.xml @@ -20,7 +20,19 @@ ssl_ros_bridge_msgs ament_lint_auto - ament_lint_common + + ament_cmake_copyright + ament_cmake_cppcheck + ament_cmake_cpplint + ament_cmake_flake8 + ament_cmake_lint_cmake + ament_cmake_pep257 + ament_cmake_uncrustify + ament_cmake_xmllint ament_cmake diff --git a/ssl_ros_bridge/src/log2bag/log_reader.hpp b/ssl_ros_bridge/src/log2bag/log_reader.hpp index 3a6ffa4..52486af 100644 --- a/ssl_ros_bridge/src/log2bag/log_reader.hpp +++ b/ssl_ros_bridge/src/log2bag/log_reader.hpp @@ -21,8 +21,8 @@ #ifndef LOG2BAG__LOG_READER_HPP_ #define LOG2BAG__LOG_READER_HPP_ -#include -#include +#include +#include #include #include #include diff --git a/ssl_ros_bridge/src/team_client/team_client.hpp b/ssl_ros_bridge/src/team_client/team_client.hpp index 0a24deb..d99bc20 100644 --- a/ssl_ros_bridge/src/team_client/team_client.hpp +++ b/ssl_ros_bridge/src/team_client/team_client.hpp @@ -21,7 +21,7 @@ #ifndef TEAM_CLIENT__TEAM_CLIENT_HPP_ #define TEAM_CLIENT__TEAM_CLIENT_HPP_ -#include +#include #include #include #include diff --git a/ssl_ros_bridge/src/vision_bridge/ssl_vision_bridge_node.cpp b/ssl_ros_bridge/src/vision_bridge/ssl_vision_bridge_node.cpp index bd8c3a7..5b508f4 100644 --- a/ssl_ros_bridge/src/vision_bridge/ssl_vision_bridge_node.cpp +++ b/ssl_ros_bridge/src/vision_bridge/ssl_vision_bridge_node.cpp @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#include +#include #include #include From d75219f6f3cbccc329df1b47ca666a4a803b3717 Mon Sep 17 00:00:00 2001 From: Will Stuckey Date: Sat, 15 Aug 2026 22:24:16 -0400 Subject: [PATCH 6/7] rename some files, bug fixes after submodule --- ARCHITECTURE.md | 8 ++-- ssl_league_msgs/CMakeLists.txt | 2 +- ...toGenCommon.cmake => ProtoGenShared.cmake} | 24 +++++----- ssl_league_msgs/cmake/Ros2MsgGen.cmake | 8 ++-- ...{ateam_proto_shared.py => proto_shared.py} | 17 +++++-- ssl_league_msgs/cmake/protoc_gen_ros2msg.py | 23 ++++++++-- .../cmake/ssl_ros_annotations.json | 13 +++++- ...m_proto_shared.py => test_proto_shared.py} | 6 +-- ssl_ros_bridge/cmake/MsgConversionGen.cmake | 12 ++--- .../cmake/gen_message_conversion.py | 44 +++++++++++++++++-- ssl_ros_bridge/src/core/CMakeLists.txt | 12 +++++ 11 files changed, 127 insertions(+), 42 deletions(-) rename ssl_league_msgs/cmake/{AteamProtoGenCommon.cmake => ProtoGenShared.cmake} (59%) rename ssl_league_msgs/cmake/{ateam_proto_shared.py => proto_shared.py} (94%) rename ssl_league_msgs/test/{test_ateam_proto_shared.py => test_proto_shared.py} (97%) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 073bd1a..cc368e4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -69,7 +69,7 @@ flowchart LR fromproto["fromProto() functions"] end - shared["ateam_proto_shared.py\n(shared naming/shape/annotation logic)"] + shared["proto_shared.py\n(shared naming/shape/annotation logic)"] protos --> plugin protos --> convgen @@ -90,7 +90,7 @@ subprocess to get a `FileDescriptorSet`, then walks that with Python's the same protobuf descriptor model; they just get there differently because one has to conform to the protoc plugin ABI and the other doesn't. -`ateam_proto_shared.py` exists so the two generators can't drift on shared +`proto_shared.py` exists so the two generators can't drift on shared concerns: flattening a proto type name into a ROS-legal one, classifying a field's shape (repeated / oneof member / proto2-optional), and parsing the annotation file's `fields`/`outputs`/`_skip_types` structure. @@ -142,7 +142,7 @@ module docstring — that's the canonical reference, not this file. ```mermaid flowchart TD - common["AteamProtoGenCommon.cmake\n(shared: run generator, fail loudly)"] + common["ProtoGenShared.cmake\n(shared: run generator, fail loudly)"] r2m["Ros2MsgGen.cmake\ngenerate_ros2_msgs()"] mcg["MsgConversionGen.cmake\ngenerate_message_conversion()"] chk["CheckGeneratedMsgList.cmake\n(staleness check, cmake -P script)"] @@ -234,7 +234,7 @@ presence of its own. ## Testing `ssl_league_msgs` has a small `pytest` suite (`ssl_league_msgs/test/`) -covering the pure logic in `ateam_proto_shared.py` and +covering the pure logic in `proto_shared.py` and `protoc_gen_ros2msg.py` — name flattening, field-shape classification, annotation-file parsing, and `find_type_cycles()`'s cycle detection — using hand-built `descriptor_pb2` messages rather than real `.proto` files, so diff --git a/ssl_league_msgs/CMakeLists.txt b/ssl_league_msgs/CMakeLists.txt index dc76422..00b9d6a 100644 --- a/ssl_league_msgs/CMakeLists.txt +++ b/ssl_league_msgs/CMakeLists.txt @@ -41,7 +41,7 @@ if(BUILD_TESTING) ament_lint_auto_find_test_dependencies() find_package(ament_cmake_pytest REQUIRED) - ament_add_pytest_test(test_ateam_proto_shared test/test_ateam_proto_shared.py) + ament_add_pytest_test(test_proto_shared test/test_proto_shared.py) ament_add_pytest_test(test_protoc_gen_ros2msg test/test_protoc_gen_ros2msg.py) endif() diff --git a/ssl_league_msgs/cmake/AteamProtoGenCommon.cmake b/ssl_league_msgs/cmake/ProtoGenShared.cmake similarity index 59% rename from ssl_league_msgs/cmake/AteamProtoGenCommon.cmake rename to ssl_league_msgs/cmake/ProtoGenShared.cmake index 536be0e..070bef8 100644 --- a/ssl_league_msgs/cmake/AteamProtoGenCommon.cmake +++ b/ssl_league_msgs/cmake/ProtoGenShared.cmake @@ -1,4 +1,4 @@ -# AteamProtoGenCommon.cmake +# ProtoGenShared.cmake # # Shared plumbing for the two proto-code-generator CMake modules — # Ros2MsgGen.cmake (ssl_league_msgs, .msg generation) and @@ -7,37 +7,37 @@ # lives once instead of as two hand-copied blocks. # # Provides: -# ateam_require_script(SCRIPT_PATH ERROR_PREFIX) +# protogen_require_script(SCRIPT_PATH ERROR_PREFIX) # FATAL_ERROR if SCRIPT_PATH doesn't exist. # -# ateam_require_sidecar(SIDECAR_PATH ERROR_PREFIX) -# FATAL_ERROR if SIDECAR_PATH is set but doesn't exist. No-op if unset. +# protogen_require_annotation_file(ANNOTATION_FILE_PATH ERROR_PREFIX) +# FATAL_ERROR if ANNOTATION_FILE_PATH is set but doesn't exist. No-op if unset. # -# ateam_run_generator(COMMAND ERROR_PREFIX ) +# protogen_run_generator(COMMAND ERROR_PREFIX ) # Runs COMMAND via execute_process(); on nonzero exit, FATAL_ERRORs with # ERROR_PREFIX and the captured stderr. cmake_minimum_required(VERSION 3.18) -function(ateam_require_script SCRIPT_PATH ERROR_PREFIX) +function(protogen_require_script SCRIPT_PATH ERROR_PREFIX) if(NOT EXISTS "${SCRIPT_PATH}") message(FATAL_ERROR "${ERROR_PREFIX}: script not found at ${SCRIPT_PATH}") endif() endfunction() -function(ateam_require_sidecar SIDECAR_PATH ERROR_PREFIX) - if(SIDECAR_PATH AND NOT EXISTS "${SIDECAR_PATH}") - message(FATAL_ERROR "${ERROR_PREFIX}: SIDECAR file not found: ${SIDECAR_PATH}") +function(protogen_require_annotation_file ANNOTATION_FILE_PATH ERROR_PREFIX) + if(ANNOTATION_FILE_PATH AND NOT EXISTS "${ANNOTATION_FILE_PATH}") + message(FATAL_ERROR "${ERROR_PREFIX}: annotation file not found: ${ANNOTATION_FILE_PATH}") endif() endfunction() -function(ateam_run_generator) +function(protogen_run_generator) cmake_parse_arguments(_ARG "" "ERROR_PREFIX" "COMMAND" ${ARGN}) if(NOT _ARG_COMMAND) - message(FATAL_ERROR "ateam_run_generator: COMMAND is required") + message(FATAL_ERROR "protogen_run_generator: COMMAND is required") endif() if(NOT _ARG_ERROR_PREFIX) - message(FATAL_ERROR "ateam_run_generator: ERROR_PREFIX is required") + message(FATAL_ERROR "protogen_run_generator: ERROR_PREFIX is required") endif() execute_process( diff --git a/ssl_league_msgs/cmake/Ros2MsgGen.cmake b/ssl_league_msgs/cmake/Ros2MsgGen.cmake index 049c582..c3fb8ae 100644 --- a/ssl_league_msgs/cmake/Ros2MsgGen.cmake +++ b/ssl_league_msgs/cmake/Ros2MsgGen.cmake @@ -30,7 +30,7 @@ # cmake_minimum_required(VERSION 3.18) # CMAKE_CURRENT_FUNCTION_LIST_DIR (3.17), find_program(REQUIRED) (3.18) -include("${CMAKE_CURRENT_LIST_DIR}/AteamProtoGenCommon.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/ProtoGenShared.cmake") function(generate_ros2_msgs) cmake_parse_arguments( @@ -73,7 +73,7 @@ function(generate_ros2_msgs) set(_CMAKE_DIR "${CMAKE_CURRENT_FUNCTION_LIST_DIR}") set(_PLUGIN_SRC "${_CMAKE_DIR}/protoc_gen_ros2msg.py") - ateam_require_script("${_PLUGIN_SRC}" "generate_ros2_msgs") + protogen_require_script("${_PLUGIN_SRC}" "generate_ros2_msgs") file(GLOB _PLUGIN_DEPS "${_CMAKE_DIR}/*.py") @@ -102,7 +102,7 @@ function(generate_ros2_msgs) # --- Build plugin options --- set(_plugin_opt "optional_submsg=${_opt_submsg}") if(_ARG_SIDECAR) - ateam_require_sidecar("${_ARG_SIDECAR}" "generate_ros2_msgs") + protogen_require_annotation_file("${_ARG_SIDECAR}" "generate_ros2_msgs") string(APPEND _plugin_opt ",sidecar=${_ARG_SIDECAR}") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_ARG_SIDECAR}") endif() @@ -118,7 +118,7 @@ function(generate_ros2_msgs) # Run once now so the generated .msg file *list* is known at configure # time — rosidl_generate_interfaces() requires it up front. - ateam_run_generator(COMMAND ${_protoc_command} ERROR_PREFIX "generate_ros2_msgs: protoc") + protogen_run_generator(COMMAND ${_protoc_command} ERROR_PREFIX "generate_ros2_msgs: protoc") file(GLOB _generated_abs "${_ARG_OUTPUT_DIR}/msg/*.msg") if(NOT _generated_abs) diff --git a/ssl_league_msgs/cmake/ateam_proto_shared.py b/ssl_league_msgs/cmake/proto_shared.py similarity index 94% rename from ssl_league_msgs/cmake/ateam_proto_shared.py rename to ssl_league_msgs/cmake/proto_shared.py index bbaf54f..a12b935 100644 --- a/ssl_league_msgs/cmake/ateam_proto_shared.py +++ b/ssl_league_msgs/cmake/proto_shared.py @@ -7,7 +7,7 @@ semantics; that logic lives here once instead of two copies. Import with: - from ateam_proto_shared import ( + from proto_shared import ( parse_options, flatten_type_name, strip_package, build_map_entry_type_names, iter_messages, load_annotations, consumed_annotation_fields, output_proto_fields, @@ -65,6 +65,7 @@ class FieldAnnotation(TypedDict, total=False): 'ros_type': str, 'from': dict[str, str], 'scale': float, + 'optional': bool, 'position': OutputComponentSpec, 'orientation': OutputComponentSpec, 'conversion_func': str, @@ -228,13 +229,21 @@ def load_annotations(path: str | None) -> Annotations: def output_proto_fields(out: OutputEntry) -> Iterator[str]: - """Yield proto field names consumed by one annotation 'outputs' entry.""" + """ + Yield proto field names consumed by one annotation 'outputs' entry. + + A 'from' value may be a dotted path (e.g. 'designated_position.x') to + reach one level into a nested message-type field. Only the top-level + name is yielded here — that's the field actually consumed from the + containing message's own field list; the rest of the path is only + meaningful to the C++ accessor chain built at bridge-codegen time. + """ for v in out.get('from', {}).values(): - yield v + yield v.split('.', 1)[0] for sub in ('position', 'orientation'): if sub in out: for v in out[sub].get('from', {}).values(): - yield v + yield v.split('.', 1)[0] def consumed_annotation_fields(annotation_entry: MessageAnnotationEntry) -> frozenset[str]: diff --git a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py index 594b563..452393e 100755 --- a/ssl_league_msgs/cmake/protoc_gen_ros2msg.py +++ b/ssl_league_msgs/cmake/protoc_gen_ros2msg.py @@ -42,11 +42,27 @@ "ros_field": "pose", "ros_type": "geometry_msgs/Pose", "position": { "from": { "x": "tx", ... }, "scale": 1e-3 }, "orientation": { "from": { "x": "q0", ... } } + }, + { + "ros_field": "designated_position", "ros_type": "geometry_msgs/Point32", + "from": { "x": "nested_field.x", "y": "nested_field.y" }, + "optional": true } ] }, "_skip_types": ["MsgNameToOmitEntirely", ...] + A "from" value may be a dotted path (e.g. "nested_field.x") to reach one + level into a nested message-type field, instead of a scalar field on the + containing message directly. Only the top-level name (before the dot) is + treated as consumed for passthrough-exclusion purposes. + + An "outputs" entry normally emits a plain (non-array) ROS field, matching + a required or always-present proto source. Set "optional": true when the + source is a proto2 "optional" message-type field, to preserve presence: + the ROS field is emitted as a 0/1-element array instead, and the bridge + layer guards the assignment on the source field's own has_() check. + Consumed fields (right-hand side of all "from" maps, plus all "fields" keys) are excluded from passthrough. The generator errors on unknown field references or user-supplied conversion_func values (only intrinsic @@ -84,7 +100,7 @@ sys.path.insert(0, str(Path(__file__).parent)) # Must follow the sys.path.insert() above, so this can't sort before the # google.protobuf imports the way import-order linting wants. -from ateam_proto_shared import ( # noqa: E402, I100 +from proto_shared import ( # noqa: E402, I100 Annotations, build_map_entry_type_names, classify_field_shape, @@ -167,7 +183,7 @@ def map_field_to_ros2_type(field: descriptor_pb2.FieldDescriptorProto) -> str: # Annotation entry validation (error accumulation is specific to this # script's protoc-plugin response.error mechanism, so this stays local; # load_annotations/consumed_annotation_fields/output_proto_fields are -# shared — see ateam_proto_shared.py) +# shared — see proto_shared.py) # --------------------------------------------------------------------------- @@ -321,7 +337,8 @@ def dfs(node: str, path: list[str], path_fields: list[str]) -> None: def _emit_one_output(out: OutputEntry, lines: list[str]) -> None: """Emit the single ROS struct field for one annotation 'outputs' entry.""" - lines.append(f"{out['ros_type']} {out['ros_field']}") + suffix = '[]' if out.get('optional') else '' + lines.append(f"{out['ros_type']}{suffix} {out['ros_field']}") def _format_default_literal(value: bool | str | int | float) -> str: diff --git a/ssl_league_msgs/cmake/ssl_ros_annotations.json b/ssl_league_msgs/cmake/ssl_ros_annotations.json index 65c46b8..3a1077b 100644 --- a/ssl_league_msgs/cmake/ssl_ros_annotations.json +++ b/ssl_league_msgs/cmake/ssl_ros_annotations.json @@ -147,7 +147,18 @@ "command_timestamp": { "ros_type": "builtin_interfaces/Time" }, "stage_time_left": { "ros_type": "builtin_interfaces/Duration" }, "current_action_time_remaining": { "ros_type": "builtin_interfaces/Duration" } - } + }, + "outputs": [ + { + "ros_field": "designated_position", + "ros_type": "geometry_msgs/Point32", + "from": { "x": "designated_position.x", "y": "designated_position.y" }, + "scale": 1e-3, + "optional": true, + "_consumed": ["designated_position"], + "_comment": "optional nested Point message (mm); repack directly into Point32[] (0/1 elements) rather than the auto-generated RefereePoint, matching what ateam_common already expects" + } + ] }, "RefereePoint": { diff --git a/ssl_league_msgs/test/test_ateam_proto_shared.py b/ssl_league_msgs/test/test_proto_shared.py similarity index 97% rename from ssl_league_msgs/test/test_ateam_proto_shared.py rename to ssl_league_msgs/test/test_proto_shared.py index cc462a6..5aae704 100644 --- a/ssl_league_msgs/test/test_ateam_proto_shared.py +++ b/ssl_league_msgs/test/test_proto_shared.py @@ -1,6 +1,7 @@ -"""Unit tests for the pure helper functions in ateam_proto_shared.py.""" +"""Unit tests for the pure helper functions in proto_shared.py.""" -from ateam_proto_shared import ( +from google.protobuf import descriptor_pb2 +from proto_shared import ( build_map_entry_type_names, classify_field_shape, consumed_annotation_fields, @@ -9,7 +10,6 @@ output_proto_fields, parse_options, ) -from google.protobuf import descriptor_pb2 FD = descriptor_pb2.FieldDescriptorProto DP = descriptor_pb2.DescriptorProto diff --git a/ssl_ros_bridge/cmake/MsgConversionGen.cmake b/ssl_ros_bridge/cmake/MsgConversionGen.cmake index dce835d..8147c92 100644 --- a/ssl_ros_bridge/cmake/MsgConversionGen.cmake +++ b/ssl_ros_bridge/cmake/MsgConversionGen.cmake @@ -18,7 +18,7 @@ cmake_minimum_required(VERSION 3.18) # CMAKE_CURRENT_FUNCTION_LIST_DIR (3.17), find_program(REQUIRED) (3.18) -include("${CMAKE_CURRENT_LIST_DIR}/../../ssl_league_msgs/cmake/AteamProtoGenCommon.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/../../ssl_league_msgs/cmake/ProtoGenShared.cmake") function(generate_message_conversion) cmake_parse_arguments(_ARG "" "OUTPUT_DIR;SIDECAR" "PROTO_FILES;PROTO_PATHS" ${ARGN}) @@ -35,10 +35,10 @@ function(generate_message_conversion) set(_CMAKE_DIR "${CMAKE_CURRENT_FUNCTION_LIST_DIR}") set(_SCRIPT "${_CMAKE_DIR}/gen_message_conversion.py") - set(_SHARED_SCRIPT "${_CMAKE_DIR}/../../ssl_league_msgs/cmake/ateam_proto_shared.py") + set(_SHARED_SCRIPT "${_CMAKE_DIR}/../../ssl_league_msgs/cmake/proto_shared.py") - ateam_require_script("${_SCRIPT}" "generate_message_conversion") - ateam_require_script("${_SHARED_SCRIPT}" "generate_message_conversion") + protogen_require_script("${_SCRIPT}" "generate_message_conversion") + protogen_require_script("${_SHARED_SCRIPT}" "generate_message_conversion") # Build --proto-paths args set(_path_args) @@ -49,7 +49,7 @@ function(generate_message_conversion) # Build --sidecar arg set(_sidecar_arg) if(_ARG_SIDECAR) - ateam_require_sidecar("${_ARG_SIDECAR}" "generate_message_conversion") + protogen_require_annotation_file("${_ARG_SIDECAR}" "generate_message_conversion") set(_sidecar_arg "--sidecar" "${_ARG_SIDECAR}") endif() @@ -69,7 +69,7 @@ function(generate_message_conversion) # Run once now so the generated files exist for configure-time consumers # (e.g. add_library() argument lists). - ateam_run_generator(COMMAND ${_gen_command} ERROR_PREFIX "generate_message_conversion: generator") + protogen_run_generator(COMMAND ${_gen_command} ERROR_PREFIX "generate_message_conversion: generator") set(_depends ${_ARG_PROTO_FILES} "${_SCRIPT}" "${_SHARED_SCRIPT}") if(_ARG_SIDECAR) diff --git a/ssl_ros_bridge/cmake/gen_message_conversion.py b/ssl_ros_bridge/cmake/gen_message_conversion.py index d72560e..e8f7c20 100644 --- a/ssl_ros_bridge/cmake/gen_message_conversion.py +++ b/ssl_ros_bridge/cmake/gen_message_conversion.py @@ -40,7 +40,7 @@ sys.path.insert(0, str(_LEAGUE_MSGS_CMAKE)) # Must follow the sys.path.insert() above, so this can't sort before the # rosidl_pycommon import the way import-order linting wants. -from ateam_proto_shared import ( # noqa: E402, I100 +from proto_shared import ( # noqa: E402, I100 Annotations, build_map_entry_type_names, classify_field_shape, @@ -91,8 +91,14 @@ def scale_literal(val: float) -> str: return f'{val}f' -def proto_accessor_expr(field_name: str) -> str: - return f'proto_msg.{field_name}()' +def proto_accessor_expr(field_path: str) -> str: + """ + Build a chained C++ accessor from a possibly dotted field path. + + 'x' -> 'proto_msg.x()'. 'designated_position.x' (one level into a + nested message field) -> 'proto_msg.designated_position().x()'. + """ + return 'proto_msg.' + '.'.join(f'{part}()' for part in field_path.split('.')) # ── Annotation output codegen ───────────────────────────────────────────────── @@ -118,12 +124,39 @@ def _emit_component_assignments( return lines +def _emit_optional_output(out: OutputEntry, ind: str) -> list[str]: + """ + Emit C++ for an 'outputs' entry sourced from a proto2-optional nested field. + + The ROS field is a 0/1-element array (see the 'optional' key in + protoc_gen_ros2msg.py's module docstring); the assignment is guarded by + a has_() check on the shared top-level source field name, taken + from the first 'from' entry. + """ + ros_field = out['ros_field'] + ros_type = out['ros_type'] + from_map = out['from'] + top_level = next(iter(from_map.values())).split('.')[0] + cpp_type = to_ros_cpp_type(ros_type) + + inner_ind = f'{ind} ' + lines = [f'{ind}if (proto_msg.has_{top_level}()) {{'] + lines.append(f'{inner_ind}{cpp_type} _value;') + lines += _emit_component_assignments('_value', from_map, out.get('scale'), inner_ind) + lines.append(f'{inner_ind}ros_msg.{ros_field}.push_back(_value);') + lines.append(f'{ind}}}') + return lines + + def emit_annotation_output(out: OutputEntry, ind: str) -> list[str]: """Emit C++ for one 'outputs' entry.""" ros_field = out['ros_field'] ros_type = out['ros_type'] target = f'ros_msg.{ros_field}' + if out.get('optional') and ros_type in ('geometry_msgs/Point32', 'geometry_msgs/Vector3'): + return _emit_optional_output(out, ind) + if ros_type in ('geometry_msgs/Point32', 'geometry_msgs/Vector3'): return _emit_component_assignments(target, out['from'], out.get('scale'), ind) @@ -512,7 +545,10 @@ def generate_source( pf_map: dict[str, descriptor_pb2.FieldDescriptorProto] = {f.name: f for f in msg.field} ros_t = to_ros_msg_type(flat, ros_pkg) - lines.append(f'{ros_t} fromProto(const {cpp_name} & proto_msg)') + # [[maybe_unused]]: a message with every field annotation-skipped + # (e.g. RealismConfig's sole field is a skipped google.protobuf.Any) + # produces a body that never references proto_msg. + lines.append(f'{ros_t} fromProto([[maybe_unused]] const {cpp_name} & proto_msg)') lines.append('{') lines.append(f' {ros_t} ros_msg;') diff --git a/ssl_ros_bridge/src/core/CMakeLists.txt b/ssl_ros_bridge/src/core/CMakeLists.txt index b5a400a..99bf3cb 100644 --- a/ssl_ros_bridge/src/core/CMakeLists.txt +++ b/ssl_ros_bridge/src/core/CMakeLists.txt @@ -1,8 +1,17 @@ +# The league protos deprecate several old GameEvent oneof arms +# ([deprecated = true] in ssl_gc_game_event.proto); protoc propagates that +# into [[deprecated]] on the generated C++ accessors, suppress in scope. +set_source_files_properties( + ${_CONV_OUT}/message_conversion_generated.cpp + PROPERTIES COMPILE_OPTIONS "-Wno-deprecated-declarations" +) + add_library(${PROJECT_NAME}_core SHARED get_ip_addresses.cpp multicast_receiver.cpp ${_CONV_OUT}/message_conversion_generated.cpp ) + target_include_directories(${PROJECT_NAME}_core PUBLIC . ${_CONV_OUT}) ament_target_dependencies(${PROJECT_NAME}_core rclcpp @@ -13,9 +22,12 @@ ament_target_dependencies(${PROJECT_NAME}_core tf2 tf2_geometry_msgs ) + target_link_libraries(${PROJECT_NAME}_core Boost::boost protobuf::libprotobuf ) + target_compile_features(${PROJECT_NAME}_core PUBLIC cxx_std_20) + install(TARGETS ${PROJECT_NAME}_core DESTINATION lib) From 7cdadbcfbe441e92534f31fa103768822ce44293 Mon Sep 17 00:00:00 2001 From: Will Stuckey Date: Sun, 23 Aug 2026 15:36:45 -0400 Subject: [PATCH 7/7] switch protoc invocation to use league cmake helper --- ARCHITECTURE.md | 2 +- ssl_league_msgs/CMakeLists.txt | 10 +-- ssl_league_protobufs/CMakeLists.txt | 87 ++++++++----------- ssl_league_protobufs/ssl-protocol-defs | 2 +- ssl_ros_bridge/CMakeLists.txt | 10 +-- ssl_ros_bridge/src/log2bag/log_reader.hpp | 2 +- .../src/team_client/team_client.hpp | 2 +- 7 files changed, 48 insertions(+), 67 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cc368e4..ab919e8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -51,7 +51,7 @@ file, but produce different output for different consumers. ```mermaid flowchart LR subgraph submodule["ssl-protocol-defs submodule"] - protos["*.proto files\n(gc/, vision/, simulation/)"] + protos["*.proto files\n(gamecontroller/, vision/, simulation/)"] end annotations["ssl_ros_annotations.json\n(sidecar/annotation file)"] diff --git a/ssl_league_msgs/CMakeLists.txt b/ssl_league_msgs/CMakeLists.txt index 00b9d6a..ec3401d 100644 --- a/ssl_league_msgs/CMakeLists.txt +++ b/ssl_league_msgs/CMakeLists.txt @@ -12,11 +12,11 @@ set(_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ssl_league_protobufs/ssl-protocol generate_ros2_msgs( PROTO_FILES - ${_PROTO_DIR}/gc/ssl_gc_common.proto - ${_PROTO_DIR}/gc/ssl_gc_geometry.proto - ${_PROTO_DIR}/gc/ssl_gc_game_event.proto - ${_PROTO_DIR}/gc/ssl_gc_referee_message.proto - ${_PROTO_DIR}/gc/ssl_gc_rcon.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_common.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_geometry.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_game_event.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_referee_message.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_rcon.proto ${_PROTO_DIR}/vision/ssl_vision_detection.proto ${_PROTO_DIR}/vision/ssl_vision_geometry.proto ${_PROTO_DIR}/vision/ssl_vision_wrapper.proto diff --git a/ssl_league_protobufs/CMakeLists.txt b/ssl_league_protobufs/CMakeLists.txt index 3001fdf..12b3917 100644 --- a/ssl_league_protobufs/CMakeLists.txt +++ b/ssl_league_protobufs/CMakeLists.txt @@ -4,60 +4,40 @@ project(ssl_league_protobufs) find_package(ament_cmake REQUIRED) find_package(Protobuf REQUIRED) -# ssl-protocol-defs/ is the RoboCup-SSL/ssl-protocol-defs submodule. Its proto -# files import each other relative to their own proto/ root (for example, -# "gc/ssl_gc_common.proto"), and protoc mirrors that same relative path into -# its generated #include lines and its --cpp_out layout. CMake's -# protobuf_generate_cpp() macro instead places each output file relative to -# CMAKE_CURRENT_SOURCE_DIR, one directory above the submodule's proto/ root. -# The two roots disagree, so compilation fails with "no such file" on the -# cross-includes. Invoking protoc directly with one consistent --proto_path -# root avoids that mismatch. -set(_PROTO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/ssl-protocol-defs/proto") +# ssl-protocol-defs/ is the RoboCup-SSL/ssl-protocol-defs submodule. It ships a +# CMake module that runs protoc with a single --proto_path root, which is what +# the cross-imports in these protos require; see the comments in +# SSLProtocolDefs.cmake for why CMake's own protobuf_generate_cpp() cannot be +# used here. PROTO_ROOT and the output layout are derived automatically, and +# OUT_DIR defaults to ${CMAKE_CURRENT_BINARY_DIR}. +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ssl-protocol-defs/cmake") +include(SSLProtocolDefs) -set(_PROTO_FILES - gc/ssl_gc_common.proto - gc/ssl_gc_game_event.proto - gc/ssl_gc_geometry.proto - gc/ssl_gc_rcon_autoref.proto - gc/ssl_gc_rcon_remotecontrol.proto - gc/ssl_gc_rcon_team.proto - gc/ssl_gc_rcon.proto - gc/ssl_gc_referee_message.proto - simulation/ssl_simulation_config.proto - simulation/ssl_simulation_control.proto - simulation/ssl_simulation_error.proto - simulation/ssl_simulation_robot_control.proto - simulation/ssl_simulation_robot_feedback.proto - simulation/ssl_simulation_synchronous.proto - vision/ssl_vision_detection_tracked.proto - vision/ssl_vision_detection.proto - vision/ssl_vision_geometry.proto - vision/ssl_vision_wrapper_tracked.proto - vision/ssl_vision_wrapper.proto +ssl_protocol_defs_generate_cpp( + PROTOS + gamecontroller/ssl_gc_common.proto + gamecontroller/ssl_gc_game_event.proto + gamecontroller/ssl_gc_geometry.proto + gamecontroller/ssl_gc_rcon_autoref.proto + gamecontroller/ssl_gc_rcon_remotecontrol.proto + gamecontroller/ssl_gc_rcon_team.proto + gamecontroller/ssl_gc_rcon.proto + gamecontroller/ssl_gc_referee_message.proto + simulation/ssl_simulation_config.proto + simulation/ssl_simulation_control.proto + simulation/ssl_simulation_error.proto + simulation/ssl_simulation_robot_control.proto + simulation/ssl_simulation_robot_feedback.proto + simulation/ssl_simulation_synchronous.proto + vision/ssl_vision_detection_tracked.proto + vision/ssl_vision_detection.proto + vision/ssl_vision_geometry.proto + vision/ssl_vision_wrapper_tracked.proto + vision/ssl_vision_wrapper.proto + SOURCES_VAR PROTOBUF_SRCS + HEADERS_VAR PROTOBUF_HDRS ) -set(PROTOBUF_SRCS) -set(PROTOBUF_HDRS) -set(_abs_proto_files) -foreach(_rel ${_PROTO_FILES}) - get_filename_component(_stem "${_rel}" NAME_WLE) - get_filename_component(_dir "${_rel}" DIRECTORY) - list(APPEND PROTOBUF_SRCS "${CMAKE_CURRENT_BINARY_DIR}/${_dir}/${_stem}.pb.cc") - list(APPEND PROTOBUF_HDRS "${CMAKE_CURRENT_BINARY_DIR}/${_dir}/${_stem}.pb.h") - list(APPEND _abs_proto_files "${_PROTO_ROOT}/${_rel}") -endforeach() - -add_custom_command( - OUTPUT ${PROTOBUF_SRCS} ${PROTOBUF_HDRS} - COMMAND protobuf::protoc - ARGS --cpp_out=${CMAKE_CURRENT_BINARY_DIR} --proto_path=${_PROTO_ROOT} ${_abs_proto_files} - DEPENDS ${_abs_proto_files} protobuf::protoc - COMMENT "Running protoc on ssl-protocol-defs protos" - VERBATIM -) -set_source_files_properties(${PROTOBUF_SRCS} ${PROTOBUF_HDRS} PROPERTIES GENERATED TRUE) - add_library(${PROJECT_NAME} SHARED ${PROTOBUF_HDRS} ${PROTOBUF_SRCS} @@ -67,9 +47,10 @@ target_link_libraries(${PROJECT_NAME} ) target_include_directories(${PROJECT_NAME} PUBLIC - # Generated headers live under subdirectories (gc/, vision/, simulation/) + # Generated headers live under subdirectories (gamecontroller/, vision/, + # simulation/) # that mirror ssl-protocol-defs' own layout, and they include each other - # by path from that root (for example, "gc/ssl_gc_common.pb.h"). PUBLIC, + # by path from that root (e.g. "gamecontroller/ssl_gc_common.pb.h"). PUBLIC, # not INTERFACE, because this package's own compilation needs that root # too, not just external consumers, to resolve those same cross-includes. # Two include roots are exported: the normal diff --git a/ssl_league_protobufs/ssl-protocol-defs b/ssl_league_protobufs/ssl-protocol-defs index 4d3326f..4388dc7 160000 --- a/ssl_league_protobufs/ssl-protocol-defs +++ b/ssl_league_protobufs/ssl-protocol-defs @@ -1 +1 @@ -Subproject commit 4d3326fa38c8f62c6fb1dbbb13cc8cea8b39b1ed +Subproject commit 4388dc72f61dc157bcc1ebf6b167aeb9a69d2694 diff --git a/ssl_ros_bridge/CMakeLists.txt b/ssl_ros_bridge/CMakeLists.txt index de00696..444b96a 100644 --- a/ssl_ros_bridge/CMakeLists.txt +++ b/ssl_ros_bridge/CMakeLists.txt @@ -26,11 +26,11 @@ set(_CONV_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated_conversion") generate_message_conversion( PROTO_FILES - ${_PROTO_DIR}/gc/ssl_gc_common.proto - ${_PROTO_DIR}/gc/ssl_gc_geometry.proto - ${_PROTO_DIR}/gc/ssl_gc_game_event.proto - ${_PROTO_DIR}/gc/ssl_gc_referee_message.proto - ${_PROTO_DIR}/gc/ssl_gc_rcon.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_common.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_geometry.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_game_event.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_referee_message.proto + ${_PROTO_DIR}/gamecontroller/ssl_gc_rcon.proto ${_PROTO_DIR}/vision/ssl_vision_detection.proto ${_PROTO_DIR}/vision/ssl_vision_geometry.proto ${_PROTO_DIR}/vision/ssl_vision_wrapper.proto diff --git a/ssl_ros_bridge/src/log2bag/log_reader.hpp b/ssl_ros_bridge/src/log2bag/log_reader.hpp index 52486af..546e484 100644 --- a/ssl_ros_bridge/src/log2bag/log_reader.hpp +++ b/ssl_ros_bridge/src/log2bag/log_reader.hpp @@ -22,7 +22,7 @@ #define LOG2BAG__LOG_READER_HPP_ #include -#include +#include #include #include #include diff --git a/ssl_ros_bridge/src/team_client/team_client.hpp b/ssl_ros_bridge/src/team_client/team_client.hpp index d99bc20..3395b5e 100644 --- a/ssl_ros_bridge/src/team_client/team_client.hpp +++ b/ssl_ros_bridge/src/team_client/team_client.hpp @@ -21,7 +21,7 @@ #ifndef TEAM_CLIENT__TEAM_CLIENT_HPP_ #define TEAM_CLIENT__TEAM_CLIENT_HPP_ -#include +#include #include #include #include