├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── codespell.yaml │ ├── humble-nightly.yml │ ├── humble.yml │ ├── jazzy-nightly.yml │ ├── jazzy.yml │ ├── kilted-nightly.yml │ ├── kilted.yml │ ├── mirror-rolling-to-master.yaml │ ├── rolling-nightly.yml │ └── rolling.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codespell.cfg ├── codespell_dictionary.txt ├── codespell_whitelist.txt ├── docker └── Dockerfile ├── images ├── QR.png └── overview_architecture.png ├── presentation ├── ros2_parameter_server.html ├── ros2_parameter_server.md └── ros2_parameter_server.pdf ├── requirements.txt ├── scripts ├── build-verification.sh └── docker_release.sh ├── server ├── CHANGELOG.rst ├── CMakeLists.txt ├── include │ └── parameter_server.h ├── launch │ └── parameter_server.launch.py ├── package.xml ├── param │ ├── parameter_server.yaml │ ├── parameters_via_cli.yaml │ └── parameters_via_launch.yaml └── src │ ├── main.cpp │ └── parameter_server.cpp └── test ├── CMakeLists.txt ├── include └── persist_parameter_client.hpp ├── launch └── test.launch.py ├── package.xml ├── src ├── persist_parameter_client.cpp └── test.cpp └── test.py /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yaml: -------------------------------------------------------------------------------- 1 | name: codespell 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | spellcheck: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | 12 | - name: Setup Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: '3.10' 16 | 17 | - name: Install dependencies with pip 18 | run: pip install --no-warn-script-location --user -r requirements.txt 19 | 20 | - name: Spellcheck 21 | run: codespell --config codespell.cfg 22 | -------------------------------------------------------------------------------- /.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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | - [Sequence](#sequence) 20 | - [Getting Started](#getting-started) 21 | - [Supported Distribution](#supported-distribution) 22 | - [Docker Container](#docker-container) 23 | - [Dependent Packages](#dependent-packages) 24 | - [Prerequisites](#prerequisites) 25 | - [Build](#build) 26 | - [Run](#run) 27 | - [Test](#test) 28 | - [Run](#run-1) 29 | - [Known Issues](#known-issues) 30 | - [Authors](#authors) 31 | - [License](#license) 32 | 33 | 34 | 35 | ## Background 36 | 37 | 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. 38 | 39 | Although, it is expected to be the following requirement. 40 | 41 | - Global configuration that many nodes share (e.g. RTOS priorities, vehicle dimensions, …) 42 | - Generic ROS 2 system property server. 43 | - 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.) 44 | - Using ROS1 based application with Parameter Server. 45 | 46 | ## Overview 47 | 48 | ![overview_architecture](./images/overview_architecture.png) 49 | 50 | 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. 51 | 52 | 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. 53 | 54 | ### Persistent Parameter Registration 55 | 56 | #### Persistent Prefix 57 | 58 | persistent parameter must have prefix ***"persistent"*** 59 | 60 | #### Scope Overview 61 | 62 | 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. 63 | 64 | | Category | Supported | Description | 65 | | ---- | ---- | ---- | 66 | | Parameter API | YES | ROS 2 Parameter Client API supported, since this activity can be detected via **/parameter_events**. | 67 | | 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 | 68 | | 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. | 69 | | 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. | 70 | | Launch Parameter | NO | e.g) ros2 launch 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. | 71 | 72 | ### Configurable Options 73 | 74 | - Node Name 75 | 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 '_'. 76 | - Persistent Volume 77 | 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. 78 | - Node Options 79 | there are two important options, 80 | allow_undeclared_parameters: (default true) 81 | automatically_declare_parameters_from_overrides: (default true) 82 | 83 | all of the configuration options will be passed via arguments as following. 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 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 |
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.
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.
120 | 121 | ## Sequence 122 | 123 | 1. parameter server is initialized via __params:= 124 | this is just a initial parameter(not persistent) to load into the parameter server's memory. 125 | 2. parameter server loads parameter specified yaml file via --file-path. 126 | and then parameter server will overwrite or declare parameters. 127 | (*) at #1 parameters might be overwritten. 128 | 3. parameter server starts the main loop with callback for parameter changes. 129 | 4. if the parameter changes are on "/persistent" that will be stored in storage at this time. 130 | 5. at the finalization, flash all of the "/persistent" parameters into the file system. 131 | 132 | ## Getting Started 133 | 134 | ### Supported Distribution 135 | 136 | - [ROS 2 Rolling Ridley](https://docs.ros.org/en/rolling/index.html) 137 | - [ROS 2 Kilted Kaiju](https://docs.ros.org/en/kilted/index.html) 138 | - [ROS 2 Jazzy Jalisco](https://docs.ros.org/en/jazzy/index.html) 139 | - [ROS 2 Humble Hawksbill](https://docs.ros.org/en/humble/index.html) 140 | 141 | #### Docker Container 142 | 143 | see available images for [tomoyafujita/ros2_param_server@dockerhub](https://hub.docker.com/repository/docker/tomoyafujita/ros2_param_server/general) 144 | 145 | ```bash 146 | docker run -it --rm --net=host tomoyafujita/ros2_param_server:humble 147 | ``` 148 | 149 | ### Dependent Packages 150 | 151 | ```bash 152 | apt install libyaml-cpp-dev libboost-program-options-dev libboost-filesystem-dev 153 | ``` 154 | 155 | ### Prerequisites 156 | 157 | ros2 source build environment([Linux-Development-Setup/](https://index.ros.org/doc/ros2/Installation/Rolling/Linux-Development-Setup/)) is required to build and run the parameter server. 158 | 159 | ### Build 160 | 161 | to install local colcon workspace, 162 | 163 | ```bash 164 | # cd /src 165 | # git clone https://github.com/fujitatomoya/ros2_persist_parameter_server 166 | # cd 167 | # colcon build --symlink-install --packages-select parameter_server ros2_persistent_parameter_server_test 168 | # source install/local_setup.bash 169 | ``` 170 | 171 | ### Run 172 | 173 | 1. start parameter server. 174 | 175 | ```bash 176 | # cp /src/ros2_persist_parameter_server/server/param/parameter_server.yaml /tmp/ 177 | # ros2 run parameter_server server 178 | [INFO] [parameter_server]: Parameter Server node named: '/parameter_server' started and ready, and serving '9' parameters already! 179 | ... 180 | ``` 181 | 182 | 2. update persistent parameter. 183 | 184 | ```bash 185 | # ros2 param set /parameter_server persistent.some_int 81 186 | Set parameter successful 187 | # ros2 param set /parameter_server persistent.a_string Konnichiwa 188 | Set parameter successful 189 | # ros2 param set /parameter_server persistent.pi 3.14159265359 190 | Set parameter successful 191 | # ros2 param set /parameter_server persistent.some_lists.some_integers 81,82,83,84 192 | Set parameter successful 193 | ``` 194 | 195 | 3. restart parameter server. 196 | 197 | ```bash 198 | # ros2 run parameter_server server 199 | [INFO] [parameter_server]: Parameter Server node named: '/parameter_server' started and ready, and serving '9' parameters already! 200 | ... 201 | ``` 202 | 203 | 4. check persistent parameter is precisely cached and loaded into parameter server. 204 | 205 | ```bash 206 | # ros2 param get /parameter_server persistent.a_string 207 | String value is: Konnichiwa 208 | # ros2 param get /parameter_server persistent.pi 209 | Double value is: 3.14159265359 210 | # ros2 param get /parameter_server persistent.some_int 211 | Integer value is: 81 212 | # ros2 param get /parameter_server persistent.some_lists.some_integers 213 | String value is: 81,82,83,84 214 | ``` 215 | 216 | ## Test 217 | 218 | These samples verify the following functions. 219 | 220 | - persistent parameter can be read/stored to/from the file system. 221 | - persistent parameter can be read/modified from parameter client. 222 | - non-persistent parameter cannot be read/stored to/from the file system. 223 | - non-persistent parameter can be read/modified from parameter client 224 | 225 | make sure to add the path of `launch` package to the PATH environment. 226 | 227 | ```bash 228 | # source /install/setup.bash 229 | ``` 230 | 231 | ### Run 232 | 233 | [test.py](./test/test.py) is the entry for test. 234 | 235 | [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. 236 | 237 | !!!NOTE 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`. 238 | 239 | ```bash 240 | # mkdir -p /tmp/test 241 | # cp /src/ros2_persist_parameter/server/param/parameter_server.yaml /tmp/test 242 | # .//src/ros2_persist_parameter/test/test.py 243 | ``` 244 | 245 | All of the test is listed with result as following 246 | 247 | !!!NOTE Client has a 5-seconds sleep during server restarts. 248 | 249 | ```bash 250 | ...... // omit some output logs 251 | 252 | [ros2-2] [INFO] [1601447662.145760479] [client]: *************************************************************************** 253 | [ros2-2] [INFO] [1601447662.145794365] [client]: *********************************Test Result******************************* 254 | [ros2-2] [INFO] [1601447662.145817265] [client]: a. Read Normal Parameter : PASS 255 | [ros2-2] [INFO] [1601447662.145842530] [client]: b. Read Persistent Parameter : PASS 256 | [ros2-2] [INFO] [1601447662.145863430] [client]: c. Modify Existed Normal parameter : PASS 257 | [ros2-2] [INFO] [1601447662.145885082] [client]: d. Modify Existed Persistent parameter : PASS 258 | [ros2-2] [INFO] [1601447662.145906067] [client]: e. Add New Normal parameter : PASS 259 | [ros2-2] [INFO] [1601447662.145926790] [client]: f. Add New Persistent parameter : PASS 260 | [ros2-2] [INFO] [1601447662.145948146] [client]: g. Test Normal Parameter Not Stores To File : PASS 261 | [ros2-2] [INFO] [1601447662.145969623] [client]: h. Test Persistent Parameter Stores To File : PASS 262 | [ros2-2] [INFO] [1601447662.145990707] [client]: i. Test New Added Normal Parameter Not Stores To File : PASS 263 | [ros2-2] [INFO] [1601447662.146011312] [client]: j. Test New Added Persistent Parameter Stores To File : PASS 264 | ``` 265 | 266 | ## Known Issues 267 | 268 | - [Error when loading a persistent parameter of type array](https://github.com/fujitatomoya/ros2_persist_parameter_server/issues/13) 269 | - `[1.0,1.1]` is deduced as `[1, 1.1000000000000001]` because of [yaml-cpp bug](https://github.com/jbeder/yaml-cpp/issues/1016), this will leads to `Failed to parse parameters` exception when loading the persistent parameters from yaml file. 270 | - The work-around is to use string sequence `["1.0", "1.1"]` instead of double type, then change it into `float()` in the program. 271 | 272 | - [Signal2(2) needs to be injected to the server executable](https://github.com/fujitatomoya/ros2_persist_parameter_server/issues/24) 273 | - 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. 274 | - 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 `. 275 | 276 | ## Authors 277 | 278 | - **Tomoya Fujita** --- Tomoya.Fujita@sony.com 279 | 280 | ## License 281 | 282 | Apache 2.0 283 | -------------------------------------------------------------------------------- /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 15 | -------------------------------------------------------------------------------- /codespell_dictionary.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fujitatomoya/ros2_persist_parameter_server/9e3af2cd7b02b32b6f98ac4d06a7de6ab60909b7/codespell_dictionary.txt -------------------------------------------------------------------------------- /codespell_whitelist.txt: -------------------------------------------------------------------------------- 1 | thead 2 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /images/QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fujitatomoya/ros2_persist_parameter_server/9e3af2cd7b02b32b6f98ac4d06a7de6ab60909b7/images/QR.png -------------------------------------------------------------------------------- /images/overview_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fujitatomoya/ros2_persist_parameter_server/9e3af2cd7b02b32b6f98ac4d06a7de6ab60909b7/images/overview_architecture.png -------------------------------------------------------------------------------- /presentation/ros2_parameter_server.html: -------------------------------------------------------------------------------- 1 |
12 |
ROS 2 Persistent Parameter Server
13 | 14 |

ROS 2 Persistent Parameter Server

15 | 16 |
    17 |
  • inspired by ROS 1 parameter server.
  • 18 |
  • can set/get any parameters in this global server.
  • 19 |
  • can save/load the parameters in storage.
  • 20 |
21 | 22 |
23 |
24 |
ROS 2 Persistent Parameter Server
25 | 26 |
27 |
28 |
ROS 2 Persistent Parameter Server
29 |

Why we need this?

30 |
    31 |
  • Global configuration that many nodes share (e.g. RTOS priorities, vehicle dimensions, …)
  • 32 |
  • Generic ROS 2 system or localhost wide parameter server.
  • 33 |
  • Persistent storage support to re-initialize the system. 34 |
      35 |
    • 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.)
    • 36 |
    37 |
  • 38 |
  • Using ROS 1 based application with Parameter Server.
  • 39 |
40 | 41 |
42 |
43 |
ROS 2 Persistent Parameter Server
44 | 45 |
46 |
47 |
ROS 2 Persistent Parameter Server
48 |

Issues and PRs are always welcome 🚀

49 |

https://github.com/fujitatomoya/ros2_persist_parameter_server

50 | 51 |
52 |

Comment Here

Supported platforms

Comment Here

Comment Here

-------------------------------------------------------------------------------- /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 | ## Issues and PRs are always welcome 🚀 52 | 53 | https://github.com/fujitatomoya/ros2_persist_parameter_server 54 | 55 | ![bg left:35% width:300px](../images/QR.png) 56 | 57 | 60 | -------------------------------------------------------------------------------- /presentation/ros2_parameter_server.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fujitatomoya/ros2_persist_parameter_server/9e3af2cd7b02b32b6f98ac4d06a7de6ab60909b7/presentation/ros2_parameter_server.pdf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | codespell 2 | -------------------------------------------------------------------------------- /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 parameter_server ros2_persistent_parameter_server_test 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /server/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(parameter_server VERSION 1.0.1) 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(rmw REQUIRED) 31 | 32 | find_package(Boost REQUIRED COMPONENTS program_options filesystem) 33 | find_package(yaml_cpp_vendor REQUIRED) 34 | 35 | add_executable(server 36 | src/parameter_server.cpp 37 | src/main.cpp 38 | ) 39 | 40 | # yaml-cpp updates CMake thing significantly on v0.8.0 or later. 41 | # so we ended up having the if statement to process differently instead of creating branches. 42 | # see https://github.com/jbeder/yaml-cpp/releases/tag/0.8.0 43 | find_package(yaml-cpp 0.8.0 QUIET) 44 | if (yaml-cpp_FOUND) 45 | message(STATUS "yaml-cpp package is greater equal than version 0.8.0") 46 | target_link_libraries(server 47 | rclcpp::rclcpp 48 | rclcpp_components::component 49 | rcutils::rcutils 50 | yaml-cpp::yaml-cpp 51 | ${std_msgs_TARGETS} 52 | ${Boost_LIBRARIES} 53 | ) 54 | else() 55 | message(STATUS "yaml-cpp package is less than version 0.8.0") 56 | find_package(yaml-cpp REQUIRED) 57 | target_link_libraries(server 58 | rclcpp::rclcpp 59 | rclcpp_components::component 60 | rcutils::rcutils 61 | yaml-cpp 62 | ${std_msgs_TARGETS} 63 | ${Boost_LIBRARIES} 64 | ) 65 | endif() 66 | 67 | target_include_directories(server 68 | PUBLIC 69 | $ 70 | ) 71 | 72 | install(TARGETS server DESTINATION lib/${PROJECT_NAME}) 73 | 74 | # Install launch files. 75 | install(DIRECTORY 76 | launch 77 | param 78 | DESTINATION share/${PROJECT_NAME}/ 79 | ) 80 | 81 | ament_package() 82 | -------------------------------------------------------------------------------- /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 "rclcpp_components/register_node_macro.hpp" 25 | #include "yaml-cpp/yaml.h" 26 | 27 | class ParameterServer : public rclcpp::Node 28 | { 29 | public: 30 | RCLCPP_SMART_PTR_DEFINITIONS(ParameterServer) 31 | 32 | ParameterServer( 33 | const std::string & node_name, 34 | const rclcpp::NodeOptions & options, 35 | const std::string & persistent_yaml_file, 36 | unsigned int storing_period); 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 ValidateYamlFile(YAML::Node node, const std::string& key = ""); 51 | void SaveNode(YAML::Emitter& out, YAML::Node node, const std::string& key = ""); 52 | 53 | // Check whether parameter name contains "persistent." in the parameter list 54 | bool CheckPersistentParam(const std::vector & parameters); 55 | 56 | // Check flag to store file 57 | std::atomic_bool param_update_; 58 | 59 | // yaml file to load/store 60 | std::string persistent_yaml_file_; 61 | 62 | // store changed(add, update) parameter name contains "persistent." after checking in 'parameter_events' callback 63 | std::set changed_parameter_lists_; 64 | 65 | // To adapt the original yaml format that contain namespace(optional) and nodename(can be /**) 66 | bool parameter_use_stars_ = false; 67 | bool parameter_ns_exist_ = false; 68 | bool parameter_name_exist_ = false; 69 | std::string node_name_; 70 | 71 | // set parameters callback handler 72 | OnSetParametersCallbackHandle::SharedPtr callback_handler_; 73 | 74 | // for periodic storing to the file system 75 | rclcpp::TimerBase::SharedPtr timer_; 76 | }; 77 | 78 | #endif // __PARAMETER_SERVER_H__ 79 | -------------------------------------------------------------------------------- /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="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 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 | # ], 52 | ) 53 | ] 54 | ) 55 | -------------------------------------------------------------------------------- /server/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | parameter_server 5 | 1.0.1 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 | rclcpp 16 | rclcpp_components 17 | rcutils 18 | rmw 19 | rmw_implementation_cmake 20 | std_msgs 21 | yaml_cpp_vendor 22 | 23 | launch_ros 24 | 25 | ament_cmake_pytest 26 | ament_lint_auto 27 | ament_lint_common 28 | launch 29 | 30 | 31 | ament_cmake 32 | 33 | 34 | -------------------------------------------------------------------------------- /server/param/parameter_server.yaml: -------------------------------------------------------------------------------- 1 | /**: 2 | ros__parameters: 3 | # This file is expected to be used as following, 4 | # > ros2 run 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 | -------------------------------------------------------------------------------- /server/param/parameters_via_cli.yaml: -------------------------------------------------------------------------------- 1 | /**: 2 | ros__parameters: 3 | # This file is expected to be used 4 | # > ros2 run 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/parameters_via_launch.yaml: -------------------------------------------------------------------------------- 1 | /**: 2 | ros__parameters: 3 | # This file is expected to be used with ros2 launch 4 | # > ros2 launch 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/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 | 46 | variables_map vm; 47 | store(basic_command_line_parser(nonros_args).options(description).run(), vm); 48 | notify(vm); 49 | 50 | std::string node_name = "parameter_server"; 51 | string opt_file("/tmp/parameter_server.yaml"); 52 | bool opt_allow_declare = true; 53 | bool opt_allow_override = true; 54 | unsigned int storing_period = 60; 55 | 56 | if (vm.count("help")) 57 | { 58 | cout << description << endl; 59 | rclcpp::shutdown(); 60 | return ret; 61 | } 62 | else 63 | { 64 | opt_file = vm["file-path"].as(); 65 | opt_allow_declare = vm["allow-declare"].as(); 66 | opt_allow_override = vm["allow-override"].as(); 67 | storing_period = vm["storing-period"].as(); 68 | } 69 | 70 | rclcpp::NodeOptions options = ( 71 | rclcpp::NodeOptions() 72 | .allow_undeclared_parameters(opt_allow_declare) 73 | .automatically_declare_parameters_from_overrides(opt_allow_override) 74 | ); 75 | 76 | ParameterServer::SharedPtr node = nullptr; 77 | try 78 | { 79 | node = ParameterServer::make_shared(node_name, options, opt_file, storing_period); 80 | if (node == nullptr) 81 | { 82 | throw std::bad_alloc(); 83 | } 84 | 85 | RCLCPP_INFO(node->get_logger(), 86 | "Parameter Server node named: '%s' started and ready, and serving '%zu' parameters already!", 87 | node->get_fully_qualified_name(), 88 | node->list_parameters({}, rcl_interfaces::srv::ListParameters::Request::DEPTH_RECURSIVE).names.size()); 89 | 90 | rclcpp::spin(node); 91 | } 92 | catch (const std::exception& e) 93 | { 94 | std::cerr << "Catch exception: " << e.what() << std::endl; 95 | ret = EXIT_FAILURE; 96 | } 97 | 98 | rclcpp::shutdown(); 99 | return ret; 100 | } 101 | -------------------------------------------------------------------------------- /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 | 23 | #include "rcl_yaml_param_parser/parser.h" 24 | #include "rclcpp/parameter.hpp" 25 | #include "rclcpp/parameter_map.hpp" 26 | 27 | #define ROS_PARAMETER_KEY "ros__parameters" 28 | #define ROS_PARAMETER_DOT_KEY "ros__parameters." 29 | #define PERSISTENT_KEY "persistent" 30 | #define PERSISTENT_DOT_KEY "persistent." 31 | 32 | ParameterServer::ParameterServer( 33 | const std::string & node_name, 34 | const rclcpp::NodeOptions & options, 35 | const std::string & persistent_yaml_file, 36 | unsigned int storing_period) 37 | : Node(node_name, options), 38 | param_update_(false), 39 | persistent_yaml_file_(persistent_yaml_file), 40 | node_name_(get_name()) 41 | { 42 | RCLCPP_DEBUG(this->get_logger(), "%s yaml:%s", __PRETTY_FUNCTION__, persistent_yaml_file_.c_str()); 43 | 44 | if (!storing_period) { 45 | RCLCPP_INFO( 46 | this->get_logger(), "Period is 0. Will not perform periodic persistent parameter storing"); 47 | } else { 48 | timer_ = this->create_wall_timer( 49 | std::chrono::seconds(storing_period), std::bind(&ParameterServer::StoreYamlFile, this)); 50 | 51 | RCLCPP_INFO( 52 | this->get_logger(), "Will perform periodic persistent parameter storing every %ds", 53 | storing_period); 54 | } 55 | 56 | // Declare a parameter change request callback 57 | auto param_change_callback = 58 | [this](const std::vector & parameters) 59 | { 60 | auto result = rcl_interfaces::msg::SetParametersResult(); 61 | result.successful = true; 62 | 63 | if (CheckPersistentParam(parameters)) 64 | { 65 | if (!param_update_) 66 | { 67 | param_update_ = true; 68 | } 69 | } 70 | 71 | return result; 72 | }; 73 | // callback_handler_ needs to be alive to keep the callback functional 74 | callback_handler_ = this->add_on_set_parameters_callback(param_change_callback); 75 | 76 | LoadYamlFile(); 77 | } 78 | 79 | ParameterServer::~ParameterServer() 80 | { 81 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 82 | this->remove_on_set_parameters_callback(callback_handler_.get()); 83 | StoreYamlFile(); 84 | } 85 | 86 | // Add a limitation that A node that is a map in custom YAML file can't contain '.' in the key name 87 | void ParameterServer::ValidateYamlFile(YAML::Node node, const std::string& key) { 88 | for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) 89 | { 90 | if (it->second.Type() == YAML::NodeType::Map) { 91 | std::string key_name = key; 92 | std::string tag = it->first.as(); 93 | key_name += "[" + tag + "]"; 94 | if (tag.find(".") != std::string::npos) { 95 | std::ostringstream ss; 96 | ss << "Custom YAML file '" << persistent_yaml_file_ << " format is invalid. " 97 | << "[A node(" << key_name << ") that is a map in custom YAML file can't contain '.' in the key name"; 98 | throw std::runtime_error(ss.str()); 99 | } 100 | 101 | ValidateYamlFile(it->second, key_name); 102 | } 103 | } 104 | } 105 | 106 | void ParameterServer::CheckYamlFile() { 107 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 108 | YAML::Node parameter_config = YAML::LoadFile(persistent_yaml_file_); 109 | // check format "YAML must be dictionary type and level 1 can only have one key" 110 | if ((parameter_config.size() == 1 && parameter_config.Type() != YAML::NodeType::Map) || 111 | parameter_config.size() > 1) { 112 | std::ostringstream ss; 113 | ss << "Custom YAML file '" << persistent_yaml_file_ << " format is invalid. [YAML must be dictionary type and level 1 can only have one key]"; 114 | throw std::runtime_error(ss.str()); 115 | } 116 | 117 | if (parameter_config.size() == 1 && parameter_config.Type() == YAML::NodeType::Map) { 118 | if (parameter_config["/**"]) { 119 | parameter_use_stars_ = true; 120 | } else { 121 | if (parameter_config[node_name_] && parameter_config[node_name_]["ros__parameters"]) { 122 | parameter_name_exist_ = true; 123 | } else { 124 | std::string tmp = "/" + node_name_; 125 | if (parameter_config[tmp] && parameter_config[tmp]["ros__parameters"]) { 126 | parameter_name_exist_ = true; 127 | node_name_ = tmp; 128 | } 129 | } 130 | 131 | if (!parameter_name_exist_) { 132 | if (parameter_config[get_namespace()] && 133 | parameter_config[get_namespace()][node_name_] && 134 | parameter_config[get_namespace()][node_name_]["ros__parameters"]) { 135 | parameter_ns_exist_ = true; 136 | parameter_name_exist_ = true; 137 | } 138 | } 139 | } 140 | 141 | if (!parameter_use_stars_ && !parameter_ns_exist_ && !parameter_name_exist_) { 142 | std::ostringstream ss; 143 | ss << "Custom YAML file '" << persistent_yaml_file_ 144 | << " content is invalid. [namespace can be optional or '" << get_namespace() << "', but node name must be exist with a concrete name'" 145 | << get_name() << "' or '/**']"; 146 | throw std::runtime_error(ss.str()); 147 | } 148 | } 149 | 150 | ValidateYamlFile(parameter_config); 151 | } 152 | 153 | void ParameterServer::LoadYamlFile() 154 | { 155 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 156 | // check whether yaml file exist 157 | if (!boost::filesystem::exists(persistent_yaml_file_)) 158 | { 159 | RCLCPP_WARN(this->get_logger(), "Custom YAML file %s not exist", persistent_yaml_file_.c_str()); 160 | return; 161 | } 162 | 163 | // check whether yaml file is valid 164 | CheckYamlFile(); 165 | 166 | // use rcl_yaml_param_parser to load custom yaml file 167 | rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base = this->get_node_base_interface(); 168 | rclcpp::node_interfaces::NodeParametersInterface::SharedPtr node_parameters = get_node_parameters_interface(); 169 | 170 | // Get the node options 171 | const rcl_node_t * node = node_base->get_rcl_node_handle(); 172 | if (nullptr == node) 173 | { 174 | throw std::runtime_error("Need valid node handle in NodeParameters"); 175 | } 176 | const rcl_node_options_t * options = rcl_node_get_options(node); 177 | if (nullptr == options) 178 | { 179 | throw std::runtime_error("Need valid node options in NodeParameters"); 180 | } 181 | 182 | rcl_params_t * yaml_params = rcl_yaml_node_struct_init(options->allocator); 183 | if (nullptr == yaml_params) 184 | { 185 | throw std::bad_alloc(); 186 | } 187 | if (!rcl_parse_yaml_file(persistent_yaml_file_.c_str(), yaml_params)) 188 | { 189 | std::ostringstream ss; 190 | ss << "Failed to parse parameters from custom yaml file '" << persistent_yaml_file_ << "': " << 191 | rcl_get_error_string().str; 192 | rcl_reset_error(); 193 | throw std::runtime_error(ss.str()); 194 | } 195 | 196 | rclcpp::ParameterMap initial_map = rclcpp::parameter_map_from(yaml_params); 197 | rcl_yaml_node_struct_fini(yaml_params); 198 | 199 | for (auto iter = initial_map.begin(); initial_map.end() != iter; iter++) 200 | { 201 | if (iter->first == "/**" || iter->first == node_base->get_fully_qualified_name()) 202 | { 203 | // set (add, update) parameter with custom yaml file 204 | for (auto & param : iter->second) 205 | { 206 | std::string name = param.get_name(); 207 | rclcpp::ParameterValue value = rclcpp::ParameterValue(param.get_value_message()); 208 | 209 | bool has_parameter_flag = node_parameters->has_parameter(name); 210 | bool undeclare_exist_override = false; 211 | 212 | if (!has_parameter_flag) 213 | { 214 | // declare parameter 215 | RCLCPP_DEBUG(this->get_logger(), "declare %s %s", name.c_str(), to_string(value).c_str()); 216 | node_parameters->declare_parameter( 217 | name, 218 | value, 219 | rcl_interfaces::msg::ParameterDescriptor()); 220 | 221 | // 1. if automatically_declare_parameters_from_overrides is false, 222 | // parameter from __params: not declared but saved in overrides list, 223 | // to check "has_parameter" return false and 224 | // to call "declare_parameter" will use value of overrides list to replace passed value. 225 | // 2. custom yaml file need to override whatever the automatically_declare_parameters_from_overrides is, 226 | // continue to check if name exist in overrides, if yes, set_parameters later 227 | const std::map &om = node_parameters->get_parameter_overrides(); 228 | if (om.find(name) != om.end()) 229 | { 230 | undeclare_exist_override = true; 231 | } 232 | } 233 | 234 | if (has_parameter_flag || undeclare_exist_override) 235 | { 236 | RCLCPP_DEBUG(this->get_logger(), "set %s %s (has_parameter_flag:%d, undeclare_exist_override:%d", 237 | name.c_str(), to_string(value).c_str(), 238 | has_parameter_flag, undeclare_exist_override); 239 | 240 | auto set_parameters_results = node_parameters->set_parameters({ 241 | rclcpp::Parameter(name, value) 242 | }); 243 | for (auto & result : set_parameters_results) 244 | { 245 | if (!result.successful) 246 | { 247 | RCLCPP_WARN(get_logger(), "Failed to set parameter: %s", result.reason.c_str()); 248 | } 249 | } 250 | } 251 | } 252 | } 253 | } 254 | } 255 | 256 | template 257 | void setConfigParam(YAML::Node node, Iter begin, Iter end, T value) 258 | { 259 | if (begin == end) 260 | { 261 | return; 262 | } 263 | auto tag = *begin; 264 | 265 | if (std::next(begin) == end) 266 | { 267 | node[tag] = value; 268 | return; 269 | } 270 | if (!node[tag]) 271 | { 272 | node[tag] = YAML::Node(YAML::NodeType::Map); 273 | } 274 | else 275 | { 276 | // if tag is not map, set all left tags as a key with the value 277 | if (node[tag].Type() != YAML::NodeType::Map) 278 | { 279 | while (true) { 280 | ++begin; 281 | tag += "." + *begin; 282 | 283 | if (std::next(begin) == end) { 284 | node[tag] = value; 285 | return; 286 | } 287 | } 288 | } 289 | } 290 | 291 | setConfigParam(node[tag], std::next(begin), end, value); 292 | } 293 | 294 | template 295 | void updateConfigParam(YAML::Node node, const std::vector& key_name_list, T value) 296 | { 297 | setConfigParam(node, key_name_list.begin(), key_name_list.end(), value); 298 | } 299 | 300 | void ParameterServer::SaveNode(YAML::Emitter& out, YAML::Node node, const std::string& key) 301 | { 302 | out << YAML::BeginMap; 303 | for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) 304 | { 305 | out << YAML::Key << it->first; 306 | 307 | std::string key_name; 308 | if (key.empty()) 309 | { 310 | key_name = it->first.as(); 311 | } 312 | else 313 | { 314 | key_name = key + "." + it->first.as(); 315 | } 316 | 317 | if (it->second.Type() == YAML::NodeType::Map) 318 | { 319 | SaveNode(out, it->second, key_name); 320 | } 321 | else 322 | { 323 | std::string ros_parameter_key = ROS_PARAMETER_DOT_KEY; 324 | std::size_t pos = key_name.find(ros_parameter_key + PERSISTENT_DOT_KEY); 325 | if (pos == std::string::npos) { 326 | // not a persistent key 327 | out << YAML::Value << it->second; 328 | } else { 329 | std::size_t pos = key_name.find(ros_parameter_key); 330 | std::string name = key_name.substr(pos + ros_parameter_key.length()); 331 | // get key type of parameter 332 | rclcpp::Parameter parameter = get_parameter(name); 333 | switch (parameter.get_type()) 334 | { 335 | case rclcpp::ParameterType::PARAMETER_NOT_SET: 336 | { 337 | RCLCPP_INFO(this->get_logger(), "parameter %s not set(or deleted), it will not be stored", name.c_str()); 338 | break; 339 | } 340 | case rclcpp::ParameterType::PARAMETER_BOOL: 341 | { 342 | bool value = parameter.as_bool(); 343 | out << YAML::Value << value; 344 | break; 345 | } 346 | case rclcpp::ParameterType::PARAMETER_INTEGER: 347 | { 348 | int64_t value = parameter.as_int(); 349 | out << YAML::Value << value; 350 | break; 351 | } 352 | case rclcpp::ParameterType::PARAMETER_DOUBLE: 353 | { 354 | double value = parameter.as_double(); 355 | out << YAML::Value << value; 356 | break; 357 | } 358 | case rclcpp::ParameterType::PARAMETER_STRING: 359 | { 360 | std::string value = parameter.as_string(); 361 | out << YAML::Value << YAML::SingleQuoted << value; 362 | break; 363 | } 364 | case rclcpp::ParameterType::PARAMETER_BYTE_ARRAY: 365 | { 366 | // TODO. rcl_yaml_param_parser not support byte array, use int array temporary 367 | auto array = parameter.as_byte_array(); 368 | std::vector int_array; 369 | for (uint8_t b: array) { 370 | int_array.push_back(int64_t(b)); 371 | } 372 | out << YAML::Value << YAML::Flow << int_array; 373 | break; 374 | } 375 | case rclcpp::ParameterType::PARAMETER_BOOL_ARRAY: 376 | { 377 | auto array = parameter.as_bool_array(); 378 | out << YAML::Value << YAML::Flow << array; 379 | break; 380 | } 381 | case rclcpp::ParameterType::PARAMETER_INTEGER_ARRAY: 382 | { 383 | auto array = parameter.as_integer_array(); 384 | out << YAML::Value << YAML::Flow << array; 385 | break; 386 | } 387 | case rclcpp::ParameterType::PARAMETER_DOUBLE_ARRAY: 388 | { 389 | auto array = parameter.as_double_array(); 390 | out << YAML::Value << YAML::Flow << array; 391 | break; 392 | } 393 | case rclcpp::ParameterType::PARAMETER_STRING_ARRAY: 394 | { 395 | auto array = parameter.as_string_array(); 396 | out << YAML::Value << YAML::Flow << YAML::SingleQuoted << array; 397 | break; 398 | } 399 | default: { 400 | RCLCPP_WARN(this->get_logger(), "parameter %s unsupported type %d", 401 | name.c_str(), parameter.get_type()); 402 | break; 403 | } 404 | } 405 | } 406 | } 407 | } 408 | out << YAML::EndMap; 409 | } 410 | 411 | void ParameterServer::StoreYamlFile() 412 | { 413 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 414 | 415 | if (param_update_) 416 | { 417 | // Store yaml at finalization 418 | YAML::Node parameter_config; 419 | if (boost::filesystem::exists(persistent_yaml_file_)) 420 | { 421 | parameter_config = YAML::LoadFile(persistent_yaml_file_); 422 | } 423 | 424 | // if file is empty or bad format, reset it to map 425 | if (parameter_config.Type() != YAML::NodeType::Map) 426 | { 427 | parameter_config = YAML::Node(YAML::NodeType::Map); 428 | } 429 | 430 | YAML::Node node_parameter; 431 | 432 | if (parameter_use_stars_) { 433 | node_parameter = parameter_config["/**"][ROS_PARAMETER_KEY]; 434 | } else if (parameter_ns_exist_ && parameter_name_exist_) { 435 | node_parameter = parameter_config[get_namespace()][node_name_][ROS_PARAMETER_KEY]; 436 | } else if (parameter_name_exist_) { 437 | node_parameter = parameter_config[node_name_][ROS_PARAMETER_KEY]; 438 | } else { 439 | node_parameter = parameter_config[get_namespace()][node_name_][ROS_PARAMETER_KEY]; 440 | } 441 | 442 | node_parameter[PERSISTENT_KEY] = YAML::Node(YAML::NodeType::Map); 443 | 444 | for (std::set::iterator iter = changed_parameter_lists_.begin(); 445 | iter != changed_parameter_lists_.end(); 446 | ++iter) 447 | { 448 | std::string name = *iter; 449 | // split parameter name 450 | std::vector key_name_list; 451 | boost::split(key_name_list, name, boost::is_any_of(".")); 452 | 453 | rclcpp::Parameter parameter = get_parameter(name); 454 | 455 | switch (parameter.get_type()) 456 | { 457 | case rclcpp::ParameterType::PARAMETER_NOT_SET: 458 | { 459 | RCLCPP_INFO(this->get_logger(), "parameter %s is not set, it will not be stored", name.c_str()); 460 | break; 461 | } 462 | case rclcpp::ParameterType::PARAMETER_BOOL: 463 | { 464 | bool value = parameter.as_bool(); 465 | updateConfigParam(node_parameter, key_name_list, value); 466 | break; 467 | } 468 | case rclcpp::ParameterType::PARAMETER_INTEGER: 469 | { 470 | int64_t value = parameter.as_int(); 471 | updateConfigParam(node_parameter, key_name_list, value); 472 | break; 473 | } 474 | case rclcpp::ParameterType::PARAMETER_DOUBLE: 475 | { 476 | double value = parameter.as_double(); 477 | updateConfigParam(node_parameter, key_name_list, value); 478 | break; 479 | } 480 | case rclcpp::ParameterType::PARAMETER_STRING: 481 | { 482 | std::string value = parameter.as_string(); 483 | updateConfigParam(node_parameter, key_name_list, value); 484 | break; 485 | } 486 | case rclcpp::ParameterType::PARAMETER_BYTE_ARRAY: 487 | { 488 | auto byte_array = parameter.as_byte_array(); 489 | if (byte_array.size() == 0) { 490 | RCLCPP_WARN(this->get_logger(), "parameter %s value is empty, it will not be stored", name.c_str()); 491 | break; 492 | } 493 | YAML::Node seq; 494 | seq.SetStyle(YAML::EmitterStyle::Flow); 495 | for (auto byte : byte_array) 496 | { 497 | // TODO. rcl_yaml_param_parser not support byte array, use int array temporary 498 | seq.push_back((int64_t)byte); 499 | } 500 | updateConfigParam(node_parameter, key_name_list, seq); 501 | break; 502 | } 503 | case rclcpp::ParameterType::PARAMETER_BOOL_ARRAY: 504 | { 505 | auto bool_array = parameter.as_bool_array(); 506 | if (bool_array.size() == 0) { 507 | RCLCPP_WARN(this->get_logger(), "parameter %s value is empty, it will not be stored", name.c_str()); 508 | break; 509 | } 510 | YAML::Node seq; 511 | seq.SetStyle(YAML::EmitterStyle::Flow); 512 | for (bool b : bool_array) // Vector is specialized for bool 513 | { 514 | seq.push_back(b); 515 | } 516 | 517 | updateConfigParam(node_parameter, key_name_list, seq); 518 | break; 519 | } 520 | case rclcpp::ParameterType::PARAMETER_INTEGER_ARRAY: 521 | { 522 | auto array = parameter.as_integer_array(); 523 | if (array.size() == 0) { 524 | RCLCPP_WARN(this->get_logger(), "parameter %s value is empty, it will not be stored", name.c_str()); 525 | break; 526 | } 527 | YAML::Node seq; 528 | seq.SetStyle(YAML::EmitterStyle::Flow); 529 | for (auto i : array) 530 | { 531 | seq.push_back(i); 532 | } 533 | updateConfigParam(node_parameter, key_name_list, seq); 534 | break; 535 | } 536 | case rclcpp::ParameterType::PARAMETER_DOUBLE_ARRAY: 537 | { 538 | auto array = parameter.as_double_array(); 539 | if (array.size() == 0) { 540 | RCLCPP_WARN(this->get_logger(), "parameter %s value is empty, it will not be stored", name.c_str()); 541 | break; 542 | } 543 | YAML::Node seq; 544 | seq.SetStyle(YAML::EmitterStyle::Flow); 545 | for (auto d : array) 546 | { 547 | seq.push_back(d); 548 | } 549 | updateConfigParam(node_parameter, key_name_list, seq); 550 | break; 551 | } 552 | case rclcpp::ParameterType::PARAMETER_STRING_ARRAY: 553 | { 554 | auto array = parameter.as_string_array(); 555 | if (array.size() == 0) { 556 | RCLCPP_WARN(this->get_logger(), "parameter %s value is empty, it will not be stored", name.c_str()); 557 | break; 558 | } 559 | YAML::Node seq; 560 | seq.SetStyle(YAML::EmitterStyle::Flow); 561 | for (auto& str : array) 562 | { 563 | seq.push_back(str); 564 | } 565 | updateConfigParam(node_parameter, key_name_list, seq); 566 | break; 567 | } 568 | default: 569 | { 570 | RCLCPP_WARN(this->get_logger(), "parameter %s unsupported type %d", 571 | name.c_str(), parameter.get_type()); 572 | break; 573 | } 574 | } 575 | } 576 | 577 | // rcl_yaml_param_parser not supported the following format, need to remove it 578 | // /: 579 | // parameter_server_bdk: 580 | // ros__parameters: 581 | // persistent: 582 | // {} 583 | if (node_parameter[PERSISTENT_KEY].size() == 0) { 584 | node_parameter.remove(PERSISTENT_KEY); 585 | } 586 | 587 | if (parameter_use_stars_) { 588 | parameter_config["/**"][ROS_PARAMETER_KEY] = node_parameter; 589 | } else if (parameter_ns_exist_ && parameter_name_exist_) { 590 | parameter_config[get_namespace()][node_name_][ROS_PARAMETER_KEY] = node_parameter; 591 | } else if (parameter_name_exist_) { 592 | parameter_config[node_name_][ROS_PARAMETER_KEY] = node_parameter; 593 | } else { 594 | parameter_config[get_namespace()][node_name_][ROS_PARAMETER_KEY] = node_parameter; 595 | } 596 | 597 | // data -> YAML::Node -> YAML::Emitter(save string with "'") 598 | // use emitter to traverse all sub nodes, if value of Node is string, add ' between value. 599 | YAML::Emitter out; 600 | SaveNode(out, parameter_config); 601 | std::ofstream fout(persistent_yaml_file_); 602 | fout << out.c_str(); 603 | fout.close(); 604 | } 605 | } 606 | 607 | bool ParameterServer::CheckPersistentParam(const std::vector & parameters) 608 | { 609 | RCLCPP_DEBUG(this->get_logger(), "%s", __PRETTY_FUNCTION__); 610 | bool flag = false; 611 | 612 | for (auto& parameter : parameters) { 613 | std::string parameter_name = parameter.get_name(); 614 | if (parameter_name.find(PERSISTENT_DOT_KEY) != 0) { 615 | continue; 616 | } 617 | 618 | rclcpp::ParameterType parameter_type = parameter.get_type(); 619 | if (rclcpp::ParameterType::PARAMETER_NOT_SET == parameter_type) { 620 | RCLCPP_DEBUG(this->get_logger(), "parameter %s not set", parameter_name.c_str()); 621 | changed_parameter_lists_.erase(parameter_name); 622 | flag = true; 623 | } else { 624 | RCLCPP_DEBUG(this->get_logger(), "parameter %s changed", parameter_name.c_str()); 625 | changed_parameter_lists_.insert(parameter_name); 626 | flag = true; 627 | } 628 | } 629 | 630 | return flag; 631 | } 632 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(ros2_persistent_parameter_server_test VERSION 1.0.1) 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(rmw REQUIRED) 31 | 32 | add_executable(client 33 | src/test.cpp 34 | src/persist_parameter_client.cpp 35 | ) 36 | 37 | target_link_libraries(client 38 | rclcpp::rclcpp 39 | rclcpp_components::component 40 | rcutils::rcutils 41 | ${std_msgs_TARGETS} 42 | ) 43 | 44 | target_include_directories(client 45 | PUBLIC 46 | $ 47 | ) 48 | 49 | install(TARGETS client DESTINATION lib/${PROJECT_NAME}) 50 | 51 | # Install launch files. 52 | install(DIRECTORY 53 | launch 54 | DESTINATION share/${PROJECT_NAME}/ 55 | ) 56 | 57 | ament_package() 58 | -------------------------------------------------------------------------------- /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 | 20 | using namespace std::chrono_literals; 21 | 22 | class PersistParametersClient : public rclcpp::Node 23 | { 24 | public: 25 | RCLCPP_SMART_PTR_DEFINITIONS(PersistParametersClient) 26 | 27 | RCLCPP_PUBLIC 28 | PersistParametersClient( 29 | const std::string & client_name, 30 | const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions(), 31 | const std::string & remote_node_name = "parameter_server" 32 | ); 33 | 34 | // Format the array value for easy output. 35 | template 36 | inline void format_array_output(std::ostringstream & ss, const std::vector & value_vec) 37 | { 38 | ss << "[ "; 39 | for(const auto & value : value_vec) { 40 | ss << value << " "; 41 | } 42 | ss << "]"; 43 | 44 | return; 45 | } 46 | 47 | // Make sure the client and the server are connected through the Service. 48 | bool wait_param_server_ready() 49 | { 50 | bool ret = false; 51 | 52 | if(rclcpp::ok()) { 53 | RCLCPP_INFO(this->get_logger(), "Waiting 5 seconds to wait for the parameter server to be ready..."); 54 | ret = sync_param_client_->wait_for_service(5s); 55 | } 56 | 57 | return ret; 58 | } 59 | 60 | /* 61 | * Read the parameter value specified by `param_name`. 62 | * @param param_name The name of parameter. 63 | * @param parameter The vector that holds the read result. 64 | * @return Operation as expected or not. 65 | */ 66 | bool read_parameter(const std::string & param_name, std::vector & parameter); 67 | 68 | /* 69 | * Change the value of `param_name` to `param_value`. 70 | * The principle is to update if param exists, otherwise insert. 71 | * 72 | * @param param_name The name of parameter. 73 | * @param parameter_value The parameter value that you want to set. 74 | * @return Operation as expected or not. 75 | */ 76 | template 77 | bool modify_parameter(const std::string & param_name, const ValueType & param_value) 78 | { 79 | bool ret = true; 80 | std::vector parameters; 81 | 82 | parameters.push_back(rclcpp::Parameter(param_name, rclcpp::ParameterValue(param_value))); 83 | auto set_param_result = sync_param_client_->set_parameters(parameters); 84 | for (auto & result : set_param_result) 85 | { 86 | if (!result.successful) 87 | { 88 | RCLCPP_INFO(this->get_logger(), "SET OPERATION : Failed to set parameter: %s", result.reason.c_str()); 89 | return false; 90 | } 91 | } 92 | RCLCPP_INFO(this->get_logger(), "SET OPERATION : Set parameter %s successfully.", param_name.c_str()); 93 | 94 | return ret; 95 | } 96 | 97 | private: 98 | std::unique_ptr sync_param_client_; 99 | }; 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /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 | import launch 20 | 21 | def generate_launch_description(): 22 | return LaunchDescription([ 23 | launch.actions.ExecuteProcess( 24 | cmd = ['ros2', 'run', 'parameter_server', 'server', '--file-path', '/tmp/test/parameter_server.yaml'], 25 | respawn=True 26 | ) 27 | ]) 28 | -------------------------------------------------------------------------------- /test/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ros2_persistent_parameter_server_test 5 | 1.0.1 6 | 7 | Client test demo for persistent parameter server 8 | 9 | dbt 10 | Apache License 2.0 11 | Tomoya Fujita> 12 | 13 | ament_cmake 14 | 15 | rclcpp 16 | rclcpp_components 17 | rcutils 18 | rmw 19 | rmw_implementation_cmake 20 | std_msgs 21 | 22 | launch_ros 23 | 24 | launch 25 | ament_lint_auto 26 | ament_lint_common 27 | 28 | 29 | ament_cmake 30 | 31 | 32 | -------------------------------------------------------------------------------- /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 | } 29 | 30 | bool PersistParametersClient::read_parameter(const std::string & param_name, std::vector & parameter) 31 | { 32 | bool ret = true; 33 | 34 | parameter = sync_param_client_->get_parameters({param_name}); 35 | for(auto & param : parameter) 36 | { 37 | switch (param.get_type()) 38 | { 39 | case rclcpp::ParameterType::PARAMETER_NOT_SET: 40 | { 41 | RCLCPP_INFO(this->get_logger(), "READ OPERATION : parameter %s was not set(or deleted), it will not be stored", param_name.c_str()); 42 | break; 43 | } 44 | case rclcpp::ParameterType::PARAMETER_BOOL: 45 | { 46 | bool value = param.as_bool(); 47 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), value?"true":"false"); 48 | break; 49 | } 50 | case rclcpp::ParameterType::PARAMETER_INTEGER: 51 | { 52 | int64_t value = param.as_int(); 53 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %ld", param_name.c_str(), value); 54 | break; 55 | } 56 | case rclcpp::ParameterType::PARAMETER_DOUBLE: 57 | { 58 | double value = param.as_double(); 59 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %lf", param_name.c_str(), value); 60 | break; 61 | } 62 | case rclcpp::ParameterType::PARAMETER_STRING: 63 | { 64 | std::string value = param.as_string(); 65 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), value.c_str()); 66 | break; 67 | } 68 | case rclcpp::ParameterType::PARAMETER_BYTE_ARRAY: 69 | { 70 | std::ostringstream ss; 71 | auto array = param.as_byte_array(); 72 | format_array_output(ss, array); 73 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 74 | break; 75 | } 76 | case rclcpp::ParameterType::PARAMETER_BOOL_ARRAY: 77 | { 78 | std::ostringstream ss; 79 | auto array = param.as_bool_array(); 80 | format_array_output(ss, array); 81 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 82 | break; 83 | } 84 | case rclcpp::ParameterType::PARAMETER_INTEGER_ARRAY: 85 | { 86 | std::ostringstream ss; 87 | auto array = param.as_integer_array(); 88 | format_array_output(ss, array); 89 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 90 | break; 91 | } 92 | case rclcpp::ParameterType::PARAMETER_DOUBLE_ARRAY: 93 | { 94 | std::ostringstream ss; 95 | auto array = param.as_double_array(); 96 | format_array_output(ss, array); 97 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 98 | break; 99 | } 100 | case rclcpp::ParameterType::PARAMETER_STRING_ARRAY: 101 | { 102 | std::ostringstream ss; 103 | auto array = param.as_string_array(); 104 | format_array_output(ss, array); 105 | RCLCPP_INFO(this->get_logger(), "GET OPERATION : parameter %s's value is %s", param_name.c_str(), ss.str().c_str()); 106 | break; 107 | } 108 | default: { 109 | ret = false; 110 | RCLCPP_INFO(this->get_logger(), "parameter %s unsupported type %d", param_name.c_str(), param.get_type()); 111 | break; 112 | } 113 | } 114 | } 115 | 116 | return ret; 117 | } 118 | -------------------------------------------------------------------------------- /test/src/test.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 | 18 | #include "persist_parameter_client.hpp" 19 | 20 | /** 21 | * NoServerError 22 | * 23 | * The client will wait 5 seconds for the server to be ready. 24 | * If timeout, then throw an exception to terminate the endless waiting. 25 | */ 26 | struct NoServerError : public std::runtime_error 27 | { 28 | public: 29 | NoServerError() 30 | : std::runtime_error("cannot connect to server"){} 31 | }; 32 | 33 | /* 34 | * SetOperationError 35 | * 36 | * When executing `set_parameter`, if the set operation failed, 37 | * throw an exception to ignore the subsequent test. 38 | */ 39 | struct SetOperationError : public std::runtime_error 40 | { 41 | public: 42 | SetOperationError() 43 | : std::runtime_error("set operation failed"){} 44 | }; 45 | 46 | class TestPersistParameter 47 | { 48 | public: 49 | TestPersistParameter( 50 | const std::string & node_name, 51 | const rclcpp::NodeOptions & options) 52 | : persist_param_client_(node_name, options) 53 | { 54 | if(!wait_param_server_ready()) { 55 | throw NoServerError(); 56 | } 57 | } 58 | 59 | inline bool wait_param_server_ready() 60 | { 61 | return persist_param_client_.wait_param_server_ready(); 62 | } 63 | 64 | /* 65 | * Read the value of parameter. 66 | * @param param_name The name of parameter. 67 | * @param expect_str The value of the parameter that you expected, take std::string as example here. 68 | * If expect_str is equal to `nullptr` pointer, that means the parameter expected to be not exist. 69 | * @param testcase The test case description. 70 | */ 71 | void do_read_and_check(const std::string & param_name, const char * expect_str, const std::string & testcase) 72 | { 73 | bool value = false; 74 | std::vector parameter; 75 | 76 | if(persist_param_client_.read_parameter(param_name, parameter)) { 77 | for(auto & param : parameter) { 78 | if(expect_str == nullptr) { 79 | if(param.get_type() == rclcpp::ParameterType::PARAMETER_NOT_SET) { 80 | value = true; 81 | break; 82 | } 83 | }else if(param.get_type() == rclcpp::ParameterType::PARAMETER_STRING && param.as_string() == expect_str) { 84 | value = true; 85 | break; 86 | } 87 | } 88 | } 89 | 90 | /* 91 | * Even if the Get operation failed, record it in result_map, and it shouldn't effect the 92 | * subsequent tests. 93 | */ 94 | this->set_result(testcase, value); 95 | } 96 | 97 | /* 98 | * Change the value of parameter. 99 | * @param param_name The name of parameter. 100 | * @param changed_value The value that you want to set. 101 | * @param testcase The test case description. 102 | */ 103 | void do_change_and_check(const std::string & param_name, const std::string & changed_value, const std::string & testcase) 104 | { 105 | bool ret = false; 106 | 107 | ret = persist_param_client_.modify_parameter(param_name, changed_value); 108 | /* 109 | * If the Modify operation failed, record it in result_map, and no need to run the 110 | * subsequent read tests. 111 | */ 112 | if(!ret) { 113 | this->set_result(testcase, false); 114 | throw SetOperationError(); 115 | } 116 | 117 | return do_read_and_check(param_name, changed_value.c_str(), testcase); 118 | } 119 | 120 | // Get all test results. 121 | inline int print_result() const 122 | { 123 | int ret = EXIT_SUCCESS; 124 | RCLCPP_INFO(this->get_logger(), "****************************************************" 125 | "***********************"); 126 | RCLCPP_INFO(this->get_logger(), "*********************************Test Result*********" 127 | "**********************"); 128 | for(const auto & res : result_map_) { 129 | RCLCPP_INFO(this->get_logger(), "%-60s : %16s", res.first.c_str(), res.second?"PASS":"NOT PASS"); 130 | 131 | // if any tests are not passed, return EXIT_FAILURE. 132 | if (res.second == false) { 133 | ret = EXIT_FAILURE; 134 | } 135 | } 136 | 137 | return ret; 138 | } 139 | 140 | static inline rclcpp::Logger get_logger() 141 | { 142 | return client_logger_; 143 | } 144 | 145 | private: 146 | // Save the result of each test operation. 147 | inline void set_result(const std::string & key, bool value) 148 | { 149 | auto pair = result_map_.insert({key, value}); 150 | if(!pair.second) { 151 | RCLCPP_INFO(this->get_logger(), "Failed when insert %s to result_map", key.c_str()); 152 | } 153 | 154 | return; 155 | } 156 | 157 | PersistParametersClient persist_param_client_; 158 | std::map result_map_; 159 | static rclcpp::Logger client_logger_; 160 | }; 161 | rclcpp::Logger TestPersistParameter::client_logger_ = rclcpp::get_logger("client"); 162 | 163 | int main(int argc, char ** argv) 164 | { 165 | // force flush of the stdout buffer. 166 | // this ensures a correct sync of all prints 167 | // even when executed simultaneously within the launch file. 168 | setvbuf(stdout, NULL, _IONBF, BUFSIZ); 169 | 170 | rclcpp::init(argc, argv); 171 | std::shared_ptr test_client; 172 | 173 | int ret_code = 0; 174 | // In case of an exception is thrown when performing an operation after `ctrl-c` occurred. 175 | try { 176 | test_client = std::make_shared("client", rclcpp::NodeOptions()); 177 | /* 178 | * First read parameter(include normal parameter and persistent parameter), to confirm the initial value of parameters. 179 | * Parameter server is launched with file `/tmp/parameter_server.yaml`, in this file, parameter `a_string` is defined 180 | * and the initial value is `Hello world`. 181 | * 182 | * Test: The default parameter `a_string` specified in YAML file which is loaded while parameter server is launched 183 | * should be read correctly. 184 | */ 185 | { 186 | // If return fail, no need to do the following. 187 | RCLCPP_INFO(test_client->get_logger(), "First read the initial value of parameter : "); 188 | test_client->do_read_and_check("a_string", "Hello world", "a. Read Normal Parameter"); 189 | test_client->do_read_and_check("persistent.a_string", "Hello world", "b. Read Persistent Parameter"); 190 | } 191 | 192 | /* 193 | * Test: Modifying the parameter `a_string`'s value to `Hello`, and add a new parameter `new_string` to YAML file. 194 | */ 195 | { 196 | RCLCPP_INFO(test_client->get_logger(), "Change the value of parameter to `Hello` : "); 197 | test_client->do_change_and_check("a_string", "Hello", "c. Modify Existed Normal parameter"); 198 | test_client->do_change_and_check("persistent.a_string", "Hello", "d. Modify Existed Persistent parameter"); 199 | RCLCPP_INFO(test_client->get_logger(), "Add a new parameter to parameter file : "); 200 | test_client->do_change_and_check("new_string", "Hello NewString", "e. Add New Normal parameter"); 201 | test_client->do_change_and_check("persistent.new_string", "Hello NewString", "f. Add New Persistent parameter"); 202 | } 203 | 204 | // Waiting for the server to restart. 205 | std::this_thread::sleep_for(std::chrono::seconds(5)); 206 | 207 | /* 208 | * Test : Reading parameter value again to confirm whether to store the modified persistent/normal parameter to the file. 209 | */ 210 | { 211 | if(!test_client->wait_param_server_ready()) { 212 | throw NoServerError(); 213 | } 214 | RCLCPP_INFO(test_client->get_logger(), "Last read the value of parameter after server restarts," 215 | "to check whether changes stores to the file : "); 216 | test_client->do_read_and_check("a_string", "Hello world", "g. Test Normal Parameter Not Stores To File"); 217 | test_client->do_read_and_check("persistent.a_string", "Hello", "h. Test Persistent Parameter Stores To File"); 218 | test_client->do_read_and_check("new_string", nullptr, "i. Test New Added Normal Parameter Not Stores To File"); 219 | test_client->do_read_and_check("persistent.new_string", "Hello NewString", "j. Test New Added Persistent Parameter Stores To File"); 220 | } 221 | } catch (const rclcpp::exceptions::RCLError & e) { 222 | ret_code = -1; 223 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 224 | } catch (const NoServerError & e) { 225 | ret_code = -2; 226 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 227 | } catch (const SetOperationError & e) { 228 | ret_code = -3; 229 | RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what()); 230 | } 231 | 232 | // if any tests are not passed, return EXIT_FAILURE. 233 | ret_code = test_client->print_result(); 234 | rclcpp::shutdown(); 235 | 236 | return ret_code; 237 | } 238 | -------------------------------------------------------------------------------- /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', 'ros2_persistent_parameter_server_test', 'test.launch.py'] 17 | launchClientCmd = ['ros2', 'run', 'ros2_persistent_parameter_server_test', 'client'] 18 | 19 | if shutil.which('ros2') is None: 20 | print("source /install/setup.bash...then retry.") 21 | sys.exit(1) 22 | 23 | def kill_server(): 24 | try: 25 | time.sleep(sleep_time) 26 | print("parameter server is about to be killed...") 27 | program_name = 'server' 28 | for process in psutil.process_iter(): 29 | if process.name() == program_name: 30 | path = psutil.Process(process.pid) 31 | if "install/parameter_server/lib/parameter_server" or "parameter_server/server" in path.exe(): 32 | os.kill(int(process.pid), signal.SIGINT) 33 | print("parameter server is killed successfully") 34 | break 35 | except: 36 | print("parameter server cannot be killed") 37 | return 38 | time.sleep(5) 39 | #print("Press CTRL-C to shutdown...") 40 | 41 | # Start Server process with re-spawn enabled, this process stays running 42 | server_process = subprocess.Popen(launchServerCmd, preexec_fn=os.setsid) 43 | print(f"Parameter Server Process started with PID: {server_process.pid}") 44 | 45 | # Start test client process 46 | client_process = subprocess.Popen(launchClientCmd) 47 | print(f"Parameter Client Process started with PID: {client_process.pid}") 48 | 49 | # Start killer thread to re-spawn the parameter server 50 | t = Thread(target = kill_server, args = ()) 51 | t.start() 52 | 53 | # Wait until the client process finishes 54 | return_code = client_process.wait() 55 | 56 | # Cleanup the process and thread 57 | t.join() 58 | os.killpg(os.getpgid(server_process.pid), signal.SIGTERM) 59 | 60 | print("\nTest process finished.") 61 | print(f"Return Code: {return_code}") 62 | 63 | # Check if the client process completed successfully 64 | if return_code == 0: 65 | print("The process completed successfully.") 66 | sys.exit(0) 67 | else: 68 | print("The process failed.") 69 | sys.exit(1) 70 | --------------------------------------------------------------------------------