├── codespell_dictionary.txt ├── codespell_whitelist.txt ├── requirements.txt ├── images ├── QR.png └── overview_architecture.png ├── presentation ├── ros2_parameter_server.pdf ├── jp │ ├── ros2_parameter_server.pdf │ └── ros2_parameter_server.md └── ros2_parameter_server.md ├── server ├── CHANGELOG.rst ├── param │ ├── parameters_via_launch.yaml │ ├── parameters_via_cli.yaml │ └── parameter_server.yaml ├── launch │ └── parameter_server.launch.py ├── include │ └── parameter_server.h └── src │ ├── main.cpp │ └── parameter_server.cpp ├── .github ├── workflows │ ├── mirror-rolling-to-master.yaml │ ├── jazzy-nightly.yml │ ├── humble-nightly.yml │ ├── kilted-nightly.yml │ ├── rolling-nightly.yml │ ├── codespell.yaml │ ├── jazzy.yml │ ├── humble.yml │ ├── kilted.yml │ ├── rolling.yml │ ├── gemini-issue-scheduled-triage.yml │ ├── gemini-issue-automated-triage.yml │ ├── gemini-cli.yml │ └── gemini-pr-review.yml └── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── codespell.cfg ├── CONTRIBUTING.md ├── k8s ├── parameters.yaml ├── deployment.yaml ├── ros2-sample.yaml └── k8s_tutorial.md ├── package.xml ├── test ├── launch │ └── test.launch.py ├── src │ ├── test_with_node_options.cpp │ ├── persist_parameter_client.cpp │ └── test_default.cpp ├── test.py └── include │ ├── persist_parameter_client.hpp │ └── test_common.h ├── docker └── Dockerfile ├── scripts ├── build-verification.sh └── docker_release.sh ├── CMakeLists.txt ├── CHANGELOG.rst ├── LICENSE └── README.md /codespell_dictionary.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /codespell_whitelist.txt: -------------------------------------------------------------------------------- 1 | thead 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | codespell 2 | -------------------------------------------------------------------------------- /images/QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fujitatomoya/ros2_persist_parameter_server/HEAD/images/QR.png -------------------------------------------------------------------------------- /images/overview_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fujitatomoya/ros2_persist_parameter_server/HEAD/images/overview_architecture.png -------------------------------------------------------------------------------- /presentation/ros2_parameter_server.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fujitatomoya/ros2_persist_parameter_server/HEAD/presentation/ros2_parameter_server.pdf -------------------------------------------------------------------------------- /presentation/jp/ros2_parameter_server.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fujitatomoya/ros2_persist_parameter_server/HEAD/presentation/jp/ros2_parameter_server.pdf -------------------------------------------------------------------------------- /server/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package ros2 persistent parameter_server 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 1.0.0 (2019-11-18) 6 | ------------------ 7 | * 1st commit for basic functions. 8 | * Contributors: Tomoya Fujita 9 | -------------------------------------------------------------------------------- /.github/workflows/mirror-rolling-to-master.yaml: -------------------------------------------------------------------------------- 1 | name: Mirror rolling to master 2 | 3 | on: 4 | push: 5 | branches: [ rolling ] 6 | 7 | jobs: 8 | mirror-to-master: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: zofrex/mirror-branch@v1 12 | with: 13 | target-branch: master 14 | -------------------------------------------------------------------------------- /codespell.cfg: -------------------------------------------------------------------------------- 1 | [codespell] 2 | 3 | # Enable built-in dictionaries/rules. 4 | # See more details for https://github.com/codespell-project/codespell/tree/main/codespell_lib/data. 5 | builtin = clear,rare,informal,code 6 | 7 | # Ignore words listed in this file. 8 | ignore-words = codespell_whitelist.txt 9 | 10 | # Add custom dictionary file. 11 | dictionary = codespell_dictionary.txt,- 12 | 13 | # Skip checking files or directories. 14 | skip = *.pdf,*.html,*.png,CHANGELOG.rst 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Robotics Stack Exchange 4 | url: https://robotics.stackexchange.com/ 5 | about: Please ask and answer questions here. 6 | - name: Documentation for Active ROS Distributions 7 | url: https://docs.ros.org/ 8 | about: Please check our documentation here. 9 | - name: ROS Discourse 10 | url: https://discourse.ros.org/ 11 | about: Discussion on ROS and ROS-related things. 12 | -------------------------------------------------------------------------------- /.github/workflows/jazzy-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Build for jazzy 2 | 3 | on: 4 | schedule: 5 | - cron: '0 13 * * *' # Runs every day at midnight, 13:00 UTC 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ros:jazzy 15 | env: 16 | ROS_DISTRO: jazzy 17 | steps: 18 | - name: Check out repository code 19 | uses: actions/checkout@v3 20 | - name: Build and Test with ROS jazzy 21 | shell: bash 22 | run: | 23 | ./scripts/build-verification.sh 24 | -------------------------------------------------------------------------------- /.github/workflows/humble-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Build for humble 2 | 3 | on: 4 | schedule: 5 | - cron: '0 13 * * *' # Runs every day at midnight, 13:00 UTC 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ros:humble 15 | env: 16 | ROS_DISTRO: humble 17 | steps: 18 | - name: Check out repository code 19 | uses: actions/checkout@v3 20 | - name: Build and Test with ROS humble 21 | shell: bash 22 | run: | 23 | ./scripts/build-verification.sh 24 | -------------------------------------------------------------------------------- /.github/workflows/kilted-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Build for kilted 2 | 3 | on: 4 | schedule: 5 | - cron: '0 13 * * *' # Runs every day at midnight, 13:00 UTC 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ros:kilted 15 | env: 16 | ROS_DISTRO: kilted 17 | steps: 18 | - name: Check out repository code 19 | uses: actions/checkout@v3 20 | - name: Build and Test with ROS kilted 21 | shell: bash 22 | run: | 23 | ./scripts/build-verification.sh 24 | -------------------------------------------------------------------------------- /.github/workflows/rolling-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Build for rolling 2 | 3 | on: 4 | schedule: 5 | - cron: '0 13 * * *' # Runs every day at midnight, 13:00 UTC 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ros:rolling 15 | env: 16 | ROS_DISTRO: rolling 17 | steps: 18 | - name: Check out repository code 19 | uses: actions/checkout@v3 20 | - name: Build and Test with ROS rolling 21 | shell: bash 22 | run: | 23 | ./scripts/build-verification.sh 24 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yaml: -------------------------------------------------------------------------------- 1 | name: codespell 2 | 3 | on: 4 | pull_request: 5 | 6 | # Allows you to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | spellcheck: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.10' 20 | 21 | - name: Install dependencies with pip 22 | run: pip install --no-warn-script-location --user -r requirements.txt 23 | 24 | - name: Spellcheck 25 | run: codespell --config codespell.cfg 26 | -------------------------------------------------------------------------------- /server/param/parameters_via_launch.yaml: -------------------------------------------------------------------------------- 1 | /**: 2 | ros__parameters: 3 | # This file is expected to be used with ros2 launch 4 | # > ros2 launch persist_parameter_server parameter_server.launch.py 5 | # 6 | # all of the parameters here cannot be registered as persistent parameter, 7 | # since these are set internally with library and not able to detect via /parameter_events topic. 8 | some_int: 1 9 | a_string: "Hello world" 10 | pi: 3.14 11 | some_lists: 12 | some_integers: [1, 2, 3, 4] 13 | persistent: 14 | some_int: 1 15 | a_string: 'Hello world' 16 | pi: 3.14 17 | some_lists: 18 | some_integers: [1, 2, 3, 4] 19 | -------------------------------------------------------------------------------- /server/param/parameters_via_cli.yaml: -------------------------------------------------------------------------------- 1 | /**: 2 | ros__parameters: 3 | # This file is expected to be used 4 | # > ros2 run persist_parameter_server server --ros-args --params-file /parameters_via_cli.yaml 5 | # 6 | # all of the parameters here cannot be registered as persistent parameter, 7 | # since these are set internally with library and not able to detect via /parameter_events topic. 8 | some_int: 1 9 | a_string: "Hello world" 10 | pi: 3.14 11 | some_lists: 12 | some_integers: [1, 2, 3, 4] 13 | persistent: 14 | some_int: 1 15 | a_string: 'Hello world' 16 | pi: 3.14 17 | some_lists: 18 | some_integers: [1, 2, 3, 4] 19 | -------------------------------------------------------------------------------- /server/param/parameter_server.yaml: -------------------------------------------------------------------------------- 1 | /**: 2 | ros__parameters: 3 | # This file is expected to be used as following, 4 | # > ros2 run persist_parameter_server server --file-path /tmp/parameter_server.yaml 5 | # 6 | # Not persistent parameter. 7 | # These just will be loaded as normal parameters. 8 | some_int: 1 9 | a_string: "Hello world" 10 | pi: 3.14 11 | some_lists: 12 | some_integers: [1, 2, 3, 4] 13 | # persistent parameter. 14 | # these parameters will be registered as persistent parameter, 15 | # so during shutdown, these will be stored back in the storage if any updates available. 16 | persistent: 17 | some_int: 1 18 | a_string: 'Hello world' 19 | pi: 3.14 20 | some_lists: 21 | some_integers: [1, 2, 3, 4] 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Any contribution that you make to this repository will 2 | be under the Apache 2 License, as dictated by that 3 | [license](http://www.apache.org/licenses/LICENSE-2.0.html): 4 | 5 | ~~~ 6 | 5. Submission of Contributions. Unless You explicitly state otherwise, 7 | any Contribution intentionally submitted for inclusion in the Work 8 | by You to the Licensor shall be under the terms and conditions of 9 | this License, without any additional terms or conditions. 10 | Notwithstanding the above, nothing herein shall supersede or modify 11 | the terms of any separate license agreement you may have executed 12 | with Licensor regarding such Contributions. 13 | ~~~ 14 | 15 | Contributors must sign-off each commit by adding a `Signed-off-by: ...` 16 | line to commit messages to certify that they have the right to submit 17 | the code they are contributing to the project according to the 18 | [Developer Certificate of Origin (DCO)](https://developercertificate.org/). 19 | -------------------------------------------------------------------------------- /k8s/parameters.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: server-initial-parameters 5 | data: 6 | parameter_server.yaml: | 7 | /parameter_server: 8 | ros__parameters: 9 | # This file is expected to be used as following, 10 | # > ros2 run persist_parameter_server server --file-path /tmp/parameter_server.yaml 11 | # 12 | # Not persistent parameter. 13 | # These just will be loaded as normal parameters. 14 | some_int: 1 15 | a_string: "Hello world" 16 | pi: 3.14 17 | some_lists: 18 | some_integers: [1, 2, 3, 4] 19 | # persistent parameter. 20 | # these parameters will be registered as persistent parameter, 21 | # so during shutdown, these will be stored back in the storage if any updates available. 22 | persistent: 23 | some_int: 1 24 | a_string: 'Hello world' 25 | pi: 3.14 26 | some_lists: 27 | some_integers: [1, 2, 3, 4] 28 | -------------------------------------------------------------------------------- /.github/workflows/jazzy.yml: -------------------------------------------------------------------------------- 1 | # This is workflow for parameter server with jazzy 2 | name: jazzy 3 | 4 | on: 5 | push: 6 | branches: [ "rolling" ] 7 | pull_request: 8 | branches: [ "rolling" ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | 16 | # each job goes for each ros supported distribution. 17 | # each job description absorb the distribution dependency as much as possible, 18 | # so that build verification script can be agnostic from distribution dependency. 19 | 20 | build: 21 | runs-on: ubuntu-latest 22 | container: 23 | image: ros:jazzy 24 | env: 25 | ROS_DISTRO: jazzy 26 | steps: 27 | - name: Check out repository code 28 | uses: actions/checkout@v3 29 | - name: Build with ROS jazzy 30 | shell: bash 31 | run: | 32 | ./scripts/build-verification.sh 33 | -------------------------------------------------------------------------------- /.github/workflows/humble.yml: -------------------------------------------------------------------------------- 1 | # This is workflow for parameter server with humble 2 | name: humble 3 | 4 | on: 5 | push: 6 | branches: [ "rolling" ] 7 | pull_request: 8 | branches: [ "rolling" ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | 16 | # each job goes for each ros supported distribution. 17 | # each job description absorb the distribution dependency as much as possible, 18 | # so that build verification script can be agnostic from distribution dependency. 19 | 20 | build: 21 | runs-on: ubuntu-latest 22 | container: 23 | image: ros:humble 24 | env: 25 | ROS_DISTRO: humble 26 | steps: 27 | - name: Check out repository code 28 | uses: actions/checkout@v3 29 | - name: Build with ROS humble 30 | shell: bash 31 | run: | 32 | ./scripts/build-verification.sh 33 | -------------------------------------------------------------------------------- /.github/workflows/kilted.yml: -------------------------------------------------------------------------------- 1 | # This is workflow for parameter server with kilted 2 | name: kilted 3 | 4 | on: 5 | push: 6 | branches: [ "rolling" ] 7 | pull_request: 8 | branches: [ "rolling" ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | 16 | # each job goes for each ros supported distribution. 17 | # each job description absorb the distribution dependency as much as possible, 18 | # so that build verification script can be agnostic from distribution dependency. 19 | 20 | build: 21 | runs-on: ubuntu-latest 22 | container: 23 | image: ros:kilted 24 | env: 25 | ROS_DISTRO: kilted 26 | steps: 27 | - name: Check out repository code 28 | uses: actions/checkout@v3 29 | - name: Build with ROS kilted 30 | shell: bash 31 | run: | 32 | ./scripts/build-verification.sh 33 | -------------------------------------------------------------------------------- /.github/workflows/rolling.yml: -------------------------------------------------------------------------------- 1 | # This is workflow for parameter server with rolling 2 | name: rolling 3 | 4 | on: 5 | push: 6 | branches: [ "rolling" ] 7 | pull_request: 8 | branches: [ "rolling" ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | 16 | # each job goes for each ros supported distribution. 17 | # each job description absorb the distribution dependency as much as possible, 18 | # so that build verification script can be agnostic from distribution dependency. 19 | 20 | build: 21 | runs-on: ubuntu-latest 22 | container: 23 | image: ros:rolling 24 | env: 25 | ROS_DISTRO: rolling 26 | steps: 27 | - name: Check out repository code 28 | uses: actions/checkout@v3 29 | - name: Build with ROS rolling 30 | shell: bash 31 | run: | 32 | ./scripts/build-verification.sh 33 | -------------------------------------------------------------------------------- /k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: parameter-server 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: parameter-server 10 | template: 11 | metadata: 12 | labels: 13 | app: parameter-server 14 | spec: 15 | containers: 16 | - name: parameter-server 17 | image: tomoyafujita/ros2_param_server:rolling 18 | command: ["/bin/bash", "-c"] 19 | args: 20 | - "source /opt/ros/rolling/setup.bash && source /root/colcon_ws/install/setup.bash && ros2 run persist_parameter_server server --file-path /etc/config/parameter_server.yaml" 21 | resources: 22 | requests: 23 | memory: "128Mi" 24 | cpu: "100m" 25 | limits: 26 | memory: "256Mi" 27 | cpu: "500m" 28 | volumeMounts: 29 | - name: config-volume 30 | mountPath: /etc/config 31 | volumes: 32 | - name: config-volume 33 | configMap: 34 | name: server-initial-parameters 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: File a feature request. 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "Thanks for taking the time to fill out this feature request form!" 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Feature description 12 | description: | 13 | Description in a few sentences what the feature consists of and what problem it will solve. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: motivation 18 | attributes: 19 | label: Feature Motivation 20 | description: | 21 | Description what you are trying to solve, what is the problem to address with this Feature Request. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: implementation 26 | attributes: 27 | label: Implementation considerations 28 | validations: 29 | required: false 30 | description: | 31 | Relevant information on how the feature could be implemented and pros and cons of the different solutions. 32 | - type: textarea 33 | id: information 34 | attributes: 35 | label: Additional Information 36 | validations: 37 | required: false 38 | description: | 39 | If you have more details information, please describe here. 40 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | persist_parameter_server 5 | 1.0.4 6 | 7 | ros2 parameter server that other nodes can write/read parameters including persistent parameters. 8 | 9 | Tomoya Fujita 10 | Apache License 2.0 11 | Tomoya Fujita> 12 | 13 | ament_cmake 14 | 15 | libboost-dev 16 | libboost-filesystem-dev 17 | libboost-program-options-dev 18 | 19 | rclcpp 20 | rclcpp_components 21 | rcutils 22 | rmw 23 | rmw_implementation_cmake 24 | std_msgs 25 | std_srvs 26 | yaml_cpp_vendor 27 | 28 | launch_ros 29 | 30 | ament_cmake_pytest 31 | ament_lint_auto 32 | ament_lint_common 33 | launch 34 | 35 | 36 | ament_cmake 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/launch/test.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Sony Corporation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Launch server && client""" 16 | 17 | from launch import LaunchDescription 18 | from launch.substitutions import EnvironmentVariable 19 | from launch.actions import ExecuteProcess, DeclareLaunchArgument 20 | from launch.substitutions import LaunchConfiguration 21 | 22 | 23 | def generate_launch_description(): 24 | allow_dynamic_typing_arg = DeclareLaunchArgument( 25 | 'allow_dynamic_typing', default_value='false', description='Enable dynamic typing for parameters' 26 | ) 27 | 28 | return LaunchDescription([ 29 | allow_dynamic_typing_arg, 30 | ExecuteProcess( 31 | cmd=[ 32 | 'ros2', 'run', 'persist_parameter_server', 'server', 33 | '--file-path', '/tmp/test/parameter_server.yaml', 34 | '--allow-dynamic-typing', LaunchConfiguration( 35 | 'allow_dynamic_typing') 36 | ], 37 | respawn=True 38 | ) 39 | ]) 40 | -------------------------------------------------------------------------------- /k8s/ros2-sample.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ros2-talker 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: ros2-talker 10 | template: 11 | metadata: 12 | labels: 13 | app: ros2-talker 14 | spec: 15 | containers: 16 | - image: tomoyafujita/ros:rolling 17 | command: ["/bin/bash", "-c"] 18 | args: ["source /opt/ros/$ROS_DISTRO/setup.bash && ros2 topic pub /chatter std_msgs/String \"data: Hello, I am talker\""] 19 | imagePullPolicy: IfNotPresent 20 | tty: true 21 | name: ros2-talker 22 | tolerations: 23 | - key: node-role.kubernetes.io/master 24 | operator: Exists 25 | effect: NoSchedule 26 | - key: node-role.kubernetes.io/control-plane 27 | operator: Exists 28 | effect: NoSchedule 29 | restartPolicy: Always 30 | 31 | --- 32 | 33 | apiVersion: apps/v1 34 | kind: Deployment 35 | metadata: 36 | name: ros2-listener 37 | spec: 38 | replicas: 1 39 | selector: 40 | matchLabels: 41 | app: ros2-listener 42 | template: 43 | metadata: 44 | labels: 45 | app: ros2-listener 46 | spec: 47 | containers: 48 | - image: tomoyafujita/ros:rolling 49 | command: ["/bin/bash", "-c"] 50 | args: ["source /opt/ros/$ROS_DISTRO/setup.bash && ros2 topic echo /chatter std_msgs/String"] 51 | imagePullPolicy: IfNotPresent 52 | tty: true 53 | name: ros2-listener 54 | tolerations: 55 | - key: node-role.kubernetes.io/master 56 | operator: Exists 57 | effect: NoSchedule 58 | - key: node-role.kubernetes.io/control-plane 59 | operator: Exists 60 | effect: NoSchedule 61 | restartPolicy: Always 62 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build: 2 | # docker build --pull --rm -f ./docker/Dockerfile --build-arg="ROS_DISTRO=rolling" --build-arg="COLCON_WS=/root/colcon_ws" -t /ros2_param_server:rolling . 3 | # 4 | # Usage: 5 | # docker pull /ros2_param_server:rolling 6 | 7 | # An ARG declared before a FROM is outside of a build stage, 8 | # so it can’t be used in any instruction after a FROM. 9 | # To use the default value of an ARG declared before the first FROM 10 | # use an ARG instruction without a value inside of a build stage: 11 | ARG ROS_DISTRO=rolling 12 | ARG COLCON_WS=/root/colcon_ws 13 | 14 | FROM ros:${ROS_DISTRO} 15 | 16 | LABEL maintainer="Tomoya Fujita " 17 | LABEL version="1.0" 18 | LABEL description="ros2 persistent parameter server ${ROS_DISTRO} docker image" 19 | 20 | ARG ROS_DISTRO 21 | ARG COLCON_WS 22 | 23 | SHELL ["/bin/bash","-c"] 24 | 25 | RUN mkdir -p ${COLCON_WS}/src 26 | COPY . ${COLCON_WS}/src/ros2_persistent_parameter_server 27 | 28 | # All apt-get commands start with an update, then install 29 | # and finally, a cache cleanup to keep the image size small. 30 | 31 | # Install packages 32 | RUN apt-get update \ 33 | && apt-get upgrade -y \ 34 | && apt-get install -y \ 35 | # required packages for ros2 persistent parameter server 36 | libyaml-cpp-dev libboost-program-options-dev libboost-filesystem-dev \ 37 | --no-install-recommends \ 38 | && rm -rf /var/lib/apt/lists/* 39 | 40 | # Build and source colcon workspace 41 | RUN cd $COLCON_WS \ 42 | && source /opt/ros/$ROS_DISTRO/setup.bash \ 43 | && colcon build --symlink-install --packages-select persist_parameter_server 44 | 45 | # Add source environment in .bashrc 46 | RUN echo -n -e "\n" >> /root/.bashrc 47 | RUN echo "### ros2 persistent parameter server workspace setting" >> /root/.bashrc 48 | RUN echo "cd $COLCON_WS && source ./install/setup.bash" >> /root/.bashrc 49 | 50 | # Overwrite as environmental variable so that entrypoint can rely on those. 51 | ENV COLCON_WS=${COLCON_WS} 52 | ENV ROS_DISTRO=${ROS_DISTRO} 53 | #ENTRYPOINT ["/ros_entrypoint.sh"] 54 | -------------------------------------------------------------------------------- /presentation/jp/ros2_parameter_server.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | theme: default 4 | header: "__ROS 2 Persistent Parameter Server__" 5 | footer: "[fujitatomoya@github](https://github.com/fujitatomoya)" 6 | --- 7 | 8 | ## [ROS 2 Persistent Parameter Server](https://github.com/fujitatomoya/ros2_persist_parameter_server) 9 | 10 | ![bg right:35% width:300px](../../images/QR.png) 11 | 12 | - ROS 1 parameter serverからインスピレーションを得て開発。 13 | - このグローバルサーバーで任意のパラメータを設定/取得可能。 14 | - パラメータをストレージに保存/読み込み可能。 15 | 16 | 19 | 20 | --- 21 | 22 | ![bg 70%](https://images.squarespace-cdn.com/content/v1/606d378755a86f589aa297b7/1653397531343-6M4IQ4JWDQV1SQ8W17UN/HumbleHawksbill_TransparentBG-NoROS.png) 23 | ![bg 75%](https://images.squarespace-cdn.com/content/v1/606d378755a86f589aa297b7/ebf9b1d5-45b7-4a73-8f48-dc5d3f4fc8fc/JazzyJalisco_Final.png?format=2500w) 24 | ![bg 90%](https://www.therobotreport.com/wp-content/uploads/2025/05/kilted-Kaiju-featured.jpg) 25 | ![bg 70%](https://images.squarespace-cdn.com/content/v1/606d378755a86f589aa297b7/1628726028642-TVRVRIQL914IVYWV8MG9/rolling.png) 26 | 27 | 30 | 31 | --- 32 | 33 | ## Why we need this? 34 | 35 | - 多くのノードが共有するグローバル設定(例:システム設定や構成など) 36 | - 汎用的なROS 2システムやlocalhost全体のパラメータサーバー。 37 | - 永続化ストレージによるシステムの再初期化サポート。 38 | - **パラメータは実行時に変更され、永続化ボリュームにキャッシュされます。次回起動や再起動時には、変更されたパラメータが初期化時に読み込まれます。(パラメータの寿命はユースケースに依存し、システム寿命の場合もあれば、ノード寿命の場合もあります。)** 39 | - Parameter Serverを使用するROS 1ベースのアプリケーションとの互換性。 40 | 41 | 44 | 45 | --- 46 | 47 | ![bg 90%](../../images/overview_architecture.png) 48 | 49 | --- 50 | 51 | ## How to Run 52 | 53 | - Docker 54 | 55 | ```console 56 | $ docker run -it tomoyafujita/ros2_param_server:rolling /bin/bash 57 | root@bf4d904e3800:~/colcon_ws# ros2 run persist_parameter_server server 58 | ``` 59 | 60 | - Kubernetes 61 | 62 | ```console 63 | $ kubectl apply -f ./k8s/parameters.yaml 64 | $ kubectl apply -f ./k8s/deployment.yaml 65 | ``` 66 | 67 | 70 | 71 | --- 72 | 73 | ## Issues and PRs are always welcome 🚀 74 | 75 | https://github.com/fujitatomoya/ros2_persist_parameter_server 76 | 77 | ![bg left:35% width:300px](../../images/QR.png) 78 | 79 | 82 | 83 | -------------------------------------------------------------------------------- /server/launch/parameter_server.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Sony Corporation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Launch a server.""" 16 | 17 | from launch import LaunchDescription 18 | from launch.substitutions import EnvironmentVariable 19 | import launch_ros.actions 20 | import os 21 | import pathlib 22 | 23 | parameters_file_name = 'parameters_via_launch.yaml' 24 | 25 | def generate_launch_description(): 26 | parameters_file_path = str(pathlib.Path(__file__).parents[1]) # get current path and go one level up 27 | parameters_file_path += '/param/' + parameters_file_name 28 | return LaunchDescription( 29 | [ 30 | launch_ros.actions.Node( 31 | package="persist_parameter_server", 32 | executable="server", 33 | output="screen", 34 | # respawn in 5.0 seconds 35 | respawn=True, 36 | respawn_delay=5.0, 37 | # these parameters in parameters_file_path cannot be registered as persistent parameters, 38 | # these will be loaded as normal parameter without event on /parameter_events topic. 39 | parameters=[parameters_file_path], 40 | # this is an example to load persistent parameter files into parameter server, 41 | # these parameters described in parameter_server.yaml with prefix "persistent" will be registered as persistent parameter. 42 | # arguments=[ 43 | # "--file-path", 44 | # "/tmp/parameter_server.yaml", 45 | # "--allow-declare", 46 | # "true", 47 | # "--allow-override", 48 | # "true", 49 | # "--storing-period", 50 | # "60", 51 | # "--allow-dynamic-typing", 52 | # "true", 53 | # ], 54 | ) 55 | ] 56 | ) 57 | -------------------------------------------------------------------------------- /k8s/k8s_tutorial.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Tutorial 2 | 3 | This guide provides instructions on how to deploy the ROS 2 Persistent Parameter Server to a Kubernetes cluster. 4 | 5 | ## Prerequisites 6 | 7 | - Kubernetes cluster (minikube, kind, or cloud provider) is available. 8 | - kubectl CLI tool installed. 9 | 10 | ## Deployment Steps 11 | 12 | ### ConfigMap 13 | 14 | As an example to load the default parameters to the parameter server, we use configmap here. 15 | This is just an example to describe and load the default parameters to the parameter server. 16 | 17 | ```console 18 | kubectl apply -f ./k8s/parameters.yaml 19 | ``` 20 | 21 | This command creates the configmap object in the cluster, which can be bound to the parameter server in next step. 22 | 23 | > [!NOTE] 24 | > We can also use the actual yaml file in the host system or shared storage files instead. This depends on the user requirement how to bind the parameter file to the parameter server. 25 | 26 | ### Deploy Parameter Server 27 | 28 | Let's start the parameter server with default parameters provided by the configmap. 29 | 30 | ```console 31 | kubectl apply -f ./k8s/deployment.yaml 32 | ``` 33 | 34 | ### Check Parameters 35 | 36 | You can deploy the ROS 2 example talker and listener containers with following pods description. 37 | 38 | ```console 39 | kubectl apply -f ./k8s/ros2-sample.yaml 40 | ``` 41 | 42 | Now we can see the parameter server is running in the ROS 2 system with specified parameters. 43 | 44 | ```console 45 | $ kubectl get pods 46 | NAME READY STATUS RESTARTS AGE 47 | parameter-server-856cbb6574-qvctl 1/1 Running 0 14m 48 | ros2-listener-7d764d5c8b-jd4vf 1/1 Running 0 34m 49 | ros2-talker-8666f7cf6d-xdqdq 1/1 Running 0 34m 50 | 51 | $ $kubectl exec -it ros2-talker-8666f7cf6d-xdqdq -- /bin/bash 52 | root@ros2-talker-8666f7cf6d-xdqdq:/# source /opt/ros/rolling/setup.bash 53 | root@ros2-talker-8666f7cf6d-xdqdq:/# ros2 param list 54 | /parameter_server: 55 | a_string 56 | persistent.a_string 57 | persistent.pi 58 | persistent.some_int 59 | persistent.some_lists.some_integers 60 | pi 61 | qos_overrides./parameter_events.publisher.depth 62 | qos_overrides./parameter_events.publisher.durability 63 | qos_overrides./parameter_events.publisher.history 64 | qos_overrides./parameter_events.publisher.reliability 65 | some_int 66 | some_lists.some_integers 67 | start_type_description_service 68 | use_sim_time 69 | ``` 70 | -------------------------------------------------------------------------------- /presentation/ros2_parameter_server.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | theme: default 4 | header: "__ROS 2 Persistent Parameter Server__" 5 | footer: "[fujitatomoya@github](https://github.com/fujitatomoya)" 6 | --- 7 | 8 | ## [ROS 2 Persistent Parameter Server](https://github.com/fujitatomoya/ros2_persist_parameter_server) 9 | 10 | ![bg right:35% width:300px](../images/QR.png) 11 | 12 | - inspired by ROS 1 parameter server. 13 | - can set/get any parameters in this global server. 14 | - can save/load the parameters in storage. 15 | 16 | 19 | 20 | --- 21 | 22 | ![bg 70%](https://images.squarespace-cdn.com/content/v1/606d378755a86f589aa297b7/1653397531343-6M4IQ4JWDQV1SQ8W17UN/HumbleHawksbill_TransparentBG-NoROS.png) 23 | ![bg 75%](https://images.squarespace-cdn.com/content/v1/606d378755a86f589aa297b7/ebf9b1d5-45b7-4a73-8f48-dc5d3f4fc8fc/JazzyJalisco_Final.png?format=2500w) 24 | ![bg 90%](https://www.therobotreport.com/wp-content/uploads/2025/05/kilted-Kaiju-featured.jpg) 25 | ![bg 70%](https://images.squarespace-cdn.com/content/v1/606d378755a86f589aa297b7/1628726028642-TVRVRIQL914IVYWV8MG9/rolling.png) 26 | 27 | 30 | 31 | --- 32 | 33 | ## Why we need this? 34 | 35 | - Global configuration that many nodes share (e.g. RTOS priorities, vehicle dimensions, …) 36 | - Generic ROS 2 system or localhost wide parameter server. 37 | - Persistent storage support to re-initialize the system. 38 | - **parameters are modified in runtime and cached into persistent volume as well. and next boot or next re-spawn, modified parameters will be loaded at initialization. (parameter lifetime is dependent on use case, sometimes system lifetime, sometimes node lifetime.)** 39 | - Using ROS 1 based application with Parameter Server. 40 | 41 | 44 | 45 | --- 46 | 47 | ![bg 90%](../images/overview_architecture.png) 48 | 49 | --- 50 | 51 | ## How to Run 52 | 53 | - Docker 54 | 55 | ```console 56 | $ docker run -it tomoyafujita/ros2_param_server:rolling /bin/bash 57 | root@bf4d904e3800:~/colcon_ws# ros2 run persist_parameter_server server 58 | ``` 59 | 60 | - Kubernetes 61 | 62 | ```console 63 | $ kubectl apply -f ./k8s/parameters.yaml 64 | $ kubectl apply -f ./k8s/deployment.yaml 65 | ``` 66 | 67 | 70 | 71 | --- 72 | 73 | ## Issues and PRs are always welcome 🚀 74 | 75 | https://github.com/fujitatomoya/ros2_persist_parameter_server 76 | 77 | ![bg left:35% width:300px](../images/QR.png) 78 | 79 | 82 | -------------------------------------------------------------------------------- /scripts/build-verification.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ##################################################################### 4 | # ROS 2 Persistent Parameter Server 5 | # 6 | # This script builds parameter server within ros docker images. 7 | # 8 | # To avoid updating and modifying the files under `.github/workflows`, 9 | # this scripts should be adjusted building process accordingly. 10 | # And `.github/workflows` just calls this script in the workflow pipeline. 11 | # This allows us to maintain the workflow process easier for contributors. 12 | # 13 | ##################################################################### 14 | 15 | ######################## 16 | # Function Definitions # 17 | ######################## 18 | 19 | function mark { 20 | export $1=`pwd`; 21 | } 22 | 23 | function exit_trap() { 24 | if [ $? != 0 ]; then 25 | echo "Command [$BASH_COMMAND] is failed" 26 | exit 1 27 | fi 28 | } 29 | 30 | function install_prerequisites () { 31 | trap exit_trap ERR 32 | echo "[${FUNCNAME[0]}]: update and install dependent packages." 33 | apt update && apt upgrade -y 34 | apt install -y ros-${ROS_DISTRO}-desktop ros-${ROS_DISTRO}-rmw-cyclonedds-cpp --no-install-recommends 35 | apt install -y libyaml-cpp-dev libboost-program-options-dev libboost-filesystem-dev 36 | cd $there 37 | } 38 | 39 | function setup_build_colcon_env () { 40 | trap exit_trap ERR 41 | echo "[${FUNCNAME[0]}]: set up colcon build environment." 42 | mkdir -p ${COLCON_WORKSPACE}/src 43 | cd ${COLCON_WORKSPACE} 44 | cp -rf $there ${COLCON_WORKSPACE}/src 45 | } 46 | 47 | function build_parameter_server () { 48 | trap exit_trap ERR 49 | echo "[${FUNCNAME[0]}]: build ROS 2 parameter server." 50 | source /opt/ros/${ROS_DISTRO}/setup.bash 51 | cd ${COLCON_WORKSPACE} 52 | colcon build --symlink-install --packages-select persist_parameter_server --cmake-args -D CLIENT_TEST_DEMO=ON 53 | } 54 | 55 | function test_parameter_server () { 56 | trap exit_trap ERR 57 | echo "[${FUNCNAME[0]}]: test ROS 2 parameter server." 58 | source /opt/ros/${ROS_DISTRO}/setup.bash 59 | cd ${COLCON_WORKSPACE} 60 | 61 | # TODO(@fujitatomoya): currently unit tests are missing for parameter server with `colcon test`. 62 | 63 | # source the parameter server local packages 64 | source ./install/local_setup.bash 65 | # setup and execute the system test 66 | mkdir /tmp/test 67 | cp ./src/ros2_persist_parameter_server/server/param/parameter_server.yaml /tmp/test 68 | ./src/ros2_persist_parameter_server/test/test.py 69 | } 70 | 71 | ######## 72 | # Main # 73 | ######## 74 | 75 | export DEBIAN_FRONTEND=noninteractive 76 | export COLCON_WORKSPACE=/tmp/colcon_ws 77 | 78 | # mark the working space root directory, so that we can come back anytime with `cd $there` 79 | mark there 80 | 81 | # set the trap on error 82 | trap exit_trap ERR 83 | 84 | # call install functions in sequence 85 | install_prerequisites 86 | setup_build_colcon_env 87 | build_parameter_server 88 | test_parameter_server 89 | 90 | exit 0 91 | -------------------------------------------------------------------------------- /test/src/test_with_node_options.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sony Corporation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "persist_parameter_client.hpp" 21 | #include "test_common.h" 22 | 23 | rclcpp::Logger TestPersistParameter::client_logger_ = rclcpp::get_logger("client"); 24 | 25 | // this test must be run somultaneously with the server node launched with the option allow-dynamic-typing set to true 26 | int main(int argc, char ** argv) 27 | { 28 | // force flush of the stdout buffer. 29 | // this ensures a correct sync of all prints 30 | // even when executed simultaneously within the launch file. 31 | setvbuf(stdout, NULL, _IONBF, BUFSIZ); 32 | 33 | rclcpp::init(argc, argv); 34 | std::shared_ptr test_client; 35 | 36 | RCLCPP_INFO(test_client->get_logger(), "****************************************************" 37 | "***********************"); 38 | int ret_code = 0; 39 | try { 40 | test_client = std::make_shared("client", rclcpp::NodeOptions()); 41 | /* 42 | * Dynamic Typing Tests 43 | * 44 | * These tests will try to change the type of a parameter to see whether the 45 | * server accepts it based on allow_dynamic_typing=true/false. 46 | */ 47 | { 48 | test_client->do_change_and_check( 49 | "persistent.some_int", "mutated", "a. dynamically change the type of an existing parameter"); 50 | test_client->do_change_and_check( 51 | "persistent.some_int", 10, "b. revert the type of the parameter to int"); 52 | test_client->do_change_and_check( 53 | "persistent.new_double", 3.14, "c. create new parameter with type double"); 54 | test_client->do_change_and_check( 55 | "persistent.new_double", "3.14", "d. change the type of the new parameter to string"); 56 | } 57 | 58 | } catch (const rclcpp::exceptions::RCLError & e) { 59 | ret_code = -1; 60 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 61 | } catch (const NoServerError & e) { 62 | ret_code = -2; 63 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 64 | } catch (const SetOperationError & e) { 65 | ret_code = -3; 66 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 67 | } 68 | 69 | // if any tests are not passed, return EXIT_FAILURE. 70 | ret_code = test_client->print_result(); 71 | rclcpp::shutdown(); 72 | 73 | return ret_code; 74 | } 75 | -------------------------------------------------------------------------------- /server/include/parameter_server.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sony Corporation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef __PARAMETER_SERVER_H__ 16 | #define __PARAMETER_SERVER_H__ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "rclcpp/rclcpp.hpp" 24 | #include "std_srvs/srv/trigger.hpp" 25 | #include "rclcpp_components/register_node_macro.hpp" 26 | #include "yaml-cpp/yaml.h" 27 | 28 | class ParameterServer : public rclcpp::Node 29 | { 30 | public: 31 | RCLCPP_SMART_PTR_DEFINITIONS(ParameterServer) 32 | 33 | ParameterServer( 34 | const std::string & node_name, 35 | const rclcpp::NodeOptions & options, 36 | const std::string & persistent_yaml_file); 37 | ~ParameterServer(); 38 | 39 | private: 40 | // Using custom yaml file same as yaml format of ros2 parameter as much as possible, 41 | // so use rcl_yaml_param_parser functions directly to load custom persistent yaml file. 42 | void LoadYamlFile(); 43 | 44 | // To store yaml into a file, think it's more convenient to use yaml_cpp than libyaml. 45 | // (rcl_yaml_param_parser/libyaml not contain store function) 46 | void StoreYamlFile(); 47 | 48 | // To check whether yaml file is valid 49 | void CheckYamlFile(); 50 | void CheckYamlFile(const std::string& file); 51 | void ValidateYamlFile(YAML::Node node, const std::string& key = ""); 52 | void SaveNode(YAML::Emitter& out, YAML::Node node, const std::string& key = ""); 53 | 54 | // Check whether parameter name contains "persistent." in the parameter list 55 | bool CheckPersistentParam(const std::vector & parameters); 56 | 57 | // Check flag to store file 58 | std::atomic_bool param_update_; 59 | 60 | // yaml file to load/store 61 | std::string persistent_yaml_file_; 62 | 63 | // store changed(add, update) parameter name contains "persistent." after checking in 'parameter_events' callback 64 | std::set changed_parameter_lists_; 65 | 66 | // To adapt the original yaml format that contain namespace(optional) and nodename(can be /**) 67 | bool parameter_use_stars_ = false; 68 | bool parameter_ns_exist_ = false; 69 | bool parameter_name_exist_ = false; 70 | std::string node_name_; 71 | 72 | // set parameters callback handler 73 | OnSetParametersCallbackHandle::SharedPtr callback_handler_; 74 | 75 | // for periodic storing to the file system 76 | rclcpp::TimerBase::SharedPtr timer_; 77 | 78 | bool allow_dynamic_typing_ = false; 79 | // For manual triggering of save 80 | rclcpp::Service::SharedPtr save_trigger_; 81 | rclcpp::Service::SharedPtr reload_trigger_; 82 | }; 83 | 84 | #endif // __PARAMETER_SERVER_H__ 85 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """A script to start `Server && Client` through launch file and responsible for killing the Server""" 4 | from threading import Thread 5 | 6 | import os 7 | import psutil 8 | import shutil 9 | import signal 10 | import subprocess 11 | import sys 12 | import time 13 | 14 | signal.signal(signal.SIGINT, signal.SIG_DFL) 15 | sleep_time = 3 16 | launchServerCmd = ['ros2', 'launch', 'persist_parameter_server', 'test.launch.py'] 17 | launchClientCmd = ['ros2', 'run', 18 | 'persist_parameter_server', 'client_default'] 19 | 20 | launchServerCmdWithNodeOptions = [ 21 | 'ros2', 'launch', 'persist_parameter_server', 'test.launch.py', 'allow_dynamic_typing:=true'] 22 | launchClientCmdWithNodeOptions = [ 23 | 'ros2', 'run', 'persist_parameter_server', 'client_with_node_options'] 24 | 25 | if shutil.which('ros2') is None: 26 | print("source /install/setup.bash...then retry.") 27 | sys.exit(1) 28 | 29 | def kill_server(): 30 | try: 31 | time.sleep(sleep_time) 32 | print("parameter server is about to be killed...") 33 | program_name = 'server' 34 | for process in psutil.process_iter(): 35 | if process.name() == program_name: 36 | path = psutil.Process(process.pid) 37 | if "install/persist_parameter_server/lib/persist_parameter_server" in path.exe() or "persist_parameter_server/server" in path.exe(): 38 | os.kill(int(process.pid), signal.SIGINT) 39 | print("parameter server is killed successfully") 40 | break 41 | except: 42 | print("parameter server cannot be killed") 43 | return 44 | time.sleep(5) 45 | #print("Press CTRL-C to shutdown...") 46 | 47 | # Start Server process with re-spawn enabled, this process stays running 48 | server_process = subprocess.Popen(launchServerCmd, preexec_fn=os.setsid) 49 | print(f"Parameter Server Process started with PID: {server_process.pid}") 50 | 51 | # Start test client process 52 | client_process = subprocess.Popen(launchClientCmd) 53 | print(f"Parameter Client Process started with PID: {client_process.pid}") 54 | 55 | # Start killer thread to re-spawn the parameter server 56 | t = Thread(target = kill_server, args = ()) 57 | t.start() 58 | 59 | # Wait until the client process finishes 60 | return_code = client_process.wait() 61 | 62 | # Cleanup the process and thread 63 | t.join() 64 | os.killpg(os.getpgid(server_process.pid), signal.SIGTERM) 65 | 66 | print("\nTest with default options finished. Proceeding to testing with node options") 67 | 68 | # Start the server 69 | server_process = subprocess.Popen( 70 | launchServerCmdWithNodeOptions, preexec_fn=os.setsid) 71 | print(f"Parameter Server Process started with PID: {server_process.pid}") 72 | 73 | # Start test client process 74 | client_process = subprocess.Popen(launchClientCmdWithNodeOptions) 75 | print(f"Parameter Client Process started with PID: {client_process.pid}") 76 | 77 | # Wait until the client process finishes and then kill the server 78 | return_code2 = client_process.wait() 79 | os.killpg(os.getpgid(server_process.pid), signal.SIGTERM) 80 | 81 | print("\nTest process finished.") 82 | print(f"Return Code: {return_code}") 83 | 84 | # Check if the client process completed successfully 85 | if return_code == return_code2 == 0: 86 | print("The process completed successfully.") 87 | sys.exit(0) 88 | else: 89 | print("The process failed.") 90 | sys.exit(1) 91 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "**Required Info:**" 8 | - type: checkboxes 9 | attributes: 10 | label: Is there an existing issue for this? 11 | description: Please search to see if an issue already exists for the bug you encountered. 12 | options: 13 | - label: I have searched the existing issues 14 | required: true 15 | - type: textarea 16 | id: regression 17 | attributes: 18 | label: Regression 19 | description: Is the reported bug a regression? If so, what is the last version of ROS where it still worked fine? 20 | - type: input 21 | id: os 22 | attributes: 23 | label: "Operating System:" 24 | description: | 25 | Please try to be specific. 26 | For Linux, please use the command `uname -a` from a terminal and copy paste its output here. 27 | For Windows, open a terminal (Win key + R and type `cmd`), type the command `ver` and press enter. 28 | Then copy paste the output here. 29 | validations: 30 | required: true 31 | - type: input 32 | id: version 33 | attributes: 34 | label: "ROS version or commit hash:" 35 | description: | 36 | **Examples:** *humble*, *jazzy*, ... 37 | validations: 38 | required: true 39 | - type: input 40 | id: rmw 41 | attributes: 42 | label: "RMW implementation:" 43 | description: | 44 | **Examples:** *rmw_fastrtps_cpp*, *rmw_connextdds*, *rmw_cyclonedds_cpp*, ... 45 | You can check the ROS Middleware (RMW) implementation with the command: `ros2 doctor --report` 46 | Find the line starting with `middleware name` in the report. 47 | validations: 48 | required: true 49 | - type: input 50 | id: clientlib 51 | attributes: 52 | label: "Client library (if applicable):" 53 | description: | 54 | **Examples:** *rclcpp*, *rclpy*, ... 55 | Client libraries are the APIs that allow users to implement their ROS 2 code. 56 | validations: 57 | required: false 58 | - type: textarea 59 | id: doctor 60 | attributes: 61 | label: "'ros2 doctor --report' output" 62 | description: | 63 | It can help us knowing the details of your ROS environment. 64 | Please use the command `ros2 doctor --report` and copy paste its output here. 65 | render: Formatted 66 | validations: 67 | required: false 68 | - type: textarea 69 | id: repro 70 | attributes: 71 | label: "Steps to reproduce issue" 72 | description: | 73 | How do you trigger this bug? Please walk us through it step by step. 74 | Include all the commands you ran in the exact order you ran them so that anyone can reproduce the bug. 75 | placeholder: | 76 | 1. 77 | 2. 78 | 3. 79 | ... 80 | validations: 81 | required: true 82 | - type: textarea 83 | id: expected 84 | attributes: 85 | label: "Expected behavior" 86 | validations: 87 | required: true 88 | - type: textarea 89 | id: actual 90 | attributes: 91 | label: "Actual behavior" 92 | validations: 93 | required: true 94 | - type: textarea 95 | id: addinfo 96 | attributes: 97 | label: "Additional information" 98 | validations: 99 | required: false 100 | -------------------------------------------------------------------------------- /scripts/docker_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################# 4 | # This script builds and releases ros2 persistent parameter server docker images. 5 | ################################################################################# 6 | 7 | ################ 8 | # User Setting # 9 | ################ 10 | 11 | DOCKERHUB_USERNAME="${DOCKERHUB_USERNAME:-tomoyafujita}" 12 | COLCON_WS="${COLCON_WS:-/root/colcon_ws}" 13 | 14 | ros_distros=( 15 | "humble" 16 | "jazzy" 17 | "kilted" 18 | "rolling" 19 | ) 20 | 21 | ###################### 22 | # Options (defaults) # 23 | ###################### 24 | 25 | build_image=false 26 | upload_image=false 27 | 28 | ######################## 29 | # Function Definitions # 30 | ######################## 31 | 32 | function print_usage() { 33 | echo "Usage: $0 [-b] [-u]" 34 | echo "Options(default):" 35 | echo " -b : build docker container images (default: false)" 36 | echo " -u : upload images to DockerHub (default: false)" 37 | exit 1 38 | } 39 | 40 | function exit_trap() { 41 | # shellcheck disable=SC2317 # Don't warn about unreachable commands in this function 42 | if [ $? != 0 ]; then 43 | echo "Command [$BASH_COMMAND] is failed" 44 | exit 1 45 | fi 46 | } 47 | 48 | function check_dockerhub_setting () { 49 | trap exit_trap ERR 50 | echo "[${FUNCNAME[0]}]: checking dockerhub setting and configuration." 51 | if [ -z "$DOCKERHUB_USERNAME" ]; then 52 | echo "DOCKERHUB_USERNAME is not set." 53 | exit 1 54 | fi 55 | # check if docker login succeeds 56 | docker login 57 | } 58 | 59 | function command_exist() { 60 | trap exit_trap ERR 61 | echo "[${FUNCNAME[0]}]: checking $1 command exists." 62 | if command -v "$1" >/dev/null 2>&1; then 63 | echo "$1 exists." 64 | else 65 | echo "Error: $1 not found." 66 | exit 1 67 | fi 68 | } 69 | 70 | function build_images() { 71 | trap exit_trap ERR 72 | echo "[${FUNCNAME[0]}]: building ros2 persistent parameter server docker container images." 73 | for distro in "${ros_distros[@]}"; do 74 | echo "----- $distro image building" 75 | docker build --pull --rm -f ./docker/Dockerfile --build-arg="ROS_DISTRO=$distro" --build-arg="COLCON_WS=$COLCON_WS" -t $DOCKERHUB_USERNAME/ros2_param_server:$distro . 76 | done 77 | echo "----- all images successfully generated!!! -----" 78 | } 79 | 80 | function upload_images() { 81 | trap exit_trap ERR 82 | echo "[${FUNCNAME[0]}]: uploading ros2 persistent parameter server docker container images." 83 | for distro in "${ros_distros[@]}"; do 84 | echo "----- $distro image uploading" 85 | # TODO@fujitatomoya: support multi-arch docker images 86 | docker push $DOCKERHUB_USERNAME/ros2_param_server:$distro 87 | done 88 | echo "----- all images successfully verified!!! -----" 89 | } 90 | 91 | ######## 92 | # Main # 93 | ######## 94 | 95 | # set the trap on error 96 | trap exit_trap ERR 97 | 98 | # parse command line options 99 | while getopts ":bvu" opt; do 100 | case $opt in 101 | b) 102 | build_image=true 103 | ;; 104 | u) 105 | upload_image=true 106 | ;; 107 | \?) 108 | echo "Invalid option: -$OPTARG" 109 | print_usage 110 | ;; 111 | esac 112 | done 113 | shift $((OPTIND-1)) 114 | 115 | # check settings 116 | command_exist docker 117 | check_dockerhub_setting 118 | 119 | # building images 120 | if [ "$build_image" = true ]; then 121 | build_images 122 | fi 123 | 124 | # upload images 125 | if [ "$upload_image" = true ]; then 126 | upload_images 127 | fi 128 | 129 | exit 0 130 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(persist_parameter_server) 3 | 4 | # Set Release build if no build type was specified 5 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 6 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING 7 | "Build type for the build. Possible values are: Debug, Release, RelWithDebInfo, MinSizeRel" 8 | FORCE) 9 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 10 | "Debug" "Release" "RelWithDebInfo" "MinSizeRel") 11 | endif() 12 | 13 | # Default to C++17 14 | if(NOT CMAKE_CXX_STANDARD) 15 | set(CMAKE_CXX_STANDARD 17) 16 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 17 | endif() 18 | 19 | # Enable additional warnings and warnings as errors 20 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 21 | add_compile_options(-Wall -Wextra -Wpedantic) 22 | endif() 23 | 24 | find_package(ament_cmake REQUIRED) 25 | 26 | find_package(rclcpp REQUIRED) 27 | find_package(rclcpp_components REQUIRED) 28 | find_package(rcutils REQUIRED) 29 | find_package(std_msgs REQUIRED) 30 | find_package(std_srvs REQUIRED) 31 | find_package(rmw REQUIRED) 32 | 33 | find_package(Boost REQUIRED COMPONENTS program_options filesystem) 34 | find_package(yaml_cpp_vendor REQUIRED) 35 | 36 | option(CLIENT_TEST_DEMO "Build client test demo" OFF) 37 | 38 | add_executable(server 39 | server/src/parameter_server.cpp 40 | server/src/main.cpp 41 | ) 42 | 43 | # yaml-cpp updates CMake thing significantly on v0.8.0 or later. 44 | # so we ended up having the if statement to process differently instead of creating branches. 45 | # see https://github.com/jbeder/yaml-cpp/releases/tag/0.8.0 46 | find_package(yaml-cpp 0.8.0 QUIET) 47 | if (yaml-cpp_FOUND) 48 | message(STATUS "yaml-cpp package is greater equal than version 0.8.0") 49 | target_link_libraries(server 50 | rclcpp::rclcpp 51 | rclcpp_components::component 52 | rcutils::rcutils 53 | yaml-cpp::yaml-cpp 54 | ${std_msgs_TARGETS} 55 | ${std_srvs_TARGETS} 56 | ${Boost_LIBRARIES} 57 | ) 58 | else() 59 | message(STATUS "yaml-cpp package is less than version 0.8.0") 60 | find_package(yaml-cpp REQUIRED) 61 | target_link_libraries(server 62 | rclcpp::rclcpp 63 | rclcpp_components::component 64 | rcutils::rcutils 65 | yaml-cpp 66 | ${std_msgs_TARGETS} 67 | ${std_srvs_TARGETS} 68 | ${Boost_LIBRARIES} 69 | ) 70 | endif() 71 | 72 | target_include_directories(server 73 | PUBLIC 74 | $ 75 | ) 76 | 77 | install(TARGETS server DESTINATION lib/${PROJECT_NAME}) 78 | 79 | # Install launch files. 80 | install(DIRECTORY 81 | server/launch 82 | server/param 83 | DESTINATION share/${PROJECT_NAME}/ 84 | ) 85 | 86 | # Build client test demo 87 | if (CLIENT_TEST_DEMO) 88 | add_executable(client_default 89 | test/src/test_default.cpp 90 | test/src/persist_parameter_client.cpp 91 | ) 92 | 93 | target_link_libraries(client_default 94 | PUBLIC 95 | rclcpp::rclcpp 96 | rclcpp_components::component 97 | rcutils::rcutils 98 | ${std_msgs_TARGETS} 99 | ${std_srvs_TARGETS} 100 | ) 101 | 102 | target_include_directories(client_default 103 | PUBLIC 104 | $ 105 | ) 106 | 107 | add_executable(client_with_node_options 108 | test/src/test_with_node_options.cpp 109 | test/src/persist_parameter_client.cpp 110 | ) 111 | 112 | target_link_libraries(client_with_node_options 113 | PUBLIC 114 | rclcpp::rclcpp 115 | rclcpp_components::component 116 | rcutils::rcutils 117 | ${std_msgs_TARGETS} 118 | ${std_srvs_TARGETS} 119 | ) 120 | 121 | target_include_directories(client_with_node_options 122 | PUBLIC 123 | $ 124 | ) 125 | 126 | install(TARGETS client_default client_with_node_options DESTINATION lib/${PROJECT_NAME}) 127 | 128 | # Install launch files. 129 | install(DIRECTORY 130 | test/launch 131 | DESTINATION share/${PROJECT_NAME}/ 132 | ) 133 | endif() 134 | 135 | ament_package() 136 | -------------------------------------------------------------------------------- /server/src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sony Corporation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | 17 | #include "parameter_server.h" 18 | 19 | using namespace std; 20 | using namespace boost::program_options; 21 | 22 | int main(int argc, char **argv) 23 | { 24 | int ret = EXIT_SUCCESS; 25 | // Force flush of the stdout buffer. 26 | setvbuf(stdout, NULL, _IONBF, BUFSIZ); 27 | 28 | // To call rclcpp::init_and_remove_ros_arguments at the beginning to prevent 29 | // from using new arguments "--ros-args" to support remapping node name 30 | // (such as "--ros-args --remap __node:=test1") 31 | // that boost program options failed to parse arguments 32 | auto nonros_args = rclcpp::init_and_remove_ros_arguments(argc, argv); 33 | 34 | options_description description("ROS2 parameter server command line interfaces"); 35 | description.add_options() 36 | ("help,h", "help message to show interfaces") 37 | ("file-path,f", value()->default_value("/tmp/parameter_server.yaml"), 38 | "volume path to load/store parameters in yaml format (default /tmp/parameter_server.yaml)") 39 | ("allow-declare,d", value()->default_value(true), 40 | "enable(true) / disable(false) allow_undeclared_parameters via node option (default true)") 41 | ("allow-override,o", value()->default_value(true), 42 | "enable(true) / disable(false) automatically_declare_parameters_from_overrides via node option (default true)") 43 | ("storing-period,s", value()->default_value(60), 44 | "period in seconds for periodic persistent parameter storing (default 60). No periodic storing is performed if this parameter is set to 0") 45 | ("allow-dynamic-typing,t", value()->default_value(false), 46 | "When enabled (true), allows parameter type to change upon reading from persistence. Disabled (false) by default"); 47 | 48 | variables_map vm; 49 | store(basic_command_line_parser(nonros_args).options(description).run(), vm); 50 | notify(vm); 51 | 52 | std::string node_name = "parameter_server"; 53 | string opt_file("/tmp/parameter_server.yaml"); 54 | bool opt_allow_declare = true; 55 | bool opt_allow_override = true; 56 | unsigned int storing_period = 60; 57 | bool opt_allow_dynamic_typing = false; 58 | 59 | if (vm.count("help")) 60 | { 61 | cout << description << endl; 62 | rclcpp::shutdown(); 63 | return ret; 64 | } 65 | else 66 | { 67 | opt_file = vm["file-path"].as(); 68 | opt_allow_declare = vm["allow-declare"].as(); 69 | opt_allow_override = vm["allow-override"].as(); 70 | storing_period = vm["storing-period"].as(); 71 | opt_allow_dynamic_typing = vm["allow-dynamic-typing"].as(); 72 | } 73 | 74 | rclcpp::NodeOptions options = ( 75 | rclcpp::NodeOptions() 76 | .allow_undeclared_parameters(opt_allow_declare) 77 | .automatically_declare_parameters_from_overrides(opt_allow_override) 78 | .append_parameter_override("allow_dynamic_typing", opt_allow_dynamic_typing) 79 | .append_parameter_override("storing_period", static_cast(storing_period)) 80 | ); 81 | 82 | ParameterServer::SharedPtr node = nullptr; 83 | try 84 | { 85 | node = ParameterServer::make_shared(node_name, options, opt_file); 86 | if (node == nullptr) 87 | { 88 | throw std::bad_alloc(); 89 | } 90 | 91 | RCLCPP_INFO(node->get_logger(), 92 | "Parameter Server node named: '%s' started and ready, and serving '%zu' parameters already!", 93 | node->get_fully_qualified_name(), 94 | node->list_parameters({}, rcl_interfaces::srv::ListParameters::Request::DEPTH_RECURSIVE).names.size()); 95 | 96 | rclcpp::spin(node); 97 | } 98 | catch (const std::exception& e) 99 | { 100 | std::cerr << "Catch exception: " << e.what() << std::endl; 101 | ret = EXIT_FAILURE; 102 | } 103 | 104 | rclcpp::shutdown(); 105 | return ret; 106 | } 107 | -------------------------------------------------------------------------------- /test/include/persist_parameter_client.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sony Corporation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef __PARAMETER_CLIENT_H__ 16 | #define __PARAMETER_CLIENT_H__ 17 | 18 | #include "rclcpp/rclcpp.hpp" 19 | #include "std_srvs/srv/trigger.hpp" 20 | 21 | using namespace std::chrono_literals; 22 | 23 | class PersistParametersClient : public rclcpp::Node 24 | { 25 | public: 26 | RCLCPP_SMART_PTR_DEFINITIONS(PersistParametersClient) 27 | 28 | RCLCPP_PUBLIC 29 | PersistParametersClient( 30 | const std::string & client_name, 31 | const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions(), 32 | const std::string & remote_node_name = "parameter_server" 33 | ); 34 | 35 | // Format the array value for easy output. 36 | template 37 | inline void format_array_output(std::ostringstream & ss, const std::vector & value_vec) 38 | { 39 | ss << "[ "; 40 | for(const auto & value : value_vec) { 41 | ss << value << " "; 42 | } 43 | ss << "]"; 44 | 45 | return; 46 | } 47 | 48 | // Make sure the client and the server are connected through the Service. 49 | bool wait_param_server_ready() 50 | { 51 | bool ret = false; 52 | 53 | if(rclcpp::ok()) { 54 | RCLCPP_INFO(this->get_logger(), "Waiting 5 seconds to wait for the parameter server to be ready..."); 55 | ret = sync_param_client_->wait_for_service(5s); 56 | } 57 | 58 | return ret; 59 | } 60 | 61 | /* 62 | * Read the parameter value specified by `param_name`. 63 | * @param param_name The name of parameter. 64 | * @param parameter The vector that holds the read result. 65 | * @return Operation as expected or not. 66 | */ 67 | bool read_parameter(const std::string & param_name, std::vector & parameter); 68 | 69 | /* 70 | * Change the value of `param_name` to `param_value`. 71 | * The principle is to update if param exists, otherwise insert. 72 | * 73 | * @param param_name The name of parameter. 74 | * @param parameter_value The parameter value that you want to set. 75 | * @return Operation as expected or not. 76 | */ 77 | template 78 | bool modify_parameter(const std::string & param_name, const ValueType & param_value) 79 | { 80 | bool ret = true; 81 | std::vector parameters; 82 | 83 | parameters.push_back(rclcpp::Parameter(param_name, rclcpp::ParameterValue(param_value))); 84 | auto set_param_result = sync_param_client_->set_parameters(parameters); 85 | for (auto & result : set_param_result) 86 | { 87 | if (!result.successful) 88 | { 89 | RCLCPP_INFO(this->get_logger(), "SET OPERATION : Failed to set parameter: %s", result.reason.c_str()); 90 | return false; 91 | } 92 | } 93 | RCLCPP_INFO(this->get_logger(), "SET OPERATION : Set parameter %s successfully.", param_name.c_str()); 94 | 95 | return ret; 96 | } 97 | 98 | inline std::shared_ptr trigger_save() { 99 | auto trigger = std::make_shared(); 100 | auto fut = this->save_trigger_client_->async_send_request(trigger); 101 | rclcpp::spin_until_future_complete(this->get_node_base_interface(), fut); 102 | return fut.get(); 103 | } 104 | 105 | inline std::shared_ptr reload_yaml() { 106 | auto trigger = std::make_shared(); 107 | auto fut = this->reload_trigger_client_->async_send_request(trigger); 108 | rclcpp::spin_until_future_complete(this->get_node_base_interface(), fut); 109 | return fut.get(); 110 | } 111 | 112 | private: 113 | std::unique_ptr sync_param_client_; 114 | std::shared_ptr> save_trigger_client_; 115 | std::shared_ptr> reload_trigger_client_; 116 | }; 117 | 118 | #endif 119 | -------------------------------------------------------------------------------- /test/src/persist_parameter_client.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sony Corporation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include "persist_parameter_client.hpp" 20 | 21 | PersistParametersClient::PersistParametersClient( 22 | const std::string & client_name, 23 | const rclcpp::NodeOptions & node_options, 24 | const std::string & remote_node_name) 25 | : Node(client_name, node_options) 26 | { 27 | sync_param_client_ = std::make_unique(this, remote_node_name); 28 | save_trigger_client_ = create_client(remote_node_name + "/save_params"); 29 | reload_trigger_client_ = create_client(remote_node_name + "/reload_params"); 30 | } 31 | 32 | bool PersistParametersClient::read_parameter(const std::string & param_name, std::vector & parameter) 33 | { 34 | bool ret = true; 35 | 36 | parameter = sync_param_client_->get_parameters({param_name}); 37 | for(auto & param : parameter) 38 | { 39 | switch (param.get_type()) 40 | { 41 | case rclcpp::ParameterType::PARAMETER_NOT_SET: 42 | { 43 | RCLCPP_INFO(this->get_logger(), "READ OPERATION : parameter %s was not set(or deleted), it will not be stored", param_name.c_str()); 44 | break; 45 | } 46 | case rclcpp::ParameterType::PARAMETER_BOOL: 47 | { 48 | bool value = param.as_bool(); 49 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), value?"true":"false"); 50 | break; 51 | } 52 | case rclcpp::ParameterType::PARAMETER_INTEGER: 53 | { 54 | int64_t value = param.as_int(); 55 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %ld", param_name.c_str(), value); 56 | break; 57 | } 58 | case rclcpp::ParameterType::PARAMETER_DOUBLE: 59 | { 60 | double value = param.as_double(); 61 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %lf", param_name.c_str(), value); 62 | break; 63 | } 64 | case rclcpp::ParameterType::PARAMETER_STRING: 65 | { 66 | std::string value = param.as_string(); 67 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), value.c_str()); 68 | break; 69 | } 70 | case rclcpp::ParameterType::PARAMETER_BYTE_ARRAY: 71 | { 72 | std::ostringstream ss; 73 | auto array = param.as_byte_array(); 74 | format_array_output(ss, array); 75 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 76 | break; 77 | } 78 | case rclcpp::ParameterType::PARAMETER_BOOL_ARRAY: 79 | { 80 | std::ostringstream ss; 81 | auto array = param.as_bool_array(); 82 | format_array_output(ss, array); 83 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 84 | break; 85 | } 86 | case rclcpp::ParameterType::PARAMETER_INTEGER_ARRAY: 87 | { 88 | std::ostringstream ss; 89 | auto array = param.as_integer_array(); 90 | format_array_output(ss, array); 91 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 92 | break; 93 | } 94 | case rclcpp::ParameterType::PARAMETER_DOUBLE_ARRAY: 95 | { 96 | std::ostringstream ss; 97 | auto array = param.as_double_array(); 98 | format_array_output(ss, array); 99 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 100 | break; 101 | } 102 | case rclcpp::ParameterType::PARAMETER_STRING_ARRAY: 103 | { 104 | std::ostringstream ss; 105 | auto array = param.as_string_array(); 106 | format_array_output(ss, array); 107 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 108 | break; 109 | } 110 | default: { 111 | ret = false; 112 | RCLCPP_INFO(this->get_logger(), "parameter %s unsupported type %d", param_name.c_str(), param.get_type()); 113 | break; 114 | } 115 | } 116 | } 117 | 118 | return ret; 119 | } 120 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package persist_parameter_server 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 1.0.4 (2025-12-20) 6 | ------------------ 7 | * fix: save floats in explicit float notation (`#67 `_) 8 | * fix: save floats in explicit float notation 9 | YAML only has the concept of scalar values. So a value of '1' could be 10 | an integer, float or string. ros2_persist_parameter_server relies 11 | on being able to differentiate if something is a float or integer based 12 | on the representation of numbers. Floating numbers must be written such 13 | that they can not be mistaken as integers. This is comparable to how 14 | many programming languages assume '1' is an integer and '1.0' is a 15 | floating point number. 16 | The library yaml-cpp exports a float without the distinguishing feature 17 | required for ros2_persist_parameter_server. To fix this we manually 18 | convert the double into a string representation and ensure that 19 | a '.0' extension will be added when required. 20 | Note to future maintainer. The current yaml-cpp branch has a 21 | YAML::FpToString function which is better suited than using 22 | std::stringstream but is not available in the current yaml-cpp release. 23 | * fix: enforce a dot as decimal point 24 | * refactor convertDoubleToString function. 25 | * add test case to make sure double type can be handled. 26 | * remove this problem from known issue description in README.md. 27 | * skil auto-genrated CHANGELOG.rst from codespell checker. 28 | * use std::abs instead of c abs(). 29 | --------- 30 | Co-authored-by: Tomoya Fujita 31 | * Contributors: Simon Gene Gottlieb 32 | 33 | 1.0.3 (2025-12-08) 34 | ------------------ 35 | * update pdf and html presentation slides. 36 | * Contributors: Tomoya Fujita 37 | 38 | 1.0.2 (2025-10-21) 39 | ------------------ 40 | * Update package name in script and document 41 | * Change package name for other configuration files 42 | * Use a set of CMakeLists.txt and package.xml files for server and test 43 | * update README for additional service interfaces. (`#60 `_) 44 | * Added ros2 service call to manually trigger yaml save (`#38 `_) 45 | * remove ament_target_dependencies deprecation warnings. (`#59 `_) 46 | * a few follow-ups after https://github.com/fujitatomoya/ros2_persist_p… (`#56 `_) 47 | * Fix/dynamic typing (`#37 `_) 48 | * add tutorial video from The Construct. 49 | * declare dependencies to boost dev libraries (`#55 `_) 50 | * update markdown presenation. 51 | * add kubernetes examples and docs. (`#54 `_) 52 | * add JP markdown presentation for possible ROSCon JP 2025 LT. (`#53 `_) 53 | * enable Gemini CLI workflow. (`#51 `_) 54 | * remove ros signing key temporary workaround. (`#48 `_) 55 | * enable builtin dictionaries with custom ones. (`#47 `_) 56 | * support codespell github action. (`#46 `_) 57 | * Support Kilted Kaiju. (`#43 `_) 58 | * Remove use of ament_target_dependencies. (`#42 `_) 59 | * perform periodic storing of the yaml file. (`#36 `_) 60 | * add nightly workflow files for each distribution. (`#34 `_) 61 | * add system test to github workflow for all distributions. (`#32 `_) 62 | * fix github workflow target branch, should be rolling. (`#30 `_) 63 | * docker images release and build script, and doc update. (`#29 `_) 64 | * fix overview html markdown link. 65 | * Iron Irwini is End of Life. 66 | * add overview slide deck link on top. 67 | * Blank issue enabled for miscellaneous issues. (`#27 `_) 68 | * cosmetic fix for github issue template files. 69 | * update issue templates and configuration. 70 | * Mirror rolling to master branch. 71 | * Signal(SIGINT) needs to be injected to the server executable. (`#25 `_) 72 | * add jazzy to the presentation slides. 73 | * Jazzy support (`#23 `_) 74 | * update README and remove deprecation. 75 | * update cmake and package files (`#22 `_) 76 | * add work-around to load double sequence [1.0, 1.1]. (`#17 `_) 77 | * add presentation slide deck for lightning talk. (`#16 `_) 78 | * add issue templates for bug, featurea and question. 79 | * add github workflow status badges. (`#15 `_) 80 | * add build script issued in ros docker containers. 81 | * add github workflows template. 82 | * add supported ROS 2 distribution in doc. 83 | * fix bunch of readme uncrustify warnings. 84 | * Fix log statement of parameter type which are of an enum type 85 | * check if ros2 env is sourced before starting test. 86 | * update readme to explicitly show it is supported on ros:foxy. 87 | * Change package name 88 | * Modify based on comments 89 | * respawn in 5 seconds in default. 90 | * minor fix in doc. 91 | * Modify based on review 92 | * Support demos with client and verify basic functionalities 93 | * add persistent parameter scope and more samples for parameter load via launch and cli. 94 | * readme update with getting started. 95 | * 1st commit for parameter server with persistent cache feature. 96 | * Initial commit 97 | * Contributors: Tomoya Fujita, Ada-King, Barry Xu, Drew Hoener, Hugal31, Mahmoud, Roberto Zegers 98 | -------------------------------------------------------------------------------- /.github/workflows/gemini-issue-scheduled-triage.yml: -------------------------------------------------------------------------------- 1 | name: '📋 Gemini Scheduled Issue Triage' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 * * * *' # Runs every hour 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: '${{ github.workflow }}' 10 | cancel-in-progress: true 11 | 12 | defaults: 13 | run: 14 | shell: 'bash' 15 | 16 | permissions: 17 | contents: 'read' 18 | id-token: 'write' 19 | issues: 'write' 20 | statuses: 'write' 21 | 22 | jobs: 23 | triage-issues: 24 | timeout-minutes: 5 25 | runs-on: 'ubuntu-latest' 26 | steps: 27 | - name: 'Checkout repository' 28 | uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4 29 | 30 | - name: 'Generate GitHub App Token' 31 | id: 'generate_token' 32 | if: |- 33 | ${{ vars.APP_ID }} 34 | uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2 35 | with: 36 | app-id: '${{ vars.APP_ID }}' 37 | private-key: '${{ secrets.APP_PRIVATE_KEY }}' 38 | 39 | - name: 'Find untriaged issues' 40 | id: 'find_issues' 41 | env: 42 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 43 | GITHUB_REPOSITORY: '${{ github.repository }}' 44 | GITHUB_OUTPUT: '${{ github.output }}' 45 | run: |- 46 | set -euo pipefail 47 | 48 | echo '🔍 Finding issues without labels...' 49 | NO_LABEL_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \ 50 | --search 'is:open is:issue no:label' --json number,title,body)" 51 | 52 | echo '🏷️ Finding issues that need triage...' 53 | NEED_TRIAGE_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \ 54 | --search 'is:open is:issue label:"status/needs-triage"' --json number,title,body)" 55 | 56 | echo '🔄 Merging and deduplicating issues...' 57 | ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')" 58 | 59 | echo '📝 Setting output for GitHub Actions...' 60 | echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}" 61 | 62 | ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')" 63 | echo "✅ Found ${ISSUE_COUNT} issues to triage! 🎯" 64 | 65 | - name: 'Get Repository Labels' 66 | id: 'get_labels' 67 | uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' 68 | with: 69 | github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 70 | script: |- 71 | const { data: labels } = await github.rest.issues.listLabelsForRepo({ 72 | owner: context.repo.owner, 73 | repo: context.repo.repo, 74 | }); 75 | const labelNames = labels.map(label => label.name); 76 | core.setOutput('available_labels', labelNames.join(',')); 77 | core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`); 78 | return labelNames; 79 | 80 | - name: 'Run Gemini Issue Analysis' 81 | if: |- 82 | ${{ steps.find_issues.outputs.issues_to_triage != '[]' }} 83 | uses: 'google-github-actions/run-gemini-cli@v0' 84 | id: 'gemini_issue_analysis' 85 | env: 86 | GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs 87 | ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}' 88 | REPOSITORY: '${{ github.repository }}' 89 | AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}' 90 | with: 91 | gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}' 92 | gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}' 93 | gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}' 94 | gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}' 95 | gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' 96 | gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' 97 | use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}' 98 | use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}' 99 | settings: |- 100 | { 101 | "debug": ${{ fromJSON(env.DEBUG || env.ACTIONS_STEP_DEBUG || false) }}, 102 | "maxSessionTurns": 25, 103 | "coreTools": [ 104 | "run_shell_command(echo)" 105 | ], 106 | "telemetry": { 107 | "enabled": false, 108 | "target": "gcp" 109 | } 110 | } 111 | prompt: |- 112 | ## Role 113 | 114 | You are an issue triage assistant. Analyze the GitHub issues and 115 | identify the most appropriate existing labels to apply. 116 | 117 | ## Steps 118 | 119 | 1. Review the available labels in the environment variable: "${AVAILABLE_LABELS}". 120 | 2. Review the issues in the environment variable: "${ISSUES_TO_TRIAGE}". 121 | 3. For each issue, classify it by the appropriate labels from the available labels. 122 | 4. Output a JSON array of objects, each containing the issue number, 123 | the labels to set, and a brief explanation. For example: 124 | ``` 125 | [ 126 | { 127 | "issue_number": 123, 128 | "labels_to_set": ["kind/bug", "priority/p2"], 129 | "explanation": "This is a bug report with high priority based on the error description" 130 | }, 131 | { 132 | "issue_number": 456, 133 | "labels_to_set": ["kind/enhancement"], 134 | "explanation": "This is a feature request for improving the UI" 135 | } 136 | ] 137 | ``` 138 | 5. If an issue cannot be classified, do not include it in the output array. 139 | 140 | ## Guidelines 141 | 142 | - Only use labels that already exist in the repository 143 | - Assign all applicable labels based on the issue content 144 | - Reference all shell variables as "${VAR}" (with quotes and braces) 145 | - Output only valid JSON format 146 | - Do not include any explanation or additional text, just the JSON 147 | 148 | - name: 'Apply Labels to Issues' 149 | if: |- 150 | ${{ steps.gemini_issue_analysis.outcome == 'success' && 151 | steps.gemini_issue_analysis.outputs.summary != '[]' }} 152 | env: 153 | REPOSITORY: '${{ github.repository }}' 154 | LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}' 155 | uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' 156 | with: 157 | github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 158 | script: |- 159 | // Strip code block markers if present 160 | const rawLabels = process.env.LABELS_OUTPUT; 161 | core.info(`Raw labels JSON: ${rawLabels}`); 162 | let parsedLabels; 163 | try { 164 | const trimmedLabels = rawLabels.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim(); 165 | parsedLabels = JSON.parse(trimmedLabels); 166 | core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`); 167 | } catch (err) { 168 | core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`); 169 | return; 170 | } 171 | 172 | for (const entry of parsedLabels) { 173 | const issueNumber = entry.issue_number; 174 | if (!issueNumber) { 175 | core.info(`Skipping entry with no issue number: ${JSON.stringify(entry)}`); 176 | continue; 177 | } 178 | 179 | // Set labels based on triage result 180 | if (entry.labels_to_set && entry.labels_to_set.length > 0) { 181 | await github.rest.issues.setLabels({ 182 | owner: context.repo.owner, 183 | repo: context.repo.repo, 184 | issue_number: issueNumber, 185 | labels: entry.labels_to_set 186 | }); 187 | const explanation = entry.explanation ? ` - ${entry.explanation}` : ''; 188 | core.info(`Successfully set labels for #${issueNumber}: ${entry.labels_to_set.join(', ')}${explanation}`); 189 | } else { 190 | // If no labels to set, leave the issue as is 191 | core.info(`No labels to set for #${issueNumber}, leaving as is`); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /.github/workflows/gemini-issue-automated-triage.yml: -------------------------------------------------------------------------------- 1 | name: '🏷️ Gemini Automated Issue Triage' 2 | 3 | on: 4 | issues: 5 | types: 6 | - 'opened' 7 | - 'reopened' 8 | issue_comment: 9 | types: 10 | - 'created' 11 | workflow_dispatch: 12 | inputs: 13 | issue_number: 14 | description: 'issue number to triage' 15 | required: true 16 | type: 'number' 17 | 18 | concurrency: 19 | group: '${{ github.workflow }}-${{ github.event.issue.number }}' 20 | cancel-in-progress: true 21 | 22 | defaults: 23 | run: 24 | shell: 'bash' 25 | 26 | permissions: 27 | contents: 'read' 28 | id-token: 'write' 29 | issues: 'write' 30 | statuses: 'write' 31 | 32 | jobs: 33 | triage-issue: 34 | if: |- 35 | github.event_name == 'issues' || 36 | github.event_name == 'workflow_dispatch' || 37 | ( 38 | github.event_name == 'issue_comment' && 39 | contains(github.event.comment.body, '@gemini-cli /triage') && 40 | contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) 41 | ) 42 | timeout-minutes: 5 43 | runs-on: 'ubuntu-latest' 44 | steps: 45 | - name: 'Checkout repository' 46 | uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4 47 | 48 | - name: 'Generate GitHub App Token' 49 | id: 'generate_token' 50 | if: |- 51 | ${{ vars.APP_ID }} 52 | uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2 53 | with: 54 | app-id: '${{ vars.APP_ID }}' 55 | private-key: '${{ secrets.APP_PRIVATE_KEY }}' 56 | 57 | - name: 'Get Repository Labels' 58 | id: 'get_labels' 59 | uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' 60 | with: 61 | github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 62 | script: |- 63 | const { data: labels } = await github.rest.issues.listLabelsForRepo({ 64 | owner: context.repo.owner, 65 | repo: context.repo.repo, 66 | }); 67 | const labelNames = labels.map(label => label.name); 68 | core.setOutput('available_labels', labelNames.join(',')); 69 | core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`); 70 | return labelNames; 71 | 72 | - name: 'Run Gemini Issue Analysis' 73 | uses: 'google-github-actions/run-gemini-cli@v0' 74 | id: 'gemini_issue_analysis' 75 | env: 76 | GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs 77 | ISSUE_TITLE: '${{ github.event.issue.title }}' 78 | ISSUE_BODY: '${{ github.event.issue.body }}' 79 | ISSUE_NUMBER: '${{ github.event.issue.number }}' 80 | REPOSITORY: '${{ github.repository }}' 81 | AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}' 82 | with: 83 | gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}' 84 | gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}' 85 | gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}' 86 | gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}' 87 | gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' 88 | gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' 89 | use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}' 90 | use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}' 91 | settings: |- 92 | { 93 | "debug": ${{ fromJSON(env.DEBUG || env.ACTIONS_STEP_DEBUG || false) }}, 94 | "maxSessionTurns": 25, 95 | "coreTools": [ 96 | "run_shell_command(echo)" 97 | ], 98 | "telemetry": { 99 | "enabled": false, 100 | "target": "gcp" 101 | } 102 | } 103 | prompt: |- 104 | ## Role 105 | 106 | You are an issue triage assistant. Analyze the current GitHub issue 107 | and identify the most appropriate existing labels. Use the available 108 | tools to gather information; do not ask for information to be 109 | provided. 110 | 111 | ## Steps 112 | 113 | 1. Review the available labels in the environment variable: "${AVAILABLE_LABELS}". 114 | 2. Review the issue title and body provided in the environment 115 | variables: "${ISSUE_TITLE}" and "${ISSUE_BODY}". 116 | 3. Classify the issue by the appropriate labels from the available labels. 117 | 4. Output the appropriate labels for this issue in JSON format with explanation, for example: 118 | ``` 119 | {"labels_to_set": ["kind/bug", "priority/p0"], "explanation": "This is a critical bug report affecting main functionality"} 120 | ``` 121 | 5. If the issue cannot be classified using the available labels, output: 122 | ``` 123 | {"labels_to_set": [], "explanation": "Unable to classify this issue with available labels"} 124 | ``` 125 | 126 | ## Guidelines 127 | 128 | - Only use labels that already exist in the repository 129 | - Assign all applicable labels based on the issue content 130 | - Reference all shell variables as "${VAR}" (with quotes and braces) 131 | - Output only valid JSON format 132 | - Do not include any explanation or additional text, just the JSON 133 | 134 | - name: 'Apply Labels to Issue' 135 | if: |- 136 | ${{ steps.gemini_issue_analysis.outputs.summary != '' }} 137 | env: 138 | REPOSITORY: '${{ github.repository }}' 139 | ISSUE_NUMBER: '${{ github.event.issue.number }}' 140 | LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}' 141 | uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' 142 | with: 143 | github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 144 | script: |- 145 | // Strip code block markers if present 146 | const rawLabels = process.env.LABELS_OUTPUT; 147 | core.info(`Raw labels JSON: ${rawLabels}`); 148 | let parsedLabels; 149 | try { 150 | const trimmedLabels = rawLabels.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim(); 151 | parsedLabels = JSON.parse(trimmedLabels); 152 | core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`); 153 | } catch (err) { 154 | core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`); 155 | return; 156 | } 157 | 158 | const issueNumber = parseInt(process.env.ISSUE_NUMBER); 159 | 160 | // Set labels based on triage result 161 | if (parsedLabels.labels_to_set && parsedLabels.labels_to_set.length > 0) { 162 | await github.rest.issues.setLabels({ 163 | owner: context.repo.owner, 164 | repo: context.repo.repo, 165 | issue_number: issueNumber, 166 | labels: parsedLabels.labels_to_set 167 | }); 168 | const explanation = parsedLabels.explanation ? ` - ${parsedLabels.explanation}` : ''; 169 | core.info(`Successfully set labels for #${issueNumber}: ${parsedLabels.labels_to_set.join(', ')}${explanation}`); 170 | } else { 171 | // If no labels to set, leave the issue as is 172 | const explanation = parsedLabels.explanation ? ` - ${parsedLabels.explanation}` : ''; 173 | core.info(`No labels to set for #${issueNumber}, leaving as is${explanation}`); 174 | } 175 | 176 | - name: 'Post Issue Analysis Failure Comment' 177 | if: |- 178 | ${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }} 179 | env: 180 | ISSUE_NUMBER: '${{ github.event.issue.number }}' 181 | RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' 182 | uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' 183 | with: 184 | github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 185 | script: |- 186 | github.rest.issues.createComment({ 187 | owner: context.repo.owner, 188 | repo: context.repo.repo, 189 | issue_number: parseInt(process.env.ISSUE_NUMBER), 190 | body: 'There is a problem with the Gemini CLI issue triaging. Please check the [action logs](${process.env.RUN_URL}) for details.' 191 | }) 192 | -------------------------------------------------------------------------------- /test/src/test_default.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sony Corporation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "persist_parameter_client.hpp" 21 | #include "test_common.h" 22 | 23 | rclcpp::Logger TestPersistParameter::client_logger_ = rclcpp::get_logger("client"); 24 | 25 | int main(int argc, char ** argv) 26 | { 27 | // force flush of the stdout buffer. 28 | // this ensures a correct sync of all prints 29 | // even when executed simultaneously within the launch file. 30 | setvbuf(stdout, NULL, _IONBF, BUFSIZ); 31 | 32 | rclcpp::init(argc, argv); 33 | std::shared_ptr test_client; 34 | 35 | int ret_code = 0; 36 | // In case of an exception is thrown when performing an operation after `ctrl-c` occurred. 37 | try { 38 | test_client = std::make_shared("client", rclcpp::NodeOptions()); 39 | /* 40 | * First read parameter(include normal parameter and persistent parameter), to confirm the initial value of parameters. 41 | * Parameter server is launched with file `/tmp/parameter_server.yaml`, in this file, parameter `a_string` is defined 42 | * and the initial value is `Hello world`. 43 | * 44 | * Test: The default parameter `a_string` specified in YAML file which is loaded while parameter server is launched 45 | * should be read correctly. 46 | */ 47 | { 48 | // If return fail, no need to do the following. 49 | RCLCPP_INFO(test_client->get_logger(), "First read the initial value of parameter : "); 50 | test_client->do_read_and_check( 51 | "a_string", "Hello world", "a. Read Normal Parameter"); 52 | test_client->do_read_and_check( 53 | "persistent.a_string", "Hello world", "b. Read Persistent Parameter"); 54 | } 55 | 56 | /* 57 | * Test: Modifying the parameter `a_string`'s value to `Hello`, and add a new parameter `new_string` to YAML file. 58 | */ 59 | { 60 | RCLCPP_INFO(test_client->get_logger(), "Change the value of parameter to `Hello` : "); 61 | test_client->do_change_and_check( 62 | "a_string", std::string{"Hello"}, "c. Modify Existed Normal parameter"); 63 | test_client->do_change_and_check( 64 | "persistent.a_string", std::string{"Hello"}, "d. Modify Existed Persistent parameter"); 65 | RCLCPP_INFO(test_client->get_logger(), "Add a new parameter to parameter file : "); 66 | test_client->do_change_and_check( 67 | "new_string", std::string{"Hello NewString"}, "e. Add New Normal parameter"); 68 | test_client->do_change_and_check( 69 | "persistent.new_string", std::string{"Hello NewString"}, "f. Add New Persistent parameter"); 70 | } 71 | 72 | // Waiting for the server to restart. 73 | std::this_thread::sleep_for(std::chrono::seconds(5)); 74 | 75 | /* 76 | * Test : Reading parameter value again to confirm whether to store the modified persistent/normal parameter to the file. 77 | */ 78 | { 79 | if (!test_client->wait_param_server_ready()) { 80 | throw NoServerError(); 81 | } 82 | RCLCPP_INFO( 83 | test_client->get_logger(), 84 | "Last read the value of parameter after server restarts," 85 | "to check whether changes stores to the file : "); 86 | test_client->do_read_and_check( 87 | "a_string", "Hello world", "g. Test Normal Parameter Not Stores To File"); 88 | test_client->do_read_and_check( 89 | "persistent.a_string", "Hello", "h. Test Persistent Parameter Stores To File"); 90 | test_client->do_read_and_check( 91 | "new_string", std::nullopt, "i. Test New Added Normal Parameter Not Stores To File"); 92 | test_client->do_read_and_check( 93 | "persistent.new_string", "Hello NewString", 94 | "j. Test New Added Persistent Parameter Stores To File"); 95 | } 96 | 97 | /* 98 | * Test : Impossible to change the type of a parameter. 99 | */ 100 | { 101 | RCLCPP_INFO( 102 | test_client->get_logger(), "Try to change the type of a parameter, must not be possible:"); 103 | test_client->do_fail_to_change( 104 | "persistent.a_string", 10, "k. Test could not change the type of persistent parameter"); 105 | test_client->do_fail_to_change( 106 | "some_int", "Not possible", "l. Test could not change the type of parameter"); 107 | } 108 | 109 | /* 110 | * Test: Modifying parameters the same as above but saving the file and then checking. 111 | */ 112 | { 113 | RCLCPP_INFO(test_client->get_logger(), "Change the value of parameter to `Hello` : "); 114 | test_client->do_save_and_check("persistent.test_saved_first", "Hello", "m. Set and saved new parameter successfully"); 115 | RCLCPP_INFO(test_client->get_logger(), "Add a new parameter to parameter file : "); 116 | test_client->do_save_and_check("persistent.test_saved_second", "SecondString", "n. Set and saved new parameter successfully"); 117 | RCLCPP_INFO(test_client->get_logger(), "Update and save a parameter and make sure it saved successfully : "); 118 | test_client->do_save_and_check("persistent.test_saved_first", "World", "o. Set and saved existing parameter successfully"); 119 | RCLCPP_INFO(test_client->get_logger(), "Update a parameter and reload without saving : "); 120 | test_client->do_reload_and_check("persistent.test_saved_second", "Discarded", "SecondString", "p. Set and saved new parameter successfully"); 121 | } 122 | 123 | /* 124 | * Test: Double precision handling (Issue #13) 125 | * Test that double values maintain their precision through save/reload cycles. 126 | */ 127 | { 128 | RCLCPP_INFO(test_client->get_logger(), "Testing double precision handling:"); 129 | // Test high precision double value 130 | double high_precision_value = 3.141592653589793; 131 | test_client->do_save_and_check( 132 | "persistent.high_precision_double", high_precision_value, 133 | "q. Set and saved high precision double successfully"); 134 | 135 | // Test another high precision value with many decimal places 136 | double scientific_value = 1.23456789012345e-10; 137 | test_client->do_save_and_check( 138 | "persistent.scientific_notation_double", scientific_value, 139 | "r. Set and saved double in scientific notation successfully"); 140 | 141 | // Test double that could lose precision (e.g., 10.0 should not become 10) 142 | double trailing_zero_value = 10.0; 143 | test_client->do_save_and_check( 144 | "persistent.trailing_zero_double", trailing_zero_value, 145 | "s. Set and saved double with trailing zero successfully"); 146 | } 147 | 148 | /* 149 | * Test: Double array precision handling (Issue #13) 150 | * Test that double arrays maintain their precision through save/reload cycles. 151 | */ 152 | { 153 | RCLCPP_INFO(test_client->get_logger(), "Testing double array precision handling:"); 154 | // Test double array with high precision values 155 | std::vector high_precision_array = { 156 | 3.141592653589793, 2.718281828459045, 1.41421356237309504880168872420969807856967187537694 157 | }; 158 | test_client->do_save_and_check>( 159 | "persistent.high_precision_double_array", high_precision_array, 160 | "t. Set and saved high precision double array successfully"); 161 | 162 | // Test double array with mixed notation values 163 | std::vector mixed_array = {10.0, 1.5e-8, 999.999999, 0.000000001}; 164 | test_client->do_save_and_check>( 165 | "persistent.mixed_notation_double_array", mixed_array, 166 | "u. Set and saved mixed notation double array successfully"); 167 | } 168 | 169 | } catch (const rclcpp::exceptions::RCLError & e) { 170 | ret_code = -1; 171 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 172 | } catch (const NoServerError & e) { 173 | ret_code = -2; 174 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 175 | } catch (const SetOperationError & e) { 176 | ret_code = -3; 177 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 178 | } 179 | 180 | // if any tests are not passed, return EXIT_FAILURE. 181 | ret_code = test_client->print_result(); 182 | rclcpp::shutdown(); 183 | 184 | return ret_code; 185 | } 186 | -------------------------------------------------------------------------------- /test/include/test_common.h: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Sony Corporation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef __TEST_COMMON_H__ 16 | #define __TEST_COMMON_H__ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "persist_parameter_client.hpp" 24 | 25 | /** 26 | * NoServerError 27 | * 28 | * The client will wait 5 seconds for the server to be ready. 29 | * If timeout, then throw an exception to terminate the endless waiting. 30 | */ 31 | struct NoServerError : public std::runtime_error 32 | { 33 | public: 34 | NoServerError() : std::runtime_error("cannot connect to server") {} 35 | }; 36 | 37 | /* 38 | * SetOperationError 39 | * 40 | * When executing `set_parameter`, if the set operation failed, 41 | * throw an exception to ignore the subsequent test. 42 | */ 43 | struct SetOperationError : public std::runtime_error 44 | { 45 | public: 46 | SetOperationError() : std::runtime_error("set operation failed") {} 47 | }; 48 | 49 | class TestPersistParameter 50 | { 51 | public: 52 | TestPersistParameter(const std::string & node_name, const rclcpp::NodeOptions & options) 53 | : persist_param_client_(node_name, options) 54 | { 55 | if (!wait_param_server_ready()) { 56 | throw NoServerError(); 57 | } 58 | } 59 | 60 | inline bool wait_param_server_ready() { return persist_param_client_.wait_param_server_ready(); } 61 | 62 | /* 63 | * Read the value of parameter. 64 | * @param param_name The name of parameter. 65 | * @param expected_value The value of the parameter that you expected, take std::string as example here. 66 | * If `expected_value` is `nullopt`, it means the parameter is expected not to exist. 67 | * @param testcase The test case description. 68 | */ 69 | template 70 | void do_read_and_check( 71 | const std::string & param_name, const std::optional & expected_value, 72 | const std::string & testcase) 73 | { 74 | bool value_matches = false; 75 | std::vector parameter; 76 | 77 | if (persist_param_client_.read_parameter(param_name, parameter)) { 78 | for (auto & param : parameter) { 79 | if (!expected_value.has_value()) { 80 | if (param.get_type() == rclcpp::ParameterType::PARAMETER_NOT_SET) { 81 | value_matches = true; 82 | break; 83 | } 84 | } else { 85 | switch (param.get_type()) { 86 | case rclcpp::ParameterType::PARAMETER_STRING: 87 | if constexpr (std::is_same_v) { 88 | if (param.as_string() == expected_value.value()) { 89 | value_matches = true; 90 | } 91 | } 92 | break; 93 | case rclcpp::ParameterType::PARAMETER_INTEGER: 94 | if constexpr (std::is_integral_v) { 95 | if (param.as_int() == expected_value.value()) { 96 | value_matches = true; 97 | } 98 | } 99 | break; 100 | case rclcpp::ParameterType::PARAMETER_DOUBLE: 101 | if constexpr (std::is_floating_point_v) { 102 | if ( 103 | std::abs(param.as_double() - expected_value.value()) < 104 | std::numeric_limits::epsilon()) { 105 | value_matches = true; 106 | } 107 | } 108 | break; 109 | case rclcpp::ParameterType::PARAMETER_DOUBLE_ARRAY: 110 | if constexpr (std::is_same_v>) { 111 | auto actual_array = param.as_double_array(); 112 | auto expected_array = expected_value.value(); 113 | if (actual_array.size() == expected_array.size()) { 114 | value_matches = true; 115 | for (size_t i = 0; i < actual_array.size(); ++i) { 116 | if (std::abs(actual_array[i] - expected_array[i]) >= std::numeric_limits::epsilon()) { 117 | value_matches = false; 118 | RCLCPP_WARN( 119 | this->get_logger(), 120 | "Double array mismatch at index %zu: actual=%.17g, expected=%.17g, diff=%.17g", 121 | i, actual_array[i], expected_array[i], std::abs(actual_array[i] - expected_array[i])); 122 | break; 123 | } 124 | } 125 | } 126 | } 127 | break; 128 | default: 129 | break; 130 | } 131 | } 132 | } 133 | } 134 | 135 | /* 136 | * Even if the Get operation failed, record it in result_map, and it shouldn't effect the 137 | * subsequent tests. 138 | */ 139 | this->set_result(testcase, value_matches); 140 | } 141 | 142 | /* 143 | * Change the value of parameter. 144 | * @param param_name The name of parameter. 145 | * @param changed_value The value that you want to set. 146 | * @param testcase The test case description. 147 | */ 148 | template 149 | void do_change_and_check( 150 | const std::string & param_name, const ValueType & changed_value, const std::string & testcase) 151 | { 152 | bool ret = persist_param_client_.modify_parameter(param_name, changed_value); 153 | /* 154 | * If the Modify operation failed, record it in result_map, and no need to run the 155 | * subsequent read tests. 156 | */ 157 | if (!ret) { 158 | this->set_result(testcase, false); 159 | throw SetOperationError(); 160 | } 161 | 162 | do_read_and_check(param_name, std::make_optional(changed_value), testcase); 163 | } 164 | 165 | template 166 | void do_fail_to_change( 167 | const std::string & param_name, const ValueType & attempted_value, const std::string & testcase) 168 | { 169 | bool ret = persist_param_client_.modify_parameter(param_name, attempted_value); 170 | 171 | // this must fail. So set result to true if ret is false 172 | this->set_result(testcase, !ret); 173 | } 174 | 175 | /* 176 | * Used to check that reloading works. If save hasn't been called, parameters should be overwritten. 177 | * @param param_name The name of parameter. 178 | * @param changed_value The value that you want to set. 179 | * @param testcase The test case description. 180 | */ 181 | template 182 | void do_reload_and_check(const std::string & param_name, const ValueType & changed_value, const std::optional & expected_value, const std::string & testcase) { 183 | bool ret = false; 184 | 185 | ret = persist_param_client_.modify_parameter(param_name, changed_value); 186 | /* 187 | * If the Modify operation failed, record it in result_map, and no need to run the 188 | * subsequent read tests. 189 | */ 190 | if(!ret) { 191 | this->set_result(testcase, false); 192 | throw SetOperationError(); 193 | } 194 | 195 | /** 196 | * Attempt to reload the YAML file 197 | */ 198 | auto reload_res = persist_param_client_.reload_yaml(); 199 | if(!reload_res || !reload_res->success) { 200 | this->set_result(testcase, false); 201 | throw SetOperationError(); 202 | } 203 | 204 | return do_read_and_check(param_name, expected_value, testcase); 205 | } 206 | 207 | /* 208 | * Change the value of parameter, save, read, then check. 209 | * @param param_name The name of parameter. 210 | * @param changed_value The value that you want to set. 211 | * @param testcase The test case description. 212 | */ 213 | template 214 | void do_save_and_check(const std::string & param_name, const ValueType & changed_value, const std::string & testcase) { 215 | bool ret = false; 216 | 217 | ret = persist_param_client_.modify_parameter(param_name, changed_value); 218 | /* 219 | * If the Modify operation failed, record it in result_map, and no need to run the 220 | * subsequent read tests. 221 | */ 222 | if(!ret) { 223 | this->set_result(testcase, false); 224 | throw SetOperationError(); 225 | } 226 | 227 | /** 228 | * Manually trigger a save, if it returns false then there must have been an error in saving. 229 | */ 230 | auto save_res = persist_param_client_.trigger_save(); 231 | if(!save_res || !save_res->success) { 232 | this->set_result(testcase, false); 233 | throw SetOperationError(); 234 | } 235 | 236 | /** 237 | * Attempt to reload the YAML file 238 | */ 239 | auto reload_res = persist_param_client_.reload_yaml(); 240 | if(!reload_res || !reload_res->success) { 241 | this->set_result(testcase, false); 242 | throw SetOperationError(); 243 | } 244 | 245 | return do_read_and_check(param_name, changed_value, testcase); 246 | } 247 | 248 | // Get all test results. 249 | inline int print_result() const 250 | { 251 | int ret = EXIT_SUCCESS; 252 | RCLCPP_INFO( 253 | this->get_logger(), 254 | "****************************************************" 255 | "***********************"); 256 | RCLCPP_INFO( 257 | this->get_logger(), 258 | "*********************************Test Result*********" 259 | "**********************"); 260 | for (const auto & res : result_map_) { 261 | RCLCPP_INFO( 262 | this->get_logger(), "%-60s : %16s", res.first.c_str(), res.second ? "PASS" : "NOT PASS"); 263 | 264 | // if any tests are not passed, return EXIT_FAILURE. 265 | if (res.second == false) { 266 | ret = EXIT_FAILURE; 267 | } 268 | } 269 | 270 | return ret; 271 | } 272 | 273 | static inline rclcpp::Logger get_logger() { return client_logger_; } 274 | 275 | private: 276 | // Save the result of each test operation. 277 | inline void set_result(const std::string & key, bool value) 278 | { 279 | auto pair = result_map_.insert({key, value}); 280 | if (!pair.second) { 281 | RCLCPP_INFO(this->get_logger(), "Failed when insert %s to result_map", key.c_str()); 282 | } 283 | 284 | return; 285 | } 286 | 287 | PersistParametersClient persist_param_client_; 288 | std::map result_map_; 289 | static rclcpp::Logger client_logger_; 290 | }; 291 | 292 | #endif 293 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /.github/workflows/gemini-cli.yml: -------------------------------------------------------------------------------- 1 | name: '💬 Gemini CLI' 2 | 3 | on: 4 | pull_request_review_comment: 5 | types: 6 | - 'created' 7 | pull_request_review: 8 | types: 9 | - 'submitted' 10 | issue_comment: 11 | types: 12 | - 'created' 13 | 14 | concurrency: 15 | group: '${{ github.workflow }}-${{ github.event.issue.number }}' 16 | cancel-in-progress: |- 17 | ${{ github.event.sender.type == 'User' && ( github.event.issue.author_association == 'OWNER' || github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'COLLABORATOR') }} 18 | 19 | defaults: 20 | run: 21 | shell: 'bash' 22 | 23 | permissions: 24 | contents: 'write' 25 | id-token: 'write' 26 | pull-requests: 'write' 27 | issues: 'write' 28 | 29 | jobs: 30 | gemini-cli: 31 | # This condition seeks to ensure the action is only run when it is triggered by a trusted user. 32 | # For private repos, users who have access to the repo are considered trusted. 33 | # For public repos, users who members, owners, or collaborators are considered trusted. 34 | if: |- 35 | github.event_name == 'workflow_dispatch' || 36 | ( 37 | github.event_name == 'issues' && github.event.action == 'opened' && 38 | contains(github.event.issue.body, '@gemini-cli') && 39 | !contains(github.event.issue.body, '@gemini-cli /review') && 40 | !contains(github.event.issue.body, '@gemini-cli /triage') && 41 | ( 42 | github.event.repository.private == true || 43 | contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association) 44 | ) 45 | ) || 46 | ( 47 | ( 48 | github.event_name == 'issue_comment' || 49 | github.event_name == 'pull_request_review_comment' 50 | ) && 51 | contains(github.event.comment.body, '@gemini-cli') && 52 | !contains(github.event.comment.body, '@gemini-cli /review') && 53 | !contains(github.event.comment.body, '@gemini-cli /triage') && 54 | ( 55 | github.event.repository.private == true || 56 | contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) 57 | ) 58 | ) || 59 | ( 60 | github.event_name == 'pull_request_review' && 61 | contains(github.event.review.body, '@gemini-cli') && 62 | !contains(github.event.review.body, '@gemini-cli /review') && 63 | !contains(github.event.review.body, '@gemini-cli /triage') && 64 | ( 65 | github.event.repository.private == true || 66 | contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association) 67 | ) 68 | ) 69 | timeout-minutes: 10 70 | runs-on: 'ubuntu-latest' 71 | steps: 72 | - name: 'Generate GitHub App Token' 73 | id: 'generate_token' 74 | if: |- 75 | ${{ vars.APP_ID }} 76 | uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2 77 | with: 78 | app-id: '${{ vars.APP_ID }}' 79 | private-key: '${{ secrets.APP_PRIVATE_KEY }}' 80 | 81 | - name: 'Get context from event' 82 | id: 'get_context' 83 | env: 84 | EVENT_NAME: '${{ github.event_name }}' 85 | EVENT_PAYLOAD: '${{ toJSON(github.event) }}' 86 | run: |- 87 | set -euo pipefail 88 | 89 | USER_REQUEST="" 90 | ISSUE_NUMBER="" 91 | IS_PR="false" 92 | 93 | if [[ "${EVENT_NAME}" == "issues" ]]; then 94 | USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.body) 95 | ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.number) 96 | elif [[ "${EVENT_NAME}" == "issue_comment" ]]; then 97 | USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .comment.body) 98 | ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.number) 99 | if [[ $(echo "${EVENT_PAYLOAD}" | jq -r .issue.pull_request) != "null" ]]; then 100 | IS_PR="true" 101 | fi 102 | elif [[ "${EVENT_NAME}" == "pull_request_review" ]]; then 103 | USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .review.body) 104 | ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .pull_request.number) 105 | IS_PR="true" 106 | elif [[ "${EVENT_NAME}" == "pull_request_review_comment" ]]; then 107 | USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .comment.body) 108 | ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .pull_request.number) 109 | IS_PR="true" 110 | fi 111 | 112 | # Clean up user request 113 | USER_REQUEST=$(echo "${USER_REQUEST}" | sed 's/.*@gemini-cli//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') 114 | 115 | { 116 | echo "user_request=${USER_REQUEST}" 117 | echo "issue_number=${ISSUE_NUMBER}" 118 | echo "is_pr=${IS_PR}" 119 | } >> "${GITHUB_OUTPUT}" 120 | 121 | - name: 'Set up git user for commits' 122 | run: |- 123 | git config --global user.name 'gemini-cli[bot]' 124 | git config --global user.email 'gemini-cli[bot]@users.noreply.github.com' 125 | 126 | - name: 'Checkout PR branch' 127 | if: |- 128 | ${{ steps.get_context.outputs.is_pr == 'true' }} 129 | uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4 130 | with: 131 | token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 132 | repository: '${{ github.repository }}' 133 | ref: 'refs/pull/${{ steps.get_context.outputs.issue_number }}/head' 134 | fetch-depth: 0 135 | 136 | - name: 'Checkout main branch' 137 | if: |- 138 | ${{ steps.get_context.outputs.is_pr == 'false' }} 139 | uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4 140 | with: 141 | token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 142 | repository: '${{ github.repository }}' 143 | fetch-depth: 0 144 | 145 | - name: 'Acknowledge request' 146 | env: 147 | GITHUB_ACTOR: '${{ github.actor }}' 148 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 149 | ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}' 150 | REPOSITORY: '${{ github.repository }}' 151 | REQUEST_TYPE: '${{ steps.get_context.outputs.request_type }}' 152 | run: |- 153 | set -euo pipefail 154 | MESSAGE="@${GITHUB_ACTOR} I've received your request and I'm working on it now! 🤖" 155 | if [[ -n "${MESSAGE}" ]]; then 156 | gh issue comment "${ISSUE_NUMBER}" \ 157 | --body "${MESSAGE}" \ 158 | --repo "${REPOSITORY}" 159 | fi 160 | 161 | - name: 'Get description' 162 | id: 'get_description' 163 | env: 164 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 165 | IS_PR: '${{ steps.get_context.outputs.is_pr }}' 166 | ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}' 167 | run: |- 168 | set -euo pipefail 169 | if [[ "${IS_PR}" == "true" ]]; then 170 | DESCRIPTION=$(gh pr view "${ISSUE_NUMBER}" --json body --template '{{.body}}') 171 | else 172 | DESCRIPTION=$(gh issue view "${ISSUE_NUMBER}" --json body --template '{{.body}}') 173 | fi 174 | { 175 | echo "description<> "${GITHUB_OUTPUT}" 179 | 180 | - name: 'Get comments' 181 | id: 'get_comments' 182 | env: 183 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 184 | IS_PR: '${{ steps.get_context.outputs.is_pr }}' 185 | ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}' 186 | run: |- 187 | set -euo pipefail 188 | if [[ "${IS_PR}" == "true" ]]; then 189 | COMMENTS=$(gh pr view "${ISSUE_NUMBER}" --json comments --template '{{range .comments}}{{.author.login}}: {{.body}}{{"\n"}}{{end}}') 190 | else 191 | COMMENTS=$(gh issue view "${ISSUE_NUMBER}" --json comments --template '{{range .comments}}{{.author.login}}: {{.body}}{{"\n"}}{{end}}') 192 | fi 193 | { 194 | echo "comments<> "${GITHUB_OUTPUT}" 198 | 199 | - name: 'Run Gemini' 200 | id: 'run_gemini' 201 | uses: 'google-github-actions/run-gemini-cli@v0' 202 | env: 203 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 204 | REPOSITORY: '${{ github.repository }}' 205 | USER_REQUEST: '${{ steps.get_context.outputs.user_request }}' 206 | ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}' 207 | IS_PR: '${{ steps.get_context.outputs.is_pr }}' 208 | with: 209 | gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' 210 | gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}' 211 | gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}' 212 | gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}' 213 | gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' 214 | use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}' 215 | use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}' 216 | settings: |- 217 | { 218 | "debug": ${{ fromJSON(env.DEBUG || env.ACTIONS_STEP_DEBUG || false) }}, 219 | "maxSessionTurns": 50, 220 | "telemetry": { 221 | "enabled": false, 222 | "target": "gcp" 223 | } 224 | } 225 | prompt: |- 226 | ## Role 227 | 228 | You are a helpful AI assistant invoked via a CLI interface in a GitHub workflow. You have access to tools to interact with the repository and respond to the user. 229 | 230 | ## Context 231 | 232 | - **Repository**: `${{ github.repository }}` 233 | - **Triggering Event**: `${{ github.event_name }}` 234 | - **Issue/PR Number**: `${{ steps.get_context.outputs.issue_number }}` 235 | - **Is this a PR?**: `${{ steps.get_context.outputs.is_pr }}` 236 | - **Issue/PR Description**: 237 | `${{ steps.get_description.outputs.description }}` 238 | - **Comments**: 239 | `${{ steps.get_comments.outputs.comments }}` 240 | 241 | ## User Request 242 | 243 | The user has sent the following request: 244 | `${{ steps.get_context.outputs.user_request }}` 245 | 246 | ## How to Respond to Issues, PR Comments, and Questions 247 | 248 | This workflow supports three main scenarios: 249 | 250 | 1. **Creating a Fix for an Issue** 251 | - Carefully read the user request and the related issue or PR description. 252 | - Use available tools to gather all relevant context (e.g., `gh issue view`, `gh pr view`, `gh pr diff`, `cat`, `head`, `tail`). 253 | - Identify the root cause of the problem before proceeding. 254 | - **Show and maintain a plan as a checklist**: 255 | - At the very beginning, outline the steps needed to resolve the issue or address the request and post them as a checklist comment on the issue or PR (use GitHub markdown checkboxes: `- [ ] Task`). 256 | - Example: 257 | ``` 258 | ### Plan 259 | - [ ] Investigate the root cause 260 | - [ ] Implement the fix in `file.py` 261 | - [ ] Add/modify tests 262 | - [ ] Update documentation 263 | - [ ] Verify the fix and close the issue 264 | ``` 265 | - Use: `gh pr comment "${ISSUE_NUMBER}" --body ""` or `gh issue comment "${ISSUE_NUMBER}" --body ""` to post the initial plan. 266 | - As you make progress, keep the checklist visible and up to date by editing the same comment (check off completed tasks with `- [x]`). 267 | - To update the checklist: 268 | 1. Find the comment ID for the checklist (use `gh pr comment list "${ISSUE_NUMBER}"` or `gh issue comment list "${ISSUE_NUMBER}"`). 269 | 2. Edit the comment with the updated checklist: 270 | - For PRs: `gh pr comment --edit --body ""` 271 | - For Issues: `gh issue comment --edit --body ""` 272 | 3. The checklist should only be maintained as a comment on the issue or PR. Do not track or update the checklist in code files. 273 | - If the fix requires code changes, determine which files and lines are affected. If clarification is needed, note any questions for the user. 274 | - Make the necessary code or documentation changes using the available tools (e.g., `write_file`). Ensure all changes follow project conventions and best practices. Reference all shell variables as `"${VAR}"` (with quotes and braces) to prevent errors. 275 | - Run any relevant tests or checks to verify the fix works as intended. If possible, provide evidence (test output, screenshots, etc.) that the issue is resolved. 276 | - **Branching and Committing**: 277 | - **NEVER commit directly to the `main` branch.** 278 | - If you are working on a **pull request** (`IS_PR` is `true`), the correct branch is already checked out. Simply commit and push to it. 279 | - `git add .` 280 | - `git commit -m "feat: "` 281 | - `git push` 282 | - If you are working on an **issue** (`IS_PR` is `false`), create a new branch for your changes. A good branch name would be `issue/${ISSUE_NUMBER}/`. 283 | - `git checkout -b issue/${ISSUE_NUMBER}/my-fix` 284 | - `git add .` 285 | - `git commit -m "feat: "` 286 | - `git push origin issue/${ISSUE_NUMBER}/my-fix` 287 | - After pushing, you can create a pull request: `gh pr create --title "Fixes #${ISSUE_NUMBER}: " --body "This PR addresses issue #${ISSUE_NUMBER}."` 288 | - Summarize what was changed and why in a markdown file: `write_file("response.md", "")` 289 | - Post the response as a comment: 290 | - For PRs: `gh pr comment "${ISSUE_NUMBER}" --body-file response.md` 291 | - For Issues: `gh issue comment "${ISSUE_NUMBER}" --body-file response.md` 292 | 293 | 2. **Addressing Comments on a Pull Request** 294 | - Read the specific comment and the context of the PR. 295 | - Use tools like `gh pr view`, `gh pr diff`, and `cat` to understand the code and discussion. 296 | - If the comment requests a change or clarification, follow the same process as for fixing an issue: create a checklist plan, implement, test, and commit any required changes, updating the checklist as you go. 297 | - **Committing Changes**: The correct PR branch is already checked out. Simply add, commit, and push your changes. 298 | - `git add .` 299 | - `git commit -m "fix: address review comments"` 300 | - `git push` 301 | - If the comment is a question, answer it directly and clearly, referencing code or documentation as needed. 302 | - Document your response in `response.md` and post it as a PR comment: `gh pr comment "${ISSUE_NUMBER}" --body-file response.md` 303 | 304 | 3. **Answering Any Question on an Issue** 305 | - Read the question and the full issue context using `gh issue view` and related tools. 306 | - Research or analyze the codebase as needed to provide an accurate answer. 307 | - If the question requires code or documentation changes, follow the fix process above, including creating and updating a checklist plan and **creating a new branch for your changes as described in section 1.** 308 | - Write a clear, concise answer in `response.md` and post it as an issue comment: `gh issue comment "${ISSUE_NUMBER}" --body-file response.md` 309 | 310 | ## Guidelines 311 | 312 | - **Be concise and actionable.** Focus on solving the user's problem efficiently. 313 | - **Always commit and push your changes if you modify code or documentation.** 314 | - **If you are unsure about the fix or answer, explain your reasoning and ask clarifying questions.** 315 | - **Follow project conventions and best practices.** 316 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![humble](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/humble.yml/badge.svg)](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/humble.yml) [![jazzy](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/jazzy.yml/badge.svg)](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/jazzy.yml) [![kilted](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/kilted.yml/badge.svg)](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/kilted.yml) [![rolling](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/rolling.yml/badge.svg)](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/rolling.yml) 2 | [![humble-nightly](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/humble-nightly.yml/badge.svg)](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/humble-nightly.yml) [![jazzy-nightly](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/jazzy-nightly.yml/badge.svg)](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/jazzy-nightly.yml) [![kilted-nightly](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/kilted-nightly.yml/badge.svg)](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/kilted-nightly.yml) [![rolling-nightly](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/rolling-nightly.yml/badge.svg)](https://github.com/fujitatomoya/ros2_persist_parameter_server/actions/workflows/rolling-nightly.yml) 3 | 4 | # ROS2 Persistent Parameter Server 5 | 6 | ROS 2 Persistent Parameter Server, that resides in the ROS 2 system to serve the parameter daemon. The other nodes(e.g the client demo provided in the code) can write/read the parameter in Parameter Server, and ***Parameter Server is able to store the parameter into the persistent storage which user can specify such as tmpfs, nfs, or disk.*** 7 | 8 | See [overview slide deck](https://raw.githack.com/fujitatomoya/ros2_persist_parameter_server/rolling/presentation/ros2_parameter_server.html) for general information. 9 | 10 | 11 | 12 | - [ROS2 Persistent Parameter Server](#ros2-persistent-parameter-server) 13 | - [Background](#background) 14 | - [Overview](#overview) 15 | - [Persistent Parameter Registration](#persistent-parameter-registration) 16 | - [Persistent Prefix](#persistent-prefix) 17 | - [Scope Overview](#scope-overview) 18 | - [Configurable Options](#configurable-options) 19 | - [Services](#services) 20 | - [Sequence](#sequence) 21 | - [Getting Started](#getting-started) 22 | - [Supported Distribution](#supported-distribution) 23 | - [Docker Container](#docker-container) 24 | - [Dependent Packages](#dependent-packages) 25 | - [Prerequisites](#prerequisites) 26 | - [Build](#build) 27 | - [Run](#run) 28 | - [Kubernetes](#kubernetes) 29 | - [Test](#test) 30 | - [Run](#run-1) 31 | - [Known Issues](#known-issues) 32 | - [Authors](#authors) 33 | - [License](#license) 34 | 35 | 36 | 37 | ## Background 38 | 39 | The discussion is opened [here](https://discourse.ros.org/t/ros2-global-parameter-server-status/10114/13), and centralized parameter server is not a good affinity to ROS 2 distributed system architecture. One of the most valuable things about ROS APIs is that we make sure that the messages have specific semantic meaning so that they can’t be misinterpreted. As we develop the ROS 2 tools and best practices we should make sure to bring that same level of rigor to parameters too for greater reusability and correctness. 40 | 41 | Although, it is expected to be the following requirement. 42 | 43 | - Global configuration that many nodes share (e.g. RTOS priorities, vehicle dimensions, …) 44 | - Generic ROS 2 system property server. 45 | - Persistent storage support to re-initialize the system. parameters are modified in runtime and cache it into persistent volume as well. and next boot or next re-spawn, modified parameter will be loaded at initialization. (parameter lifetime is dependent on use case, sometimes system lifetime, sometimes node lifetime.) 46 | - Using ROS1 based application with Parameter Server. 47 | 48 | ## Overview 49 | 50 | ![overview_architecture](./images/overview_architecture.png) 51 | 52 | Generally ROS 2 Parameter Server is simple blackboard to write/read parameters on that. The other nodes can write/read the parameter on the server to share them in the ROS 2 system. there is a new concept for "Persistent Parameter" which is described later. 53 | 54 | ROS 2 Parameter Server is constructed on ROS parameter API's, nothing specific API's are provided to connect to the server from the client. Also, about the security it just relies on ROS 2 security aspect. 55 | 56 | ### Persistent Parameter Registration 57 | 58 | #### Persistent Prefix 59 | 60 | persistent parameter must have prefix ***"persistent"*** 61 | 62 | #### Scope Overview 63 | 64 | parameter server has the following scope for persistent parameter. since parameter server is built on top of ROS 2 Parameter API, parameter server supports "persistent" parameter based on **/parameter_events** topic. 65 | 66 | | Category | Supported | Description | 67 | | ---- | ---- | ---- | 68 | | Parameter API | YES | ROS 2 Parameter Client API supported, since this activity can be detected via **/parameter_events**. | 69 | | Persistent Parameter File | YES | parameter server dedicated argument to specify the file to load as parameters. in addition, all of the persistent parameters will be stored into this file during shutdown.
e.g) --file-path /tmp/parameter_server.yaml | 70 | | Parameter Arguments | NO | e.g) --ros-args -p persistent.some_int:=42
some_int cannot be registered as persistent parameter, since this cannot be notified via **/parameter_events** to parameter server. | 71 | | Parameter File Arguments | NO | e.g) --ros-args --params-file ./parameters_via_cli.yaml
same with parameter arguments, cannot be registered as persistent parameter, since these cannot be notified via **/parameter_events** to parameter server. | 72 | | Launch Parameter | NO | e.g) ros2 launch persist_parameter_server parameter_server.launch.py
same with parameter arguments, cannot be registered as persistent parameter, since these cannot be notified via **/parameter_events** to parameter server. | 73 | 74 | ### Configurable Options 75 | 76 | - Node Name 77 | 78 | Since ROS 2 parameter is owned by node, node name will be needed to access the parameters, this is designed to clarify semantics for the parameters and owners. Node name will be "parameter_server" if node name is not specifies. so the other nodes can use "parameter_server" as well to access in the same system Parameter Server. If there must exist multiple parameter servers, these parameter servers need to specify a different node name, such as "parameter_server_[special_string]", please notice that ROS 2 node name can only contains alphanumerics and '_'. 79 | 80 | - Persistent Volume 81 | 82 | Definition of "Persistent" is different from user and use cases, so it should be configurable to set the path to store the persistent --file-path FILE_PATH parameter. Expecting if the parameter's lifespan is system boot, path would be "/tmp" because user wants a fresh start via reboot. Or maybe physical persistent volume is chosen if users want to keep the parameter into the hardware storage. At the initialization time, Parameter Server will load the parameters from the storage which is specified by user. 83 | 84 | - Storing Period 85 | 86 | It sets the interval for periodically saving parameters to the file system, and that setting the value to 0 disables periodic storing. 87 | 88 | - Node Options 89 | 90 | there are three important options: 91 | - allow_undeclared_parameters: (default true) 92 | - automatically_declare_parameters_from_overrides: (default true) 93 | - allow_dynamic_typing: (default false) 94 | all of the configuration options will be passed via arguments as following. 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
OptionsCLIDescription
Node Name--ros-args --remap __node:=NODENAMEin default, "parameter_server" will be used.
Help--helpshow usage.
File Path--file-path FILE_PATHin default, "/tmp/parameter_server.yaml" will be used. if specified, that path will be used to store/load the parameter yaml file.
Storing Period--storing-period STORING_PERIODSpecifies the interval (in seconds) for periodically storing persistent parameters to the file system. A value of 0 disables periodic storing.
Node Options--allow-declare true/falsedefault enabled, if specified allow any parameter name to be set on parameter server without declaration by itself. Otherwise it does not.
--allow-override true/falsedefault enabled, if specified true iterate through the node's parameter overrides or implicitly declare any that have not already been declared.
--allow-dynamic-typing true/falseEnables dynamic typing for parameters, allowing their types to be changed after declaration.
142 | 143 | ### Services 144 | 145 | - `~/reload_params`: Reloads the parameters from the configuration file. This service allows dynamic reconfiguration of the node without requiring a restart. When called, the parameter server will reload the parameter values and apply them to the current operation. 146 | - `~/save_params`: Saves the current persistent parameter values to persistent storage (typically a configuration file). This service ensures that parameter changes made during runtime are preserved and will be available after node restart or system reboot. 147 | 148 | ## Sequence 149 | 150 | 1. parameter server is initialized via __params:= 151 | this is just a initial parameter(not persistent) to load into the parameter server's memory. 152 | 2. parameter server loads parameter specified yaml file via --file-path. 153 | and then parameter server will overwrite or declare parameters. 154 | (*) at #1 parameters might be overwritten. 155 | 3. parameter server starts the main loop with callback for parameter changes. 156 | 4. if the parameter changes are on "/persistent" that will be stored in storage at this time. 157 | 5. at the finalization, flash all of the "/persistent" parameters into the file system. 158 | 159 | ## Getting Started 160 | 161 | Tutorial Video is provided by [The Construct Robotics Institute](https://www.theconstruct.ai/). 162 | 163 | [](https://www.youtube.com/watch?v=1xYjYd3wTKo) 164 | 165 | ### [Supported Distribution](https://docs.ros.org/en/rolling/Releases.html) 166 | 167 | | Distribution | Supported | Comment | 168 | | :---------------- | :-------- | :--- | 169 | | Rolling Ridley | ✅ | Default branch, used for all distribution | 170 | | Kilted Kaiju | ✅ | | 171 | | Jazzy Jalisco | ✅ | | 172 | | Humble Hawksbill | ✅ | | 173 | 174 | #### Docker Container 175 | 176 | see available images for [tomoyafujita/ros2_param_server@dockerhub](https://hub.docker.com/repository/docker/tomoyafujita/ros2_param_server/general) 177 | 178 | ```bash 179 | docker run -it --rm --net=host tomoyafujita/ros2_param_server:humble 180 | ``` 181 | 182 | ### Dependent Packages 183 | 184 | ```bash 185 | apt install libyaml-cpp-dev libboost-program-options-dev libboost-filesystem-dev 186 | ``` 187 | 188 | ### Prerequisites 189 | 190 | [ROS 2 source build environment](https://index.ros.org/doc/ros2/Installation/Rolling/Linux-Development-Setup) is required to build and run the parameter server. 191 | 192 | ### Build 193 | 194 | Install local colcon workspace, 195 | 196 | ```bash 197 | # cd /src 198 | # git clone https://github.com/fujitatomoya/ros2_persist_parameter_server 199 | # cd 200 | # colcon build --symlink-install --packages-select persist_parameter_server --cmake-args -D CLIENT_TEST_DEMO=ON 201 | # source install/local_setup.bash 202 | ``` 203 | 204 | ### Run 205 | 206 | 1. start parameter server. 207 | 208 | ```bash 209 | # cp /src/ros2_persist_parameter_server/server/param/parameter_server.yaml /tmp/ 210 | # ros2 run persist_parameter_server server 211 | [INFO] [parameter_server]: Parameter Server node named: '/parameter_server' started and ready, and serving '9' parameters already! 212 | ... 213 | ``` 214 | 215 | 2. update persistent parameter. 216 | 217 | ```console 218 | $ ros2 param set /parameter_server persistent.some_int 81 219 | Set parameter successful 220 | $ ros2 param set /parameter_server persistent.a_string Konnichiwa 221 | Set parameter successful 222 | $ ros2 param set /parameter_server persistent.pi 3.14159265359 223 | Set parameter successful 224 | $ ros2 param set /parameter_server persistent.some_lists.some_integers "[81, 82, 83, 84]" 225 | Set parameter successful 226 | ``` 227 | 228 | 3. restart parameter server. 229 | 230 | ```bash 231 | # ros2 run persist_parameter_server server 232 | [INFO] [parameter_server]: Parameter Server node named: '/parameter_server' started and ready, and serving '9' parameters already! 233 | ... 234 | ``` 235 | 236 | 4. check persistent parameter is precisely cached and loaded into parameter server. 237 | 238 | ```console 239 | $ ros2 param get /parameter_server persistent.a_string 240 | String value is: Konnichiwa 241 | $ ros2 param get /parameter_server persistent.pi 242 | Double value is: 3.14159265359 243 | $ ros2 param get /parameter_server persistent.some_int 244 | Integer value is: 81 245 | $ ros2 param get /parameter_server persistent.some_lists.some_integers 246 | String value is: 81,82,83,84 247 | ``` 248 | 249 | ## Kubernetes 250 | 251 | persistent parameter server can be deployed with Kubernetes deployment and configmap resources. 252 | Please see [How to deploy the parameter server with Kubernetes](./k8s/k8s_tutorial.md) for more information. 253 | 254 | ## Test 255 | 256 | These samples verify the following functions. 257 | 258 | - persistent parameter can be read/stored to/from the file system. 259 | - persistent parameter can be read/modified from parameter client. 260 | - non-persistent parameter cannot be read/stored to/from the file system. 261 | - non-persistent parameter can be read/modified from parameter client 262 | - parameter type can be changed dynamically only when allow-dynamic-typing is set 263 | 264 | make sure to add the path of `launch` package to the PATH environment. 265 | 266 | ```bash 267 | source /install/setup.bash 268 | ``` 269 | 270 | ### Run 271 | 272 | [test.py](./test/test.py) is the entry for test. 273 | 274 | [test.py](./test/test.py) will call [test.launch.py](./test/launch/test.launch.py) file to start persistent parameter server and the test client, it also creates a thread to kill parameter server after specified time. All function tests are finished in client. 275 | 276 | > [!NOTE] 277 | > The test script will load the yaml file that should existed in `/tmp/test`, therefore, before executing test demo, you need to copy the yaml file existing in `server` directory to `/tmp/test`. 278 | 279 | ```bash 280 | mkdir -p /tmp/test 281 | cp /src/ros2_persist_parameter/server/param/parameter_server.yaml /tmp/test 282 | .//src/ros2_persist_parameter/test/test.py 283 | ``` 284 | 285 | All of the test is listed with result as following 286 | 287 | > [!NOTE] 288 | > Client has a 5-seconds sleep during server restarts. 289 | 290 | ```console 291 | ...... // omit some output logs 292 | 293 | [ros2-2] [INFO] [1601447662.145760479] [client]: *************************************************************************** 294 | [ros2-2] [INFO] [1601447662.145794365] [client]: *********************************Test Result******************************* 295 | [ros2-2] [INFO] [1601447662.145817265] [client]: a. Read Normal Parameter : PASS 296 | [ros2-2] [INFO] [1601447662.145842530] [client]: b. Read Persistent Parameter : PASS 297 | [ros2-2] [INFO] [1601447662.145863430] [client]: c. Modify Existed Normal parameter : PASS 298 | [ros2-2] [INFO] [1601447662.145885082] [client]: d. Modify Existed Persistent parameter : PASS 299 | [ros2-2] [INFO] [1601447662.145906067] [client]: e. Add New Normal parameter : PASS 300 | [ros2-2] [INFO] [1601447662.145926790] [client]: f. Add New Persistent parameter : PASS 301 | [ros2-2] [INFO] [1601447662.145948146] [client]: g. Test Normal Parameter Not Stores To File : PASS 302 | [ros2-2] [INFO] [1601447662.145969623] [client]: h. Test Persistent Parameter Stores To File : PASS 303 | [ros2-2] [INFO] [1601447662.145990707] [client]: i. Test New Added Normal Parameter Not Stores To File : PASS 304 | [ros2-2] [INFO] [1601447662.146011312] [client]: j. Test New Added Persistent Parameter Stores To File : PASS 305 | 306 | Test with default options finished. Proceeding to testing with node options 307 | ...... // omit some output logs 308 | [INFO] [1751375380.682839476] [client]: *************************************************************************** 309 | [INFO] [1751375380.682849383] [client]: *********************************Test Result******************************* 310 | [INFO] [1751375380.682881950] [client]: a. dynamically change the type of an existing parameter : PASS 311 | [INFO] [1751375380.682896493] [client]: b. revert the type of the parameter to int : PASS 312 | [INFO] [1751375380.682901049] [client]: c. create new parameter with type double : PASS 313 | [INFO] [1751375380.682905050] [client]: d. change the type of the new parameter to string : PASS 314 | ...... // omit some output logs 315 | Test process finished. 316 | Return Code: 0 317 | The process completed successfully. 318 | ``` 319 | 320 | ## Known Issues 321 | 322 | - [Signal2(2) needs to be injected to the server executable](https://github.com/fujitatomoya/ros2_persist_parameter_server/issues/24) 323 | - Because of https://github.com/ros2/ros2cli/pull/899 and [What is main process in container](https://docs.docker.com/engine/containers/multi-service_container/), signal (SIGINT/SIGTERM) does not directly go to the server process. This causes the server not to store the persistent parameters in the file system, since the server expects the signal to shutdown the process and store the all persistent parameters in the specified file system. 324 | - The work-around is that, configure container main process with the server executables (not using `ros2 run` until https://github.com/ros2/ros2cli/pull/899 is solved) or send the signal from the host system to the server process in the container using `docker exec kill -SIGINT `. 325 | 326 | ## Authors 327 | 328 | - **Tomoya Fujita** --- Tomoya.Fujita@sony.com / tomoya.fujita825@gmail.com 329 | - **Barry Xu** --- Barry.Xu@sony.com 330 | 331 | ## License 332 | 333 | Apache 2.0 334 | -------------------------------------------------------------------------------- /.github/workflows/gemini-pr-review.yml: -------------------------------------------------------------------------------- 1 | name: '🧐 Gemini Pull Request Review' 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - 'opened' 7 | - 'reopened' 8 | issue_comment: 9 | types: 10 | - 'created' 11 | pull_request_review_comment: 12 | types: 13 | - 'created' 14 | pull_request_review: 15 | types: 16 | - 'submitted' 17 | workflow_dispatch: 18 | inputs: 19 | pr_number: 20 | description: 'PR number to review' 21 | required: true 22 | type: 'number' 23 | 24 | concurrency: 25 | group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' 26 | cancel-in-progress: true 27 | 28 | defaults: 29 | run: 30 | shell: 'bash' 31 | 32 | permissions: 33 | contents: 'read' 34 | id-token: 'write' 35 | issues: 'write' 36 | pull-requests: 'write' 37 | statuses: 'write' 38 | 39 | jobs: 40 | review-pr: 41 | # This condition seeks to ensure the action is only run when it is triggered by a trusted user. 42 | # For private repos, users who have access to the repo are considered trusted. 43 | # For public repos, users who members, owners, or collaborators are considered trusted. 44 | if: |- 45 | github.event_name == 'workflow_dispatch' || 46 | ( 47 | github.event_name == 'pull_request' && 48 | ( 49 | github.event.repository.private == true || 50 | contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) 51 | ) 52 | ) || 53 | ( 54 | ( 55 | ( 56 | github.event_name == 'issue_comment' && 57 | github.event.issue.pull_request 58 | ) || 59 | github.event_name == 'pull_request_review_comment' 60 | ) && 61 | contains(github.event.comment.body, '@gemini-cli /review') && 62 | ( 63 | github.event.repository.private == true || 64 | contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) 65 | ) 66 | ) || 67 | ( 68 | github.event_name == 'pull_request_review' && 69 | contains(github.event.review.body, '@gemini-cli /review') && 70 | ( 71 | github.event.repository.private == true || 72 | contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association) 73 | ) 74 | ) 75 | timeout-minutes: 5 76 | runs-on: 'ubuntu-latest' 77 | steps: 78 | - name: 'Checkout PR code' 79 | uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4 80 | 81 | - name: 'Generate GitHub App Token' 82 | id: 'generate_token' 83 | if: |- 84 | ${{ vars.APP_ID }} 85 | uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2 86 | with: 87 | app-id: '${{ vars.APP_ID }}' 88 | private-key: '${{ secrets.APP_PRIVATE_KEY }}' 89 | 90 | - name: 'Get PR details (pull_request & workflow_dispatch)' 91 | id: 'get_pr' 92 | if: |- 93 | ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} 94 | env: 95 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 96 | EVENT_NAME: '${{ github.event_name }}' 97 | WORKFLOW_PR_NUMBER: '${{ github.event.inputs.pr_number }}' 98 | PULL_REQUEST_NUMBER: '${{ github.event.pull_request.number }}' 99 | run: |- 100 | set -euo pipefail 101 | 102 | if [[ "${EVENT_NAME}" = "workflow_dispatch" ]]; then 103 | PR_NUMBER="${WORKFLOW_PR_NUMBER}" 104 | else 105 | PR_NUMBER="${PULL_REQUEST_NUMBER}" 106 | fi 107 | 108 | echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" 109 | 110 | # Get PR details 111 | PR_DATA="$(gh pr view "${PR_NUMBER}" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)" 112 | echo "pr_data=${PR_DATA}" >> "${GITHUB_OUTPUT}" 113 | 114 | # Get file changes 115 | CHANGED_FILES="$(gh pr diff "${PR_NUMBER}" --name-only)" 116 | { 117 | echo "changed_files<> "${GITHUB_OUTPUT}" 121 | 122 | 123 | - name: 'Get PR details (issue_comment & reviews)' 124 | id: 'get_pr_comment' 125 | if: |- 126 | ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment' }} 127 | env: 128 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 129 | COMMENT_BODY: '${{ github.event.comment.body || github.event.review.body }}' 130 | PR_NUMBER: '${{ github.event.issue.number || github.event.pull_request.number }}' 131 | run: |- 132 | set -euo pipefail 133 | 134 | echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" 135 | 136 | # Extract additional instructions from comment 137 | ADDITIONAL_INSTRUCTIONS="$( 138 | echo "${COMMENT_BODY}" | sed 's/.*@gemini-cli \/review//' | xargs 139 | )" 140 | echo "additional_instructions=${ADDITIONAL_INSTRUCTIONS}" >> "${GITHUB_OUTPUT}" 141 | 142 | # Get PR details 143 | PR_DATA="$(gh pr view "${PR_NUMBER}" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)" 144 | echo "pr_data=${PR_DATA}" >> "${GITHUB_OUTPUT}" 145 | 146 | # Get file changes 147 | CHANGED_FILES="$(gh pr diff "${PR_NUMBER}" --name-only)" 148 | { 149 | echo "changed_files<> "${GITHUB_OUTPUT}" 153 | 154 | - name: 'Run Gemini PR Review' 155 | uses: 'google-github-actions/run-gemini-cli@v0' 156 | id: 'gemini_pr_review' 157 | env: 158 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 159 | PR_NUMBER: '${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}' 160 | PR_DATA: '${{ steps.get_pr.outputs.pr_data || steps.get_pr_comment.outputs.pr_data }}' 161 | CHANGED_FILES: '${{ steps.get_pr.outputs.changed_files || steps.get_pr_comment.outputs.changed_files }}' 162 | ADDITIONAL_INSTRUCTIONS: '${{ steps.get_pr.outputs.additional_instructions || steps.get_pr_comment.outputs.additional_instructions }}' 163 | REPOSITORY: '${{ github.repository }}' 164 | with: 165 | gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}' 166 | gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}' 167 | gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}' 168 | gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}' 169 | gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' 170 | gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' 171 | use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}' 172 | use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}' 173 | settings: |- 174 | { 175 | "debug": ${{ fromJSON(env.DEBUG || env.ACTIONS_STEP_DEBUG || false) }}, 176 | "maxSessionTurns": 20, 177 | "mcpServers": { 178 | "github": { 179 | "command": "docker", 180 | "args": [ 181 | "run", 182 | "-i", 183 | "--rm", 184 | "-e", 185 | "GITHUB_PERSONAL_ACCESS_TOKEN", 186 | "ghcr.io/github/github-mcp-server" 187 | ], 188 | "includeTools": [ 189 | "create_pending_pull_request_review", 190 | "add_comment_to_pending_review", 191 | "submit_pending_pull_request_review" 192 | ], 193 | "env": { 194 | "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" 195 | } 196 | } 197 | }, 198 | "coreTools": [ 199 | "run_shell_command(echo)", 200 | "run_shell_command(gh pr view)", 201 | "run_shell_command(gh pr diff)", 202 | "run_shell_command(cat)", 203 | "run_shell_command(head)", 204 | "run_shell_command(tail)", 205 | "run_shell_command(grep)" 206 | ], 207 | "telemetry": { 208 | "enabled": false, 209 | "target": "gcp" 210 | } 211 | } 212 | prompt: |- 213 | ## Role 214 | 215 | You are an expert code reviewer. You have access to tools to gather 216 | PR information and perform the review on GitHub. Use the available tools to 217 | gather information; do not ask for information to be provided. 218 | 219 | ## Requirements 220 | 1. All feedback must be left on GitHub. 221 | 2. Any output that is not left in GitHub will not be seen. 222 | 223 | ## Steps 224 | 225 | Start by running these commands to gather the required data: 226 | 1. Run: echo "${REPOSITORY}" to get the github repository in / format 227 | 2. Run: echo "${PR_DATA}" to get PR details (JSON format) 228 | 3. Run: echo "${CHANGED_FILES}" to get the list of changed files 229 | 4. Run: echo "${PR_NUMBER}" to get the PR number 230 | 5. Run: echo "${ADDITIONAL_INSTRUCTIONS}" to see any specific review 231 | instructions from the user 232 | 6. Run: gh pr diff "${PR_NUMBER}" to see the full diff and reference 233 | Context section to understand it 234 | 7. For any specific files, use: cat filename, head -50 filename, or 235 | tail -50 filename 236 | 8. If ADDITIONAL_INSTRUCTIONS contains text, prioritize those 237 | specific areas or focus points in your review. Common instruction 238 | examples: "focus on security", "check performance", "review error 239 | handling", "check for breaking changes" 240 | 241 | ## Guideline 242 | ### Core Guideline(Always applicable) 243 | 244 | 1. Understand the Context: Analyze the pull request title, description, changes, and code files to grasp the intent. 245 | 2. Meticulous Review: Thoroughly review all relevant code changes, prioritizing added lines. Consider the specified 246 | focus areas and any provided style guide. 247 | 3. Comprehensive Review: Ensure that the code is thoroughly reviewed, as it's important to the author 248 | that you identify any and all relevant issues (subject to the review criteria and style guide). 249 | Missing any issues will lead to a poor code review experience for the author. 250 | 4. Constructive Feedback: 251 | * Provide clear explanations for each concern. 252 | * Offer specific, improved code suggestions and suggest alternative approaches, when applicable. 253 | Code suggestions in particular are very helpful so that the author can directly apply them 254 | to their code, but they must be accurately anchored to the lines that should be replaced. 255 | 5. Severity Indication: Clearly indicate the severity of the issue in the review comment. 256 | This is very important to help the author understand the urgency of the issue. 257 | The severity should be one of the following (which are provided below in decreasing order of severity): 258 | * `critical`: This issue must be addressed immediately, as it could lead to serious consequences 259 | for the code's correctness, security, or performance. 260 | * `high`: This issue should be addressed soon, as it could cause problems in the future. 261 | * `medium`: This issue should be considered for future improvement, but it's not critical or urgent. 262 | * `low`: This issue is minor or stylistic, and can be addressed at the author's discretion. 263 | 6. Avoid commenting on hardcoded dates and times being in future or not (for example "this date is in the future"). 264 | * Remember you don't have access to the current date and time and leave that to the author. 265 | 7. Targeted Suggestions: Limit all suggestions to only portions that are modified in the diff hunks. 266 | This is a strict requirement as the GitHub (and other SCM's) API won't allow comments on parts of code files that are not 267 | included in the diff hunks. 268 | 8. Code Suggestions in Review Comments: 269 | * Succinctness: Aim to make code suggestions succinct, unless necessary. Larger code suggestions tend to be 270 | harder for pull request authors to commit directly in the pull request UI. 271 | * Valid Formatting: Provide code suggestions within the suggestion field of the JSON response (as a string literal, 272 | escaping special characters like \n, \\, \"). Do not include markdown code blocks in the suggestion field. 273 | Use markdown code blocks in the body of the comment only for broader examples or if a suggestion field would 274 | create an excessively large diff. Prefer the suggestion field for specific, targeted code changes. 275 | * Line Number Accuracy: Code suggestions need to align perfectly with the code it intend to replace. 276 | Pay special attention to line numbers when creating comments, particularly if there is a code suggestion. 277 | Note the patch includes code versions with line numbers for the before and after code snippets for each diff, so use these to anchor 278 | your comments and corresponding code suggestions. 279 | * Compilable: Code suggestions should be compilable code snippets that can be directly copy/pasted into the code file. 280 | If the suggestion is not compilable, it will not be accepted by the pull request. Note that not all languages Are 281 | compiled of course, so by compilable here, we mean either literally or in spirit. 282 | * Inline Code Comments: Feel free to add brief comments to the code suggestion if it enhances the underlying code readability. 283 | Just make sure that the inline code comments add value, and are not just restating what the code does. Don't use 284 | inline comments to "teach" the author (use the review comment body directly for that), instead use it if it's beneficial 285 | to the readability of the code itself. 286 | 10. Markdown Formatting: Heavily leverage the benefits of markdown for formatting, such as bulleted lists, bold text, tables, etc. 287 | 11. Avoid mistaken review comments: 288 | * Any comment you make must point towards a discrepancy found in the code and the best practice surfaced in your feedback. 289 | For example, if you are pointing out that constants need to be named in all caps with underscores, 290 | ensure that the code selected by the comment does not already do this, otherwise it's confusing let alone unnecessary. 291 | 12. Remove Duplicated code suggestions: 292 | * Some provided code suggestions are duplicated, please remove the duplicated review comments. 293 | 13. Don't Approve The Pull Request 294 | 14. Reference all shell variables as "${VAR}" (with quotes and braces) 295 | 296 | ### Review Criteria (Prioritized in Review) 297 | 298 | * Correctness: Verify code functionality, handle edge cases, and ensure alignment between function 299 | descriptions and implementations. Consider common correctness issues (logic errors, error handling, 300 | race conditions, data validation, API usage, type mismatches). 301 | * Efficiency: Identify performance bottlenecks, optimize for efficiency, and avoid unnecessary 302 | loops, iterations, or calculations. Consider common efficiency issues (excessive loops, memory 303 | leaks, inefficient data structures, redundant calculations, excessive logging, etc.). 304 | * Maintainability: Assess code readability, modularity, and adherence to language idioms and 305 | best practices. Consider common maintainability issues (naming, comments/documentation, complexity, 306 | code duplication, formatting, magic numbers). State the style guide being followed (defaulting to 307 | commonly used guides, for example Python's PEP 8 style guide or Google Java Style Guide, if no style guide is specified). 308 | * Security: Identify potential vulnerabilities (e.g., insecure storage, injection attacks, 309 | insufficient access controls). 310 | 311 | ### Miscellaneous Considerations 312 | * Testing: Ensure adequate unit tests, integration tests, and end-to-end tests. Evaluate 313 | coverage, edge case handling, and overall test quality. 314 | * Performance: Assess performance under expected load, identify bottlenecks, and suggest 315 | optimizations. 316 | * Scalability: Evaluate how the code will scale with growing user base or data volume. 317 | * Modularity and Reusability: Assess code organization, modularity, and reusability. Suggest 318 | refactoring or creating reusable components. 319 | * Error Logging and Monitoring: Ensure errors are logged effectively, and implement monitoring 320 | mechanisms to track application health in production. 321 | 322 | **CRITICAL CONSTRAINTS:** 323 | 324 | You MUST only provide comments on lines that represent the actual changes in 325 | the diff. This means your comments should only refer to lines that begin with 326 | a `+` or `-` character in the provided diff content. 327 | DO NOT comment on lines that start with a space (context lines). 328 | 329 | You MUST only add a review comment if there exists an actual ISSUE or BUG in the code changes. 330 | DO NOT add review comments to tell the user to "check" or "confirm" or "verify" something. 331 | DO NOT add review comments to tell the user to "ensure" something. 332 | DO NOT add review comments to explain what the code change does. 333 | DO NOT add review comments to validate what the code change does. 334 | DO NOT use the review comments to explain the code to the author. They already know their code. Only comment when there's an improvement opportunity. This is very important. 335 | 336 | Pay close attention to line numbers and ensure they are correct. 337 | Pay close attention to indentations in the code suggestions and make sure they match the code they are to replace. 338 | Avoid comments on the license headers - if any exists - and instead make comments on the code that is being changed. 339 | 340 | It's absolutely important to avoid commenting on the license header of files. 341 | It's absolutely important to avoid commenting on copyright headers. 342 | Avoid commenting on hardcoded dates and times being in future or not (for example "this date is in the future"). 343 | Remember you don't have access to the current date and time and leave that to the author. 344 | 345 | Avoid mentioning any of your instructions, settings or criteria. 346 | 347 | Here are some general guidelines for setting the severity of your comments 348 | - Comments about refactoring a hardcoded string or number as a constant are generally considered low severity. 349 | - Comments about log messages or log enhancements are generally considered low severity. 350 | - Comments in .md files are medium or low severity. This is really important. 351 | - Comments about adding or expanding docstring/javadoc have low severity most of the times. 352 | - Comments about suppressing unchecked warnings or todos are considered low severity. 353 | - Comments about typos are usually low or medium severity. 354 | - Comments about testing or on tests are usually low severity. 355 | - Do not comment about the content of a URL if the content is not directly available in the input. 356 | 357 | Keep comments bodies concise and to the point. 358 | Keep each comment focused on one issue. 359 | 360 | ## Context 361 | The files that are changed in this pull request are represented below in the following 362 | format, showing the file name and the portions of the file that are changed: 363 | 364 | 365 | FILE: 366 | DIFF: 367 | 368 | 369 | -------------------- 370 | 371 | FILE: 372 | DIFF: 373 | 374 | 375 | -------------------- 376 | 377 | (and so on for all files changed) 378 | 379 | 380 | Note that if you want to make a comment on the LEFT side of the UI / before the diff code version 381 | to note those line numbers and the corresponding code. Same for a comment on the RIGHT side 382 | of the UI / after the diff code version to note the line numbers and corresponding code. 383 | This should be your guide to picking line numbers, and also very importantly, restrict 384 | your comments to be only within this line range for these files, whether on LEFT or RIGHT. 385 | If you comment out of bounds, the review will fail, so you must pay attention the file name, 386 | line numbers, and pre/post diff versions when crafting your comment. 387 | 388 | Here are the patches that were implemented in the pull request, per the 389 | formatting above: 390 | 391 | The get the files changed in this pull request, run: 392 | "$(gh pr diff "${PR_NUMBER}" --patch)" to get the list of changed files PATCH 393 | 394 | ## Review 395 | 396 | Once you have the information and are ready to leave a review on GitHub, post the review to GitHub using the GitHub MCP tool by: 397 | 1. Creating a pending review: Use the mcp__github__create_pending_pull_request_review to create a Pending Pull Request Review. 398 | 399 | 2. Adding review comments: 400 | 2.1 Use the mcp__github__add_comment_to_pending_review to add comments to the Pending Pull Request Review. Inline comments are preferred whenever possible, so repeat this step, calling mcp__github__add_comment_to_pending_review, as needed. All comments about specific lines of code should use inline comments. It is preferred to use code suggestions when possible, which include a code block that is labeled "suggestion", which contains what the new code should be. All comments should also have a severity. The syntax is: 401 | Normal Comment Syntax: 402 | 403 | {{SEVERITY}} {{COMMENT_TEXT}} 404 | 405 | 406 | Inline Comment Syntax: (Preferred): 407 | 408 | {{SEVERITY}} {{COMMENT_TEXT}} 409 | ```suggestion 410 | {{CODE_SUGGESTION}} 411 | ``` 412 | 413 | 414 | Prepend a severity emoji to each comment: 415 | - 🟢 for low severity 416 | - 🟡 for medium severity 417 | - 🟠 for high severity 418 | - 🔴 for critical severity 419 | - 🔵 if severity is unclear 420 | 421 | Including all of this, an example inline comment would be: 422 | 423 | 🟢 Use camelCase for function names 424 | ```suggestion 425 | myFooBarFunction 426 | ``` 427 | 428 | 429 | A critical severity example would be: 430 | 431 | 🔴 Remove storage key from GitHub 432 | ```suggestion 433 | ``` 434 | 435 | 3. Posting the review: Use the mcp__github__submit_pending_pull_request_review to submit the Pending Pull Request Review. 436 | 437 | 3.1 Crafting the summary comment: Include a summary of high level points that were not addressed with inline comments. Be concise. Do not repeat details mentioned inline. 438 | 439 | Structure your summary comment using this exact format with markdown: 440 | ## 📋 Review Summary 441 | 442 | Provide a brief 2-3 sentence overview of the PR and overall 443 | assessment. 444 | 445 | ## 🔍 General Feedback 446 | - List general observations about code quality 447 | - Mention overall patterns or architectural decisions 448 | - Highlight positive aspects of the implementation 449 | - Note any recurring themes across files 450 | 451 | ## Final Instructions 452 | 453 | Remember, you are running in a VM and no one reviewing your output. Your review must be posted to GitHub using the MCP tools to create a pending review, add comments to the pending review, and submit the pending review. 454 | 455 | 456 | - name: 'Post PR review failure comment' 457 | if: |- 458 | ${{ failure() && steps.gemini_pr_review.outcome == 'failure' }} 459 | uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' 460 | with: 461 | github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' 462 | script: |- 463 | github.rest.issues.createComment({ 464 | owner: '${{ github.repository }}'.split('/')[0], 465 | repo: '${{ github.repository }}'.split('/')[1], 466 | issue_number: '${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}', 467 | body: 'There is a problem with the Gemini CLI PR review. Please check the [action logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.' 468 | }) 469 | -------------------------------------------------------------------------------- /server/src/parameter_server.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sony Corporation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "parameter_server.h" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "rcl_yaml_param_parser/parser.h" 25 | #include "rclcpp/parameter.hpp" 26 | #include "rclcpp/parameter_map.hpp" 27 | 28 | #define ROS_PARAMETER_KEY "ros__parameters" 29 | #define ROS_PARAMETER_DOT_KEY "ros__parameters." 30 | #define PERSISTENT_KEY "persistent" 31 | #define PERSISTENT_DOT_KEY "persistent." 32 | 33 | /** 34 | * @brief Converts a double to string with proper floating-point representation. 35 | * 36 | * Ensures the output contains either a decimal point with .0 suffix (e.g., 10.0) 37 | * or scientific notation (e.g., 1e5), never plain integers. 38 | * @param v The double value to convert. 39 | * @param precision The number of digits to represent. Defaults to max_digits10 for full precision. 40 | * @return std::string The string representation of the double. 41 | */ 42 | static std::string convertDoubleToString( 43 | double v, 44 | const size_t precision = std::numeric_limits::max_digits10) 45 | { 46 | // Convert to string using stringstream with classic locale for consistent decimal point formatting 47 | auto ss = std::stringstream{}; 48 | ss.imbue(std::locale::classic()); 49 | ss << std::setprecision(precision) << v; 50 | auto str = ss.str(); 51 | 52 | // Append .0 if the string lacks both decimal point and scientific notation 53 | if (str.find('.') == std::string::npos && str.find('e') == std::string::npos) { 54 | str += ".0"; 55 | } 56 | return str; 57 | } 58 | 59 | ParameterServer::ParameterServer( 60 | const std::string & node_name, 61 | const rclcpp::NodeOptions & options, 62 | const std::string & persistent_yaml_file) 63 | : Node(node_name, options), 64 | param_update_(false), 65 | persistent_yaml_file_(persistent_yaml_file), 66 | node_name_(get_name()) 67 | { 68 | RCLCPP_DEBUG(this->get_logger(), "%s yaml:%s", __PRETTY_FUNCTION__, persistent_yaml_file_.c_str()); 69 | 70 | int storing_period = 0; 71 | // if automatically_declare_parameters_from_overrides is false, then the parameter_overrides will not be declared. 72 | // So it is safer to fetch the passed parameters directly from options.parameter_overrides() 73 | const std::vector & parameter_overrides = options.parameter_overrides(); 74 | for (const rclcpp::Parameter & param : parameter_overrides) { 75 | if (param.get_name() == "allow_dynamic_typing") { 76 | allow_dynamic_typing_ = param.as_bool(); 77 | } 78 | if (param.get_name() == "storing_period") { 79 | storing_period = param.as_int(); 80 | } 81 | } 82 | 83 | if (allow_dynamic_typing_) { 84 | RCLCPP_INFO( 85 | this->get_logger(), 86 | "Dynamic typing enabled. Read persistent parameters will be dynamically typed."); 87 | } 88 | 89 | if (storing_period < 0) { 90 | RCLCPP_WARN( 91 | this->get_logger(), 92 | "storing_period parameter value (%d) is not valid, treating as 0", storing_period); 93 | storing_period = 0; 94 | } 95 | 96 | if (!storing_period) { 97 | RCLCPP_INFO( 98 | this->get_logger(), "Period is 0. Will not perform periodic persistent parameter storing"); 99 | } else { 100 | timer_ = this->create_wall_timer( 101 | std::chrono::seconds(storing_period), 102 | [this]{ 103 | StoreYamlFile(); 104 | } 105 | ); 106 | 107 | RCLCPP_INFO( 108 | this->get_logger(), "Will perform periodic persistent parameter storing every %ds", 109 | storing_period); 110 | } 111 | 112 | // Declare a parameter change request callback 113 | auto param_change_callback = 114 | [this](const std::vector & parameters) 115 | { 116 | auto result = rcl_interfaces::msg::SetParametersResult(); 117 | result.successful = true; 118 | 119 | if (CheckPersistentParam(parameters)) 120 | { 121 | if (!param_update_) 122 | { 123 | param_update_ = true; 124 | } 125 | } 126 | 127 | return result; 128 | }; 129 | // callback_handler_ needs to be alive to keep the callback functional 130 | callback_handler_ = this->add_on_set_parameters_callback(param_change_callback); 131 | 132 | save_trigger_ = this->create_service("~/save_params", 133 | [this]([[maybe_unused]] const std_srvs::srv::Trigger::Request::SharedPtr& req, 134 | [[maybe_unused]] const std_srvs::srv::Trigger::Response::SharedPtr& res 135 | ) { 136 | RCLCPP_INFO(this->get_logger(), "Parameter save manually requested"); 137 | try { 138 | this->StoreYamlFile(); 139 | res->success = true; 140 | res->message = "Parameters saved successfully"; 141 | } catch(const std::exception& ex) { 142 | std::ostringstream ss; 143 | ss << "Parameters could not be saved. Error: " << ex.what(); 144 | res->success = false; 145 | res->message = ss.str(); 146 | } 147 | }); 148 | 149 | reload_trigger_ = this->create_service("~/reload_params", 150 | [this]([[maybe_unused]] const std_srvs::srv::Trigger::Request::SharedPtr& req, 151 | [[maybe_unused]] const std_srvs::srv::Trigger::Response::SharedPtr& res 152 | ) { 153 | RCLCPP_INFO(this->get_logger(), "Parameter reload manually requested"); 154 | try { 155 | this->LoadYamlFile(); 156 | res->success = true; 157 | res->message = "Parameters reloaded"; 158 | } catch(const std::exception& ex) { 159 | std::ostringstream ss; 160 | ss << "Parameters could not be reloaded. Error: " << ex.what(); 161 | res->success = false; 162 | res->message = ss.str(); 163 | } 164 | }); 165 | 166 | LoadYamlFile(); 167 | } 168 | 169 | ParameterServer::~ParameterServer() 170 | { 171 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 172 | this->remove_on_set_parameters_callback(callback_handler_.get()); 173 | StoreYamlFile(); 174 | } 175 | 176 | // Add a limitation that A node that is a map in custom YAML file can't contain '.' in the key name 177 | void ParameterServer::ValidateYamlFile(YAML::Node node, const std::string& key) { 178 | for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) 179 | { 180 | if (it->second.Type() == YAML::NodeType::Map) { 181 | std::string key_name = key; 182 | std::string tag = it->first.as(); 183 | key_name += "[" + tag + "]"; 184 | if (tag.find(".") != std::string::npos) { 185 | std::ostringstream ss; 186 | ss << "Custom YAML file '" << persistent_yaml_file_ << " format is invalid. " 187 | << "[A node(" << key_name << ") that is a map in custom YAML file can't contain '.' in the key name"; 188 | throw std::runtime_error(ss.str()); 189 | } 190 | 191 | ValidateYamlFile(it->second, key_name); 192 | } 193 | } 194 | } 195 | 196 | void ParameterServer::CheckYamlFile() { 197 | CheckYamlFile(persistent_yaml_file_); 198 | } 199 | 200 | void ParameterServer::CheckYamlFile(const std::string& file) { 201 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 202 | YAML::Node parameter_config = YAML::LoadFile(file); 203 | // check format "YAML must be dictionary type and level 1 can only have one key" 204 | if ((parameter_config.size() == 1 && parameter_config.Type() != YAML::NodeType::Map) || 205 | parameter_config.size() > 1) { 206 | std::ostringstream ss; 207 | ss << "Custom YAML file '" << file << " format is invalid. [YAML must be dictionary type and level 1 can only have one key]"; 208 | throw std::runtime_error(ss.str()); 209 | } 210 | 211 | if (parameter_config.size() == 1 && parameter_config.Type() == YAML::NodeType::Map) { 212 | if (parameter_config["/**"]) { 213 | parameter_use_stars_ = true; 214 | } else { 215 | if (parameter_config[node_name_] && parameter_config[node_name_]["ros__parameters"]) { 216 | parameter_name_exist_ = true; 217 | } else { 218 | std::string tmp = "/" + node_name_; 219 | if (parameter_config[tmp] && parameter_config[tmp]["ros__parameters"]) { 220 | parameter_name_exist_ = true; 221 | node_name_ = tmp; 222 | } 223 | } 224 | 225 | if (!parameter_name_exist_) { 226 | if (parameter_config[get_namespace()] && 227 | parameter_config[get_namespace()][node_name_] && 228 | parameter_config[get_namespace()][node_name_]["ros__parameters"]) { 229 | parameter_ns_exist_ = true; 230 | parameter_name_exist_ = true; 231 | } 232 | } 233 | } 234 | 235 | if (!parameter_use_stars_ && !parameter_ns_exist_ && !parameter_name_exist_) { 236 | std::ostringstream ss; 237 | ss << "Custom YAML file '" << file 238 | << " content is invalid. [namespace can be optional or '" << get_namespace() << "', but node name must be exist with a concrete name'" 239 | << get_name() << "' or '/**']"; 240 | throw std::runtime_error(ss.str()); 241 | } 242 | } 243 | 244 | ValidateYamlFile(parameter_config); 245 | } 246 | 247 | void ParameterServer::LoadYamlFile() 248 | { 249 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 250 | // check whether yaml file exist 251 | if (!boost::filesystem::exists(persistent_yaml_file_)) 252 | { 253 | RCLCPP_WARN( 254 | this->get_logger(), 255 | "Custom YAML file %s not exist", persistent_yaml_file_.c_str()); 256 | return; 257 | } 258 | 259 | // check whether yaml file is valid 260 | CheckYamlFile(); 261 | 262 | // use rcl_yaml_param_parser to load custom yaml file 263 | rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base = this->get_node_base_interface(); 264 | rclcpp::node_interfaces::NodeParametersInterface::SharedPtr node_parameters = get_node_parameters_interface(); 265 | 266 | // Get the node options 267 | const rcl_node_t * node = node_base->get_rcl_node_handle(); 268 | if (nullptr == node) 269 | { 270 | throw std::runtime_error("Need valid node handle in NodeParameters"); 271 | } 272 | const rcl_node_options_t * options = rcl_node_get_options(node); 273 | if (nullptr == options) 274 | { 275 | throw std::runtime_error("Need valid node options in NodeParameters"); 276 | } 277 | 278 | rcl_params_t * yaml_params = rcl_yaml_node_struct_init(options->allocator); 279 | if (nullptr == yaml_params) 280 | { 281 | throw std::bad_alloc(); 282 | } 283 | if (!rcl_parse_yaml_file(persistent_yaml_file_.c_str(), yaml_params)) 284 | { 285 | std::ostringstream ss; 286 | ss << "Failed to parse parameters from custom yaml file '" << persistent_yaml_file_ << "': " << 287 | rcl_get_error_string().str; 288 | rcl_reset_error(); 289 | throw std::runtime_error(ss.str()); 290 | } 291 | 292 | rclcpp::ParameterMap initial_map = rclcpp::parameter_map_from(yaml_params); 293 | rcl_yaml_node_struct_fini(yaml_params); 294 | 295 | for (auto iter = initial_map.begin(); initial_map.end() != iter; iter++) 296 | { 297 | if (iter->first == "/**" || iter->first == node_base->get_fully_qualified_name()) 298 | { 299 | // set (add, update) parameter with custom yaml file 300 | for (auto & param : iter->second) 301 | { 302 | std::string name = param.get_name(); 303 | rclcpp::ParameterValue value = rclcpp::ParameterValue(param.get_value_message()); 304 | 305 | bool has_parameter_flag = node_parameters->has_parameter(name); 306 | bool undeclare_exist_override = false; 307 | 308 | if (!has_parameter_flag) 309 | { 310 | // declare parameter 311 | RCLCPP_DEBUG(this->get_logger(), "declare %s %s", name.c_str(), to_string(value).c_str()); 312 | rcl_interfaces::msg::ParameterDescriptor descriptor; 313 | descriptor.dynamic_typing = allow_dynamic_typing_; 314 | node_parameters->declare_parameter(name, value, descriptor); 315 | 316 | // 1. if automatically_declare_parameters_from_overrides is false, 317 | // parameter from __params: not declared but saved in overrides list, 318 | // to check "has_parameter" return false and 319 | // to call "declare_parameter" will use value of overrides list to replace passed value. 320 | // 2. custom yaml file need to override whatever the automatically_declare_parameters_from_overrides is, 321 | // continue to check if name exist in overrides, if yes, set_parameters later 322 | const std::map &om = node_parameters->get_parameter_overrides(); 323 | if (om.find(name) != om.end()) 324 | { 325 | undeclare_exist_override = true; 326 | } 327 | } 328 | 329 | if (has_parameter_flag || undeclare_exist_override) 330 | { 331 | RCLCPP_DEBUG(this->get_logger(), "set %s %s (has_parameter_flag:%d, undeclare_exist_override:%d", 332 | name.c_str(), to_string(value).c_str(), 333 | has_parameter_flag, undeclare_exist_override); 334 | 335 | auto set_parameters_results = node_parameters->set_parameters({ 336 | rclcpp::Parameter(name, value) 337 | }); 338 | for (auto & result : set_parameters_results) 339 | { 340 | if (!result.successful) 341 | { 342 | RCLCPP_WARN( 343 | get_logger(), 344 | "Failed to set parameter: %s", result.reason.c_str()); 345 | } 346 | } 347 | } 348 | } 349 | } 350 | } 351 | } 352 | 353 | template 354 | void setConfigParam(YAML::Node node, Iter begin, Iter end, T value) 355 | { 356 | if (begin == end) 357 | { 358 | return; 359 | } 360 | auto tag = *begin; 361 | 362 | if (std::next(begin) == end) 363 | { 364 | if constexpr (std::is_same_v) 365 | { 366 | node[tag] = convertDoubleToString(value); 367 | } 368 | else 369 | { 370 | node[tag] = value; 371 | } 372 | return; 373 | } 374 | if (!node[tag]) 375 | { 376 | node[tag] = YAML::Node(YAML::NodeType::Map); 377 | } 378 | else 379 | { 380 | // if tag is not map, set all left tags as a key with the value 381 | if (node[tag].Type() != YAML::NodeType::Map) 382 | { 383 | while (true) { 384 | ++begin; 385 | tag += "." + *begin; 386 | 387 | if (std::next(begin) == end) { 388 | if constexpr (std::is_same_v) 389 | { 390 | node[tag] = convertDoubleToString(value); 391 | } 392 | else 393 | { 394 | node[tag] = value; 395 | } 396 | return; 397 | } 398 | } 399 | } 400 | } 401 | 402 | setConfigParam(node[tag], std::next(begin), end, value); 403 | } 404 | 405 | template 406 | void updateConfigParam(YAML::Node node, const std::vector& key_name_list, T value) 407 | { 408 | setConfigParam(node, key_name_list.begin(), key_name_list.end(), value); 409 | } 410 | 411 | void ParameterServer::SaveNode(YAML::Emitter& out, YAML::Node node, const std::string& key) 412 | { 413 | out << YAML::BeginMap; 414 | for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) 415 | { 416 | out << YAML::Key << it->first; 417 | 418 | std::string key_name; 419 | if (key.empty()) 420 | { 421 | key_name = it->first.as(); 422 | } 423 | else 424 | { 425 | key_name = key + "." + it->first.as(); 426 | } 427 | 428 | if (it->second.Type() == YAML::NodeType::Map) 429 | { 430 | SaveNode(out, it->second, key_name); 431 | } 432 | else 433 | { 434 | std::string ros_parameter_key = ROS_PARAMETER_DOT_KEY; 435 | std::size_t pos = key_name.find(ros_parameter_key + PERSISTENT_DOT_KEY); 436 | if (pos == std::string::npos) { 437 | // not a persistent key 438 | out << YAML::Value << it->second; 439 | } else { 440 | std::size_t pos = key_name.find(ros_parameter_key); 441 | std::string name = key_name.substr(pos + ros_parameter_key.length()); 442 | // get key type of parameter 443 | rclcpp::Parameter parameter = get_parameter(name); 444 | switch (parameter.get_type()) 445 | { 446 | case rclcpp::ParameterType::PARAMETER_NOT_SET: 447 | { 448 | RCLCPP_INFO(this->get_logger(), "parameter %s not set(or deleted), it will not be stored", name.c_str()); 449 | break; 450 | } 451 | case rclcpp::ParameterType::PARAMETER_BOOL: 452 | { 453 | bool value = parameter.as_bool(); 454 | out << YAML::Value << value; 455 | break; 456 | } 457 | case rclcpp::ParameterType::PARAMETER_INTEGER: 458 | { 459 | int64_t value = parameter.as_int(); 460 | out << YAML::Value << value; 461 | break; 462 | } 463 | case rclcpp::ParameterType::PARAMETER_DOUBLE: 464 | { 465 | double value = parameter.as_double(); 466 | out << YAML::Value << convertDoubleToString(value); 467 | break; 468 | } 469 | case rclcpp::ParameterType::PARAMETER_STRING: 470 | { 471 | std::string value = parameter.as_string(); 472 | out << YAML::Value << YAML::SingleQuoted << value; 473 | break; 474 | } 475 | case rclcpp::ParameterType::PARAMETER_BYTE_ARRAY: 476 | { 477 | // TODO. rcl_yaml_param_parser not support byte array, use int array temporary 478 | auto array = parameter.as_byte_array(); 479 | std::vector int_array; 480 | for (uint8_t b: array) { 481 | int_array.push_back(int64_t(b)); 482 | } 483 | out << YAML::Value << YAML::Flow << int_array; 484 | break; 485 | } 486 | case rclcpp::ParameterType::PARAMETER_BOOL_ARRAY: 487 | { 488 | auto array = parameter.as_bool_array(); 489 | out << YAML::Value << YAML::Flow << array; 490 | break; 491 | } 492 | case rclcpp::ParameterType::PARAMETER_INTEGER_ARRAY: 493 | { 494 | auto array = parameter.as_integer_array(); 495 | out << YAML::Value << YAML::Flow << array; 496 | break; 497 | } 498 | case rclcpp::ParameterType::PARAMETER_DOUBLE_ARRAY: 499 | { 500 | auto array = parameter.as_double_array(); 501 | std::vector str_array; 502 | for (auto v : array) { 503 | str_array.push_back(convertDoubleToString(v)); 504 | } 505 | out << YAML::Value << YAML::Flow << str_array; 506 | break; 507 | } 508 | case rclcpp::ParameterType::PARAMETER_STRING_ARRAY: 509 | { 510 | auto array = parameter.as_string_array(); 511 | out << YAML::Value << YAML::Flow << YAML::SingleQuoted << array; 512 | break; 513 | } 514 | default: { 515 | RCLCPP_WARN( 516 | this->get_logger(), 517 | "parameter %s unsupported type %d", 518 | name.c_str(), parameter.get_type()); 519 | break; 520 | } 521 | } 522 | } 523 | } 524 | } 525 | out << YAML::EndMap; 526 | } 527 | 528 | void ParameterServer::StoreYamlFile() 529 | { 530 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 531 | 532 | if (param_update_) 533 | { 534 | // Store yaml at finalization 535 | YAML::Node parameter_config; 536 | if (boost::filesystem::exists(persistent_yaml_file_)) 537 | { 538 | parameter_config = YAML::LoadFile(persistent_yaml_file_); 539 | } 540 | 541 | // if file is empty or bad format, reset it to map 542 | if (parameter_config.Type() != YAML::NodeType::Map) 543 | { 544 | parameter_config = YAML::Node(YAML::NodeType::Map); 545 | } 546 | 547 | YAML::Node node_parameter; 548 | 549 | if (parameter_use_stars_) { 550 | node_parameter = parameter_config["/**"][ROS_PARAMETER_KEY]; 551 | } else if (parameter_ns_exist_ && parameter_name_exist_) { 552 | node_parameter = parameter_config[get_namespace()][node_name_][ROS_PARAMETER_KEY]; 553 | } else if (parameter_name_exist_) { 554 | node_parameter = parameter_config[node_name_][ROS_PARAMETER_KEY]; 555 | } else { 556 | node_parameter = parameter_config[get_namespace()][node_name_][ROS_PARAMETER_KEY]; 557 | } 558 | 559 | node_parameter[PERSISTENT_KEY] = YAML::Node(YAML::NodeType::Map); 560 | 561 | for (std::set::iterator iter = changed_parameter_lists_.begin(); 562 | iter != changed_parameter_lists_.end(); 563 | ++iter) 564 | { 565 | std::string name = *iter; 566 | // split parameter name 567 | std::vector key_name_list; 568 | boost::split(key_name_list, name, boost::is_any_of(".")); 569 | 570 | rclcpp::Parameter parameter = get_parameter(name); 571 | 572 | switch (parameter.get_type()) 573 | { 574 | case rclcpp::ParameterType::PARAMETER_NOT_SET: 575 | { 576 | RCLCPP_INFO(this->get_logger(), "parameter %s is not set, it will not be stored", name.c_str()); 577 | break; 578 | } 579 | case rclcpp::ParameterType::PARAMETER_BOOL: 580 | { 581 | bool value = parameter.as_bool(); 582 | updateConfigParam(node_parameter, key_name_list, value); 583 | break; 584 | } 585 | case rclcpp::ParameterType::PARAMETER_INTEGER: 586 | { 587 | int64_t value = parameter.as_int(); 588 | updateConfigParam(node_parameter, key_name_list, value); 589 | break; 590 | } 591 | case rclcpp::ParameterType::PARAMETER_DOUBLE: 592 | { 593 | double value = parameter.as_double(); 594 | updateConfigParam(node_parameter, key_name_list, value); 595 | break; 596 | } 597 | case rclcpp::ParameterType::PARAMETER_STRING: 598 | { 599 | std::string value = parameter.as_string(); 600 | updateConfigParam(node_parameter, key_name_list, value); 601 | break; 602 | } 603 | case rclcpp::ParameterType::PARAMETER_BYTE_ARRAY: 604 | { 605 | auto byte_array = parameter.as_byte_array(); 606 | if (byte_array.size() == 0) { 607 | RCLCPP_WARN( 608 | this->get_logger(), 609 | "parameter %s value is empty, it will not be stored", name.c_str()); 610 | break; 611 | } 612 | YAML::Node seq; 613 | seq.SetStyle(YAML::EmitterStyle::Flow); 614 | for (auto byte : byte_array) 615 | { 616 | // TODO. rcl_yaml_param_parser not support byte array, use int array temporary 617 | seq.push_back((int64_t)byte); 618 | } 619 | updateConfigParam(node_parameter, key_name_list, seq); 620 | break; 621 | } 622 | case rclcpp::ParameterType::PARAMETER_BOOL_ARRAY: 623 | { 624 | auto bool_array = parameter.as_bool_array(); 625 | if (bool_array.size() == 0) { 626 | RCLCPP_WARN( 627 | this->get_logger(), 628 | "parameter %s value is empty, it will not be stored", name.c_str()); 629 | break; 630 | } 631 | YAML::Node seq; 632 | seq.SetStyle(YAML::EmitterStyle::Flow); 633 | for (bool b : bool_array) // Vector is specialized for bool 634 | { 635 | seq.push_back(b); 636 | } 637 | 638 | updateConfigParam(node_parameter, key_name_list, seq); 639 | break; 640 | } 641 | case rclcpp::ParameterType::PARAMETER_INTEGER_ARRAY: 642 | { 643 | auto array = parameter.as_integer_array(); 644 | if (array.size() == 0) { 645 | RCLCPP_WARN( 646 | this->get_logger(), 647 | "parameter %s value is empty, it will not be stored", name.c_str()); 648 | break; 649 | } 650 | YAML::Node seq; 651 | seq.SetStyle(YAML::EmitterStyle::Flow); 652 | for (auto i : array) 653 | { 654 | seq.push_back(i); 655 | } 656 | updateConfigParam(node_parameter, key_name_list, seq); 657 | break; 658 | } 659 | case rclcpp::ParameterType::PARAMETER_DOUBLE_ARRAY: 660 | { 661 | auto array = parameter.as_double_array(); 662 | if (array.size() == 0) { 663 | RCLCPP_WARN( 664 | this->get_logger(), 665 | "parameter %s value is empty, it will not be stored", name.c_str()); 666 | break; 667 | } 668 | YAML::Node seq; 669 | seq.SetStyle(YAML::EmitterStyle::Flow); 670 | for (auto d : array) 671 | { 672 | seq.push_back(d); 673 | } 674 | updateConfigParam(node_parameter, key_name_list, seq); 675 | break; 676 | } 677 | case rclcpp::ParameterType::PARAMETER_STRING_ARRAY: 678 | { 679 | auto array = parameter.as_string_array(); 680 | if (array.size() == 0) { 681 | RCLCPP_WARN( 682 | this->get_logger(), 683 | "parameter %s value is empty, it will not be stored", name.c_str()); 684 | break; 685 | } 686 | YAML::Node seq; 687 | seq.SetStyle(YAML::EmitterStyle::Flow); 688 | for (auto& str : array) 689 | { 690 | seq.push_back(str); 691 | } 692 | updateConfigParam(node_parameter, key_name_list, seq); 693 | break; 694 | } 695 | default: 696 | { 697 | RCLCPP_WARN( 698 | this->get_logger(), 699 | "parameter %s unsupported type %d", 700 | name.c_str(), parameter.get_type()); 701 | break; 702 | } 703 | } 704 | } 705 | 706 | // rcl_yaml_param_parser not supported the following format, need to remove it 707 | // /: 708 | // parameter_server_bdk: 709 | // ros__parameters: 710 | // persistent: 711 | // {} 712 | if (node_parameter[PERSISTENT_KEY].size() == 0) { 713 | node_parameter.remove(PERSISTENT_KEY); 714 | } 715 | 716 | if (parameter_use_stars_) { 717 | parameter_config["/**"][ROS_PARAMETER_KEY] = node_parameter; 718 | } else if (parameter_ns_exist_ && parameter_name_exist_) { 719 | parameter_config[get_namespace()][node_name_][ROS_PARAMETER_KEY] = node_parameter; 720 | } else if (parameter_name_exist_) { 721 | parameter_config[node_name_][ROS_PARAMETER_KEY] = node_parameter; 722 | } else { 723 | parameter_config[get_namespace()][node_name_][ROS_PARAMETER_KEY] = node_parameter; 724 | } 725 | 726 | // data -> YAML::Node -> YAML::Emitter(save string with "'") 727 | // use emitter to traverse all sub nodes, if value of Node is string, add ' between value. 728 | YAML::Emitter out; 729 | SaveNode(out, parameter_config); 730 | std::ofstream fout(persistent_yaml_file_); 731 | fout << out.c_str(); 732 | fout.close(); 733 | } 734 | } 735 | 736 | bool ParameterServer::CheckPersistentParam(const std::vector & parameters) 737 | { 738 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 739 | bool flag = false; 740 | 741 | for (auto& parameter : parameters) { 742 | std::string parameter_name = parameter.get_name(); 743 | if (parameter_name.find(PERSISTENT_DOT_KEY) != 0) { 744 | continue; 745 | } 746 | 747 | rclcpp::ParameterType parameter_type = parameter.get_type(); 748 | if (rclcpp::ParameterType::PARAMETER_NOT_SET == parameter_type) { 749 | RCLCPP_DEBUG(this->get_logger(), "parameter %s not set", parameter_name.c_str()); 750 | changed_parameter_lists_.erase(parameter_name); 751 | flag = true; 752 | } else { 753 | RCLCPP_DEBUG(this->get_logger(), "parameter %s changed", parameter_name.c_str()); 754 | changed_parameter_lists_.insert(parameter_name); 755 | flag = true; 756 | } 757 | } 758 | 759 | return flag; 760 | } 761 | --------------------------------------------------------------------------------