├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 0-troubleshooting-redirect.yml │ ├── 1-bug.yml │ ├── 2-feature.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.rst ├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── boot └── firmware │ ├── cmdline.txt │ └── config.txt ├── debian ├── install └── postinst ├── etc ├── motd ├── netplan │ ├── 40-ethernets.yaml │ └── 50-wifis.yaml ├── rc.local ├── systemd │ └── system │ │ └── webserver.service └── turtlebot4 │ ├── aliases.bash │ ├── chrony.conf │ ├── cyclonedds_rpi.xml │ ├── discovery.conf │ ├── discovery.sh │ ├── fastdds_discovery_create3.xml │ ├── fastdds_rpi.xml │ ├── firmware │ ├── cmdline.txt │ └── config.txt │ ├── setup.bash │ └── system ├── install_config └── logind │ └── 70-clearpath-standard-logind.conf ├── package.xml ├── scripts ├── create_update.sh ├── jazzy.sh ├── sd_flash.sh ├── swap_off.sh ├── swap_on.sh └── turtlebot4_setup.sh ├── turtlebot4_discovery └── configure_discovery.sh ├── turtlebot4_setup ├── __init__.py ├── conf.py ├── menu.py ├── ros_setup.py ├── turtlebot4_setup └── wifi.py └── udev ├── 50-turtlebot4.rules ├── 60-logitech.rules ├── 80-movidius.rules └── 99-gpio.rules /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default all changes will request review from: 2 | * @roni-kreinin -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/0-troubleshooting-redirect.yml: -------------------------------------------------------------------------------- 1 | name: Troubleshooting Help 2 | description: "If you need troubleshooting help please use the troubleshooting form from the Turtlebot4 repo: https://github.com/turtlebot/turtlebot4/issues/new/choose." 3 | labels: ["troubleshooting"] 4 | assignees: 5 | - smatarCPR 6 | - RustyCPR 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: "If you need troubleshooting help please use the troubleshooting form here: https://github.com/turtlebot/turtlebot4/issues/new/choose." 11 | - type: dropdown 12 | id: value 13 | attributes: 14 | label: Understand? 15 | options: 16 | - 'Yes' 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Use this form when you are confident that there is a bug in this particular package. If you are not sure then use the Troubleshooting Form. 3 | labels: ["bug"] 4 | assignees: 5 | - roni-kreinin 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: __Only use this form if you are confident that there is a bug in this package and that it is not user error. If you are not sure then please use the troubleshooting form.__ 10 | - type: markdown 11 | attributes: 12 | value: "# System Information" 13 | - type: dropdown 14 | id: model 15 | attributes: 16 | label: Robot Model 17 | description: Standard has a screen, Lite does not. For simulation select the one that you are simulating. 18 | options: 19 | - Select One 20 | - Turtlebot4 Standard 21 | - Turtlebot4 Lite 22 | validations: 23 | required: true 24 | - type: dropdown 25 | id: ros-distro 26 | attributes: 27 | label: ROS distro 28 | description: What ROS distribution are you using (must match on all devices in the system)? 29 | options: 30 | - Select One 31 | - Galactic 32 | - Humble 33 | - Jazzy 34 | validations: 35 | required: true 36 | - type: dropdown 37 | id: networking 38 | attributes: 39 | label: Networking Configuration 40 | options: 41 | - Select One 42 | - Simple Discovery 43 | - Discovery Server 44 | - I do not know 45 | validations: 46 | required: true 47 | - type: dropdown 48 | id: os 49 | attributes: 50 | label: OS 51 | description: What OS are you running on your companion PC (used to interact with the Turtlebot4)? 52 | options: 53 | - Select One 54 | - Ubuntu 20.04 55 | - Ubuntu 22.04 56 | - Ubuntu 24.04 57 | - Other Linux 58 | - Windows / MAC 59 | validations: 60 | required: true 61 | - type: dropdown 62 | id: build-type 63 | attributes: 64 | label: Built from source or installed? 65 | description: Did you build from source (build the packages yourself) or did you install the packages (e.g. `sudo apt install ...`)? 66 | options: 67 | - Select One 68 | - Built from Source 69 | - Installed 70 | validations: 71 | required: true 72 | - type: textarea 73 | id: version 74 | attributes: 75 | label: Package version 76 | description: What version of the package are you running? (if installed run `dpkg -s ros-$ROS_DISTRO-turtlebot4-PACKAGE_WITH_ISSUE`, if from source, give commit hash) 77 | validations: 78 | required: true 79 | 80 | - type: markdown 81 | attributes: 82 | value: "# Problem Description" 83 | - type: textarea 84 | attributes: 85 | label: Expected behaviour 86 | description: A clear and concise description of what you expected to happen. 87 | validations: 88 | required: true 89 | - type: textarea 90 | attributes: 91 | label: Actual behaviour 92 | description: A clear and concise description of what you encountered. 93 | validations: 94 | required: true 95 | - type: textarea 96 | attributes: 97 | label: Error messages 98 | description: Error messages copied from terminal and/or relevant logs. Copy these directly from the terminal in full. 99 | render: bash 100 | - type: textarea 101 | attributes: 102 | label: To Reproduce 103 | description: Provide the steps to reproduce. 104 | placeholder: | 105 | 1. run something 106 | 2. launch something else 107 | 3. see the error 108 | validations: 109 | required: true 110 | - type: textarea 111 | attributes: 112 | label: Other notes 113 | description: Add anything else you thing is important. 114 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Use this form for requesting a feature that current doesn't exist. 3 | labels: ["enhancement"] 4 | assignees: 5 | - smatarCPR 6 | - RustyCPR 7 | body: 8 | - type: textarea 9 | attributes: 10 | label: Describe the the feature you would like 11 | description: A clear and concise description of what you want to happen. 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Motivation and impact 17 | description: Why is this an important feature and who will it impact? 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Other notes 23 | description: Add anything else you thing is important. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Also include relevant motivation and context. 4 | 5 | Fixes # (issue). 6 | 7 | ## Type of change 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | 12 | ## How Has This Been Tested? 13 | 14 | Please describe the tests that you ran to verify your changes. 15 | Provide instructions so we can reproduce. Also list any relevant details for your test configuration. 16 | 17 | ```bash 18 | # Run this command 19 | ros2 launch package launch.py 20 | ``` 21 | 22 | ## Checklist 23 | 24 | - [ ] I have performed a self-review of my own code 25 | - [ ] I have commented my code, particularly in hard-to-understand areas 26 | - [ ] I have made corresponding changes to the documentation -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: turtlebot4_setup_ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | turtlebot4_jazzy_ci: 7 | name: Jazzy 8 | runs-on: ubuntu-24.04 9 | steps: 10 | - uses: actions/checkout@v2.3.4 11 | - uses: ros-tooling/setup-ros@v0.7 12 | with: 13 | required-ros-distributions: jazzy 14 | use-ros2-testing: true 15 | - uses: ros-tooling/action-ros-ci@v0.3 16 | id: action_ros_ci_step 17 | with: 18 | target-ros2-distro: jazzy 19 | import-token: ${{ secrets.GITHUB_TOKEN }} 20 | skip-tests: false 21 | package-name: 22 | turtlebot4_setup 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package turtlebot4_setup 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.0.3 (2025-03-04) 6 | ------------------ 7 | * Update the hostname in `/boot/firmware/user-data` (`#19 `_) 8 | * Update the hostname in /boot/firmware/user-data as well as /etc/hostname to ensure it persists properly across reboots 9 | * Suppress error output about failure to preserve permissions across filesystems 10 | * Write the updated hostname to the turtlebot4/system file 11 | * Fix/shm (`#21 `_) 12 | Turn off clearing of SHM on log out (interfered with services) 13 | * Replace `User=ubuntu` with `User={os.getlogin()}` when generating the discovery server configuration file if necessary (`#20 `_) 14 | * Fix tag order 15 | * Update package maintainers 16 | * Add an alias to reconnect to a previously-paired game controller 17 | * Add a udev rule for the Logitech F710 family of controllers 18 | * Contributors: Chris Iverach-Brereton, Hilary Luo 19 | 20 | 2.0.2 (2024-10-23) 21 | ------------------ 22 | * Add e2fsck to SD-flasher script 23 | * Bump default version numbers 24 | * Fix default hostname to match released SD card images 25 | * Append `p2` for `mmcblk` devices, but just `2` for `sd*` devices when expanding the last partition 26 | * Contributors: Chris Iverach-Brereton 27 | 28 | 2.0.1 (2024-10-04) 29 | ------------------ 30 | * Add a copy of the boot/firmware files to /etc/turtlebot for reference in case users modify these and want a clean, offline copy for reference 31 | * Add ROBOT_SETUP to setup.bash 32 | * Add growpart & resize2fs commands to the SD card-flashing script to expand the partition to use up the whole SD card 33 | * Add socat as a package dependency instead of an ad-hoc post-install package 34 | * Add MOTD file with the Turtlebot4 logotype 35 | * Contributors: Chris Iverach-Brereton 36 | 37 | 2.0.0 (2024-09-28) 38 | ------------------ 39 | * Initial Jazzy implementation 40 | * Add a note about firmware compatibility to the readme 41 | * Add exception handling to the file i/o so the node doesn't just crash if we're missing a file 42 | * Add improved exception handling to the wifi settings parser 43 | * Update CI 44 | * Properly escape all `\` characters in stylized titles, add translation & link to generator page in comments 45 | * Add copyright & contribution notices, fix up code formatting, import ordering. Disable linting for some specific lines where appropriate 46 | * Add XML namespaces & version to cyclone DDS config 47 | * Omit XML linting (for now); it's consistently timing out and failing 48 | * Code formatting fixes 49 | * Add exception handling to the file preview 50 | * Add an option to force the Create3 settings to be reapplied, even if we haven't changed anything else. Always apply the _do_not_use namespace, as we're universally using the republisher now 51 | * Remove superfluous concatenation 52 | * Enable testing packages for CI 53 | * Disable checks on two lines with long format strings 54 | * Add exception handling for install & uninstall 55 | * Add an error prompt to show errors during installation 56 | * Handle KeyErrors separately 57 | * Add newline to end of file 58 | * Fix indentation 59 | * `''.format` -> `f''` 60 | * Update the default system file, print the keys instead of the enums 61 | * Add a `__str_\_` function to the relevant classes 62 | * Disable DHCP4 on the built-in ethernet interface, make it non-optional 63 | * Change the post-install chrony file command from mv to cp 64 | * Only copy if the file exists 65 | * Initial Jazzy implementation (`#15 `_) 66 | * Contributors: Chris Iverach-Brereton 67 | 68 | 1.0.4 (2024-07-02) 69 | ------------------ 70 | * Multi-robot discovery server support (`#11 `) 71 | * Add discovery server ID 72 | * Switch from xml super client to envar 73 | * Don't look for an ntp server on create3 74 | * Adjust create3 discovery server envar for server_id 75 | * Get feedback from the curl command to abort the apply if the create3 is not accessible 76 | * Push ntp config to create3, pointing it at the pi 77 | * Write discovery.sh fresh each time for robustness 78 | * Insert missing exports when writing setup.bash 79 | * Update script for server ID 80 | * Enforce a local server in discovery server for the create3 and support an offboard server for pi only 81 | * Give the create3 a hidden namespace to prepare for republishing 82 | * Put environment variables in quotes to handle multiple discovery servers 83 | * Make Super Client only apply to user terminals 84 | * Fix error when setting Offboard Discovery Server IP to blank 85 | * Remove IP Routing from script to set up discovery server on the user computer, no longer needed due to the republisher, includes file/service cleanup 86 | * Force compares as string to handle boolean settings correctly 87 | * Ensure that usb0 and wlan0 networks are up before either turtlebot4 service is started and use only NetworkManager to speed up boot 88 | * ipv4 forwarding is no longer required 89 | * Add create3 rmw profile for discovery server 90 | * git clone no longer necessary 91 | * Update discovery server user pc config script to accept any number of discovery servers 92 | * Added missing exec dependencies 93 | * Ensure that the chrony file always gets overwritten 94 | * Update username for github issue asignment (`#10 `) 95 | * Updated issue templates to forms and redirected troubleshooting to turtlebot4 repo (`#9 `) 96 | * Contributors: Hilary Luo 97 | 98 | 1.0.3 (2023-11-08) 99 | ------------------ 100 | * Cleanup 101 | * Remove scripts that should not be used in Humble 102 | * Update create_update.sh to reference Humble minimum version 103 | * Updated README 104 | * Updated turtlebot4_setup.sh script 105 | * Fixed setting robot model 106 | * Contributors: Hilary Luo, Roni Kreinin 107 | 108 | 1.0.2 (2023-03-01) 109 | ------------------ 110 | * Fixed Discovery Server IP 111 | * Updated default configs 112 | * Contributors: Roni Kreinin 113 | 114 | 1.0.1 (2023-02-28) 115 | ------------------ 116 | * Fixed script install path 117 | * Contributors: Roni Kreinin 118 | 119 | 1.0.0 (2023-02-24) 120 | ------------------ 121 | * turtlebot4_setup tool 122 | * RPI config updates 123 | * Discovery server files 124 | * Contributors: Roni Kreinin 125 | 126 | 0.1.3 (2022-09-27) 127 | ------------------ 128 | * Merge pull request `#2 `_ from turtlebot/roni-kreinin/domain_id 129 | v0.1.3 130 | * Added webserver service 131 | * Added argparser to install.py 132 | Removed namespacing for now 133 | * Added 'ros_config' script for setting ROS_DOMAIN_ID, namespace, and RMW_IMPLEMENTATION 134 | * Contributors: Roni Kreinin, roni-kreinin 135 | 136 | 0.1.2 (2022-06-14) 137 | ------------------ 138 | * Added chrony 139 | Updated wifi script 140 | * Updated dependencies 141 | Move swap_on and swap_off to /usr/local/bin 142 | * Fixed comment 143 | * Updated Create 3 curl commands 144 | Move wifi and create update scripts to /usr/local/bin 145 | * Updated oakd branch 146 | * Update README.md 147 | * Moved contents to root folder 148 | Updated oakd script to work for both pro and lite 149 | Updated turtlebot4_setup script 150 | Updated wifi script to allow the create 3 to be set up through the pi 151 | Added create 3 firmware flash scripts 152 | * Update README.md 153 | * Updated robot_upstart repo 154 | * Added swap memory scripts when more RAM is needed to build packages 155 | * Updated README 156 | * Initial commit 157 | * Contributors: Roni Kreinin, roni-kreinin 158 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | project(turtlebot4_setup) 3 | 4 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 5 | add_compile_options(-Wall -Wextra -Wpedantic) 6 | endif() 7 | 8 | # find dependencies 9 | find_package(ament_cmake REQUIRED) 10 | find_package(ament_cmake_python REQUIRED) 11 | 12 | ament_python_install_package(${PROJECT_NAME}) 13 | 14 | install( 15 | PROGRAMS ${PROJECT_NAME}/turtlebot4_setup 16 | DESTINATION lib/${PROJECT_NAME} 17 | ) 18 | 19 | # disable XML linting; it consistently times out 20 | # TODO (civerachb-cpr) -- figure out why it's timing out and re-enable 21 | # hypothesis: it's related to the additional XML files in etc/turtlebot4 22 | list(APPEND AMENT_LINT_AUTO_EXCLUDE 23 | ament_cmake_xmllint 24 | ) 25 | 26 | if(BUILD_TESTING) 27 | find_package(ament_lint_auto REQUIRED) 28 | ament_lint_auto_find_test_dependencies() 29 | endif() 30 | 31 | ament_package() 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to TurtleBot4 Setup 2 | 3 | Any contribution that you make to this repository will 4 | be under the Apache 2 License, as dictated by that 5 | [license](http://www.apache.org/licenses/LICENSE-2.0.html): 6 | 7 | ~~~ 8 | 5. Submission of Contributions. Unless You explicitly state otherwise, 9 | any Contribution intentionally submitted for inclusion in the Work 10 | by You to the Licensor shall be under the terms and conditions of 11 | this License, without any additional terms or conditions. 12 | Notwithstanding the above, nothing herein shall supersede or modify 13 | the terms of any separate license agreement you may have executed 14 | with Licensor regarding such Contributions. 15 | ~~~ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turtlebot4 Setup 2 | 3 | Setup scripts and tools for the TurtleBot 4 Raspberry Pi. 4 | 5 | Visit the [TurtleBot 4 User Manual](https://turtlebot.github.io/turtlebot4-user-manual/software/turtlebot4_setup.html) for more details. 6 | 7 | Make sure your Create® 3 is updated to the `I.*.*` firmware; older versions of the firmware are not compatible with ROS 2 Jazzy. 8 | 9 | # Create an image manually 10 | 11 | Follow these instructions if you wish to create a Turtlebot4 image manually. 12 | 13 | ## Create an Ubuntu Image 14 | 15 | First install the [Raspberry Pi Imager](https://www.raspberrypi.com/software/). 16 | 17 | - Insert your SD card into your PC and run the Raspberry Pi Imager. Follow the instructions and install Ubuntu 24.04 Server (64-bit) onto the SD card. 18 | - Ensure your Raspberry Pi 4 is not powered before inserting the flashed SD card. 19 | - You can set up the Raspberry Pi by either connecting it to your network via Ethernet or by using a keyboard and HDMI monitor via a micro HDMI cable. 20 | 21 | ### Ethernet Setup 22 | 23 | - Connect the Raspberry Pi to your Network with an Ethernet cable. 24 | - Boot the Raspberry Pi. 25 | - Find the Raspberry Pi's IP using your router's portal. 26 | - SSH into the Raspberry Pi using the IP address. 27 | ```bash 28 | ssh ubuntu@xxx.xxx.xxx.xxx 29 | ``` 30 | - The default login is `ubuntu` and password is `ubuntu`. You will be prompted to change your password. 31 | 32 | ### HDMI Setup 33 | 34 | - Connect a keyboard to the Raspberry Pi via USB. 35 | - Connect a monitor to the Raspberry Pi via the HDMI0 port. 36 | - Boot the Raspberry Pi. 37 | - The default login is `ubuntu` and password is `ubuntu`. You will be prompted to change your password. 38 | 39 | ## Manually configure Wi-Fi 40 | 41 | Once you are logged into the Raspberry Pi, configure the Wi-Fi: 42 | 43 | ```bash 44 | sudo nano /etc/netplan/50-cloud-init.yaml 45 | ``` 46 | Add the following lines: 47 | ```bash 48 | wifis: 49 | wlan0: 50 | optional: true 51 | access-points: 52 | "YOUR_WIFI_SSID": 53 | password: "YOUR_WIFI_PASSWORD" 54 | dhcp4: true 55 | ``` 56 | Note: Ensure that `wifis:` is aligned with the existing `ethernets:` line. All indentations should be 4 spaces. Do not use tabs. 57 | - Reboot the Raspberry Pi. It should now be connected to your Wi-Fi. 58 | - Find the Raspberry Pi's IP using your router's portal. 59 | - SSH into the Raspberry Pi using the IP address. 60 | ```bash 61 | ssh ubuntu@xxx.xxx.xxx.xxx 62 | ``` 63 | 64 | ## Download and run the setup script 65 | 66 | ``` 67 | wget -qO - https://raw.githubusercontent.com/turtlebot/turtlebot4_setup/jazzy/scripts/turtlebot4_setup.sh | bash 68 | ``` 69 | 70 | The script will automatically install ROS 2 Jazzy, TurtleBot 4 packages, and other important apt packages. It will also configure the RPi4 to work in a TurtleBot 4. Once complete, the RPi4 should be rebooted with `sudo reboot`. Then, run `turtlebot4-setup` to configure the robot with the setup tool. 71 | -------------------------------------------------------------------------------- /boot/firmware/cmdline.txt: -------------------------------------------------------------------------------- 1 | console=serial0,115200 dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 rootwait fixrtc quiet splash modules-load=dwc2,g_ether 2 | -------------------------------------------------------------------------------- /boot/firmware/config.txt: -------------------------------------------------------------------------------- 1 | [all] 2 | kernel=vmlinuz 3 | cmdline=cmdline.txt 4 | initramfs initrd.img followkernel 5 | 6 | [pi4] 7 | max_framebuffers=2 8 | arm_boost=1 9 | 10 | [all] 11 | # Enable the audio output, I2C and SPI interfaces on the GPIO header. As these 12 | # parameters related to the base device-tree they must appear *before* any 13 | # other dtoverlay= specification 14 | dtparam=audio=on 15 | dtparam=i2c_arm=on 16 | dtparam=spi=on 17 | 18 | # Comment out the following line if the edges of the desktop appear outside 19 | # the edges of your display 20 | disable_overscan=1 21 | 22 | # If you have issues with audio, you may try uncommenting the following line 23 | # which forces the HDMI output into HDMI mode instead of DVI (which doesn't 24 | # support audio output) 25 | #hdmi_drive=2 26 | 27 | # Enable the serial pins 28 | enable_uart=1 29 | 30 | # Autoload overlays for any recognized cameras or displays that are attached 31 | # to the CSI/DSI ports. Please note this is for libcamera support, *not* for 32 | # the legacy camera stack 33 | camera_auto_detect=1 34 | display_auto_detect=1 35 | 36 | # Config settings specific to arm64 37 | arm_64bit=1 38 | dtoverlay=dwc2,dr_mode=peripheral 39 | dtoverlay=i2c-gpio,bus=3,i2c_gpio_delay_us=1,i2c_gpio_sda=4,i2c_gpio_scl=5 40 | 41 | [cm4] 42 | # Enable the USB2 outputs on the IO board (assuming your CM4 is plugged into 43 | # such a board) 44 | dtoverlay=dwc2,dr_mode=host 45 | 46 | [all] -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | scripts/* /usr/bin/ 2 | udev/*.rules /etc/udev/rules.d/ 3 | etc/* /etc/ 4 | install_config/logind/70-clearpath-standard-logind.conf /usr/lib/systemd/logind.conf.d/ 5 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | PKG="turtlebot4_setup" 4 | 5 | if [ -f /etc/turtlebot4/chrony.conf ]; then 6 | sudo cp /etc/turtlebot4/chrony.conf /etc/chrony/chrony.conf 7 | fi 8 | sudo service chrony restart 9 | sudo systemctl enable webserver.service 10 | sudo systemctl disable systemd-networkd-wait-online.service 11 | 12 | # Remove old/new dpkg configs 13 | sudo rm -f /etc/turtlebot4/*.dpkg-new 14 | sudo rm -f /etc/turtlebot4/*.dpkg-old 15 | sudo rm -f /etc/netplan/*.dpkg-new 16 | sudo rm -f /etc/netplan/*.dpkg-old 17 | sudo rm -f /etc/chrony/*.dpkg-new 18 | sudo rm -f /etc/chrony/*.dpkg-old 19 | 20 | #DEBHELPER# -------------------------------------------------------------------------------- /etc/motd: -------------------------------------------------------------------------------- 1 | _____ _ _ _ _ _ _ 2 | |_ _| _ _ _| |_| |___| |__ ___| |_| | | 3 | | || || | '_| _| / -_) '_ \/ _ \ _|_ _| 4 | |_| \_,_|_| \__|_\___|_.__/\___/\__| |_| 5 | -------------------------------------------------------------------------------- /etc/netplan/40-ethernets.yaml: -------------------------------------------------------------------------------- 1 | network: 2 | version: 2 3 | ethernets: 4 | renderer: NetworkManager 5 | eth0: 6 | addresses: 7 | - 192.168.185.3/24 8 | dhcp4: false 9 | optional: false 10 | usb0: 11 | addresses: 12 | - 192.168.186.3/24 13 | dhcp4: false 14 | -------------------------------------------------------------------------------- /etc/netplan/50-wifis.yaml: -------------------------------------------------------------------------------- 1 | network: 2 | version: 2 3 | wifis: 4 | renderer: NetworkManager 5 | wlan0: 6 | access-points: 7 | Turtlebot4: 8 | band: 5GHz 9 | mode: ap 10 | password: Turtlebot4 11 | dhcp4: true 12 | -------------------------------------------------------------------------------- /etc/rc.local: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Give ourselves some swap to deal with RAM issues 4 | if [ -f /swapfile ]; then 5 | swapon /swapfile 6 | fi 7 | -------------------------------------------------------------------------------- /etc/systemd/system/webserver.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Create 3 Webserver forwarding service 3 | After=network.target 4 | StartLimitIntervalSec=0 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | RestartSec=1 9 | ExecStart=/usr/bin/socat TCP-LISTEN:8080,fork,reuseaddr tcp:192.168.186.2:80 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /etc/turtlebot4/aliases.bash: -------------------------------------------------------------------------------- 1 | # Restart ROS2 daemon 2 | alias turtlebot4-daemon-restart='ros2 daemon stop; ros2 daemon start' 3 | 4 | # Help command 5 | alias turtlebot4-help='echo -e "\ 6 | TurtleBot 4 User Manual: https://turtlebot.github.io/turtlebot4-user-manual \n\ 7 | TurtleBot 4 Github: https://github.com/turtlebot/turtlebot4"' 8 | 9 | # Restart ntpd on Create 3 10 | alias turtlebot4-ntpd-sync='curl -X POST http://192.168.186.2/api/restart-ntpd' 11 | 12 | # Restart turtlebot4 service 13 | alias turtlebot4-service-restart='sudo systemctl restart turtlebot4.service' 14 | 15 | # Run turtlebot4_setup 16 | alias turtlebot4-setup='ros2 run turtlebot4_setup turtlebot4_setup' 17 | 18 | # Source ROBOT_SETUP 19 | alias turtlebot4-source='source $ROBOT_SETUP' 20 | 21 | # Update all packages 22 | alias turtlebot4-update='sudo apt update && sudo apt upgrade' 23 | 24 | # Re-connect a previously-paired game controller 25 | alias turtlebot4-connect-controller='bluetoothctl connect $(bt-device -l|grep "Wireless Controller" | grep -o "..:..:..:..:..:..")' 26 | -------------------------------------------------------------------------------- /etc/turtlebot4/chrony.conf: -------------------------------------------------------------------------------- 1 | # Welcome to the chrony configuration file. See chrony.conf(5) for more 2 | # information about usuable directives. 3 | 4 | # This will use (up to): 5 | # - 4 sources from ntp.ubuntu.com which some are ipv6 enabled 6 | # - 2 sources from 2.ubuntu.pool.ntp.org which is ipv6 enabled as well 7 | # - 1 source from [01].ubuntu.pool.ntp.org each (ipv4 only atm) 8 | # This means by default, up to 6 dual-stack and up to 2 additional IPv4-only 9 | # sources will be used. 10 | # At the same time it retains some protection against one of the entries being 11 | # down (compare to just using one of the lines). See (LP: #1754358) for the 12 | # discussion. 13 | # 14 | # About using servers from the NTP Pool Project in general see (LP: #104525). 15 | # Approved by Ubuntu Technical Board on 2011-02-08. 16 | # See http://www.pool.ntp.org/join.html for more information. 17 | pool ntp.ubuntu.com iburst maxsources 4 18 | pool 0.ubuntu.pool.ntp.org iburst maxsources 1 19 | pool 1.ubuntu.pool.ntp.org iburst maxsources 1 20 | pool 2.ubuntu.pool.ntp.org iburst maxsources 2 21 | 22 | # Enable serving time to ntp clients on 192.168.186.0 subnet. 23 | allow 192.168.186.0/24 24 | 25 | # Allow local sync 26 | local stratum 10 27 | 28 | # This directive specify the location of the file containing ID/key pairs for 29 | # NTP authentication. 30 | keyfile /etc/chrony/chrony.keys 31 | 32 | # This directive specify the file into which chronyd will store the rate 33 | # information. 34 | driftfile /var/lib/chrony/chrony.drift 35 | 36 | # Uncomment the following line to turn logging on. 37 | #log tracking measurements statistics 38 | 39 | # Log files location. 40 | logdir /var/log/chrony 41 | 42 | # Stop bad estimates upsetting machine clock. 43 | maxupdateskew 100.0 44 | 45 | # This directive enables kernel synchronisation (every 11 minutes) of the 46 | # real-time clock. Note that it can’t be used along with the 'rtcfile' directive. 47 | rtcsync 48 | 49 | # Step the system clock instead of slewing it if the adjustment is larger than 50 | # one second, but only in the first three clock updates. 51 | makestep 1 3 52 | -------------------------------------------------------------------------------- /etc/turtlebot4/cyclonedds_rpi.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | true 13 | 14 | 15 | -------------------------------------------------------------------------------- /etc/turtlebot4/discovery.conf: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=FastDDS discovery server 3 | After=network-online.target 4 | 5 | [Service] 6 | User=ubuntu 7 | Type=simple 8 | Restart=on-failure 9 | RestartSec=1 10 | ExecStart=/bin/bash -e /usr/sbin/discovery 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /etc/turtlebot4/discovery.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source /opt/ros/jazzy/setup.bash 3 | fastdds discovery -i 0 -p 11811 4 | -------------------------------------------------------------------------------- /etc/turtlebot4/fastdds_discovery_create3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | udp_transport 8 | UDPv4 9 | 10 | 32768 11 | 32768 12 | 13 | 8192 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | udp_transport 23 | 24 | 25 | false 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 0 35 | 0 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 0 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /etc/turtlebot4/fastdds_rpi.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /etc/turtlebot4/firmware/cmdline.txt: -------------------------------------------------------------------------------- 1 | console=serial0,115200 dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 rootwait fixrtc quiet splash modules-load=dwc2,g_ether 2 | -------------------------------------------------------------------------------- /etc/turtlebot4/firmware/config.txt: -------------------------------------------------------------------------------- 1 | [all] 2 | kernel=vmlinuz 3 | cmdline=cmdline.txt 4 | initramfs initrd.img followkernel 5 | 6 | [pi4] 7 | max_framebuffers=2 8 | arm_boost=1 9 | 10 | [all] 11 | # Enable the audio output, I2C and SPI interfaces on the GPIO header. As these 12 | # parameters related to the base device-tree they must appear *before* any 13 | # other dtoverlay= specification 14 | dtparam=audio=on 15 | dtparam=i2c_arm=on 16 | dtparam=spi=on 17 | 18 | # Comment out the following line if the edges of the desktop appear outside 19 | # the edges of your display 20 | disable_overscan=1 21 | 22 | # If you have issues with audio, you may try uncommenting the following line 23 | # which forces the HDMI output into HDMI mode instead of DVI (which doesn't 24 | # support audio output) 25 | #hdmi_drive=2 26 | 27 | # Enable the serial pins 28 | enable_uart=1 29 | 30 | # Autoload overlays for any recognized cameras or displays that are attached 31 | # to the CSI/DSI ports. Please note this is for libcamera support, *not* for 32 | # the legacy camera stack 33 | camera_auto_detect=1 34 | display_auto_detect=1 35 | 36 | # Config settings specific to arm64 37 | arm_64bit=1 38 | dtoverlay=dwc2,dr_mode=peripheral 39 | dtoverlay=i2c-gpio,bus=3,i2c_gpio_delay_us=1,i2c_gpio_sda=4,i2c_gpio_scl=5 40 | 41 | [cm4] 42 | # Enable the USB2 outputs on the IO board (assuming your CM4 is plugged into 43 | # such a board) 44 | dtoverlay=dwc2,dr_mode=host 45 | 46 | [all] -------------------------------------------------------------------------------- /etc/turtlebot4/setup.bash: -------------------------------------------------------------------------------- 1 | export CYCLONEDDS_URI=/etc/turtlebot4/cyclonedds_rpi.xml 2 | export FASTRTPS_DEFAULT_PROFILES_FILE=/etc/turtlebot4/fastdds_rpi.xml 3 | export ROBOT_NAMESPACE= 4 | export ROS_DOMAIN_ID=0 5 | export ROS_DISCOVERY_SERVER= 6 | export RMW_IMPLEMENTATION=rmw_fastrtps_cpp 7 | export TURTLEBOT4_DIAGNOSTICS=1 8 | export WORKSPACE_SETUP=/opt/ros/jazzy/setup.bash 9 | export ROS_SUPER_CLIENT=False 10 | export ROBOT_SETUP=/etc/turtlebot4/setup.bash 11 | 12 | source $WORKSPACE_SETUP 13 | -------------------------------------------------------------------------------- /etc/turtlebot4/system: -------------------------------------------------------------------------------- 1 | MODEL:lite 2 | VERSION:2.0.2 3 | ROS:Jazzy 4 | HOSTNAME:turtlebot4 -------------------------------------------------------------------------------- /install_config/logind/70-clearpath-standard-logind.conf: -------------------------------------------------------------------------------- 1 | # Disable the clearing of SHM links when all user sessions are ended so that running 2 | # ROS 2 services are not impacted when SSH sessions are closed. 3 | [Login] 4 | RemoveIPC=no 5 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | turtlebot4_setup 5 | 2.0.3 6 | Turtlebot4 setup scripts 7 | Chris Iverach-Brereton 8 | Hilary Luo 9 | 10 | Apache 2.0 11 | 12 | Roni Kreinin 13 | 14 | ament_cmake 15 | 16 | chrony 17 | curl 18 | network-manager 19 | rmw_cyclonedds_cpp 20 | rmw_fastrtps_cpp 21 | robot_upstart 22 | simple_term_menu_vendor 23 | socat 24 | 25 | ament_lint_auto 26 | ament_lint_common 27 | 28 | 29 | ament_cmake 30 | 31 | 32 | -------------------------------------------------------------------------------- /scripts/create_update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Flags: 4 | # -h Help 5 | 6 | Help() 7 | { 8 | echo "Create 3 update script for robots running I.*.*" 9 | echo 10 | echo "usage: bash create_update.sh /path/to/image.swu [-h]" 11 | echo "options:" 12 | echo " -h Print this help statement" 13 | echo 14 | } 15 | 16 | while getopts "h" flag 17 | do 18 | case "${flag}" in 19 | h) 20 | Help 21 | exit;; 22 | \?) 23 | echo "Error: Invalid flag" 24 | exit;; 25 | esac 26 | done 27 | 28 | echo "Image path: $1"; 29 | 30 | curl -X POST --data-binary @$1 http://192.168.186.2/api/firmware-update 31 | -------------------------------------------------------------------------------- /scripts/jazzy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sudo apt update && sudo apt install curl gnupg lsb-release -y 3 | 4 | # Add ROS sources 5 | sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg 6 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null 7 | 8 | # Install the packages 9 | sudo apt update 10 | sudo apt install -y \ 11 | ros-jazzy-ros-base \ 12 | build-essential \ 13 | cmake \ 14 | git \ 15 | wget \ 16 | ros-dev-tools \ 17 | socat \ 18 | network-manager \ 19 | chrony 20 | -------------------------------------------------------------------------------- /scripts/sd_flash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Flags: 4 | # -h Help 5 | 6 | Help() 7 | { 8 | echo "RPI4 SD card flash script. Supports flashing multiple cards simultaneously." 9 | echo 10 | echo "usage: sudo bash sd_flash.sh /path/to/image.img [-h]" 11 | echo "options:" 12 | echo " -h Print this help statement" 13 | echo 14 | } 15 | 16 | while getopts "h" flag 17 | do 18 | case "${flag}" in 19 | h) 20 | Help 21 | exit;; 22 | \?) 23 | echo "Error: Invalid flag" 24 | exit;; 25 | esac 26 | done 27 | 28 | echo "Image path: $1"; 29 | 30 | read -p "Enter each SD card device name separated with a space (i.e. sda sdb sdc): " device_names 31 | 32 | read -p "The SD card(s) will be unmounted and flashed. Press enter to continue." 33 | 34 | for device in $device_names 35 | do 36 | of="$of of=/dev/$device" 37 | sudo umount /dev/$device* 38 | done 39 | 40 | sudo dcfldd if=$1 sizeprobe=if bs=1M$of 41 | 42 | for device in $device_names 43 | do 44 | if [[ $device == mmcblk* ]]; then 45 | last_partition="${device}p2" 46 | else 47 | last_partition="${device}2" 48 | fi 49 | 50 | sudo e2fsck -f /dev/${last_partition} 51 | sudo growpart /dev/$device 2 52 | sudo resize2fs /dev/${last_partition} 53 | done -------------------------------------------------------------------------------- /scripts/swap_off.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo swapoff -v /swapfile 4 | sudo rm /swapfile 5 | -------------------------------------------------------------------------------- /scripts/swap_on.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo fallocate -l 4G /swapfile 4 | sudo chmod 600 /swapfile 5 | sudo mkswap /swapfile 6 | sudo swapon /swapfile 7 | -------------------------------------------------------------------------------- /scripts/turtlebot4_setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2023 Clearpath Robotics, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # @author Roni Kreinin (rkreinin@clearpathrobotics.com) 18 | 19 | 20 | Help() 21 | { 22 | echo "Turtlebot4 Setup script." 23 | echo 24 | echo "usage: sudo bash turtlebot4_setup.sh [-h]" 25 | echo "options:" 26 | echo " h Print this help statement" 27 | echo 28 | } 29 | 30 | echo "Setting up Turtlebot4"; 31 | 32 | sudo apt update && sudo apt upgrade 33 | 34 | wget -qO - https://raw.githubusercontent.com/turtlebot/turtlebot4_setup/jazzy/scripts/jazzy.sh | bash 35 | 36 | sudo apt update && sudo apt upgrade 37 | 38 | sudo apt install -y ros-jazzy-ros-base \ 39 | ros-jazzy-turtlebot4-setup \ 40 | ros-jazzy-turtlebot4-robot \ 41 | ros-jazzy-irobot-create-control \ 42 | ros-jazzy-turtlebot4-navigation \ 43 | ros-dev-tools \ 44 | network-manager \ 45 | chrony 46 | 47 | if [ -f /etc/netplan/50-cloud-init.yaml ]; then 48 | sudo rm /etc/netplan/50-cloud-init.yaml 49 | fi 50 | 51 | git clone https://github.com/turtlebot/turtlebot4_setup.git -b jazzy && \ 52 | sudo mv turtlebot4_setup/boot/firmware/* /boot/firmware && rm turtlebot4_setup/ -rf 53 | 54 | echo "export ROBOT_SETUP=/etc/turtlebot4/setup.bash" | sudo tee -a ~/.bashrc 55 | echo "source \$ROBOT_SETUP" | sudo tee -a ~/.bashrc 56 | echo "source /etc/turtlebot4/aliases.bash" | sudo tee -a ~/.bashrc 57 | 58 | echo "Installation complete. Reboot then run turtlebot4-setup to configure the robot." 59 | -------------------------------------------------------------------------------- /turtlebot4_discovery/configure_discovery.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | prompt_YESno() { 4 | # as the user a Y/n question 5 | # $1 is the variable into which the answer is saved as either "n" or "y" 6 | # $2 is the question to ask 7 | 8 | local __resultvar=$1 9 | local __prompt=$2 10 | 11 | echo -e "\e[39m$__prompt\e[0m" 12 | echo "Y/n: " 13 | 14 | if [[ $AUTO_YES == 1 ]]; 15 | then 16 | echo "Automatically answering Yes" 17 | eval $__resultvar="y" 18 | else 19 | read answer 20 | if [[ $answer =~ ^[n,N].* ]]; 21 | then 22 | eval $__resultvar="n" 23 | else 24 | eval $__resultvar="y" 25 | fi 26 | fi 27 | } 28 | 29 | # Read in the ROS domain ID from user input 30 | while [ 1 ] 31 | do 32 | read -p "ROS_DOMAIN_ID [0]: " domain_id 33 | domain_id=${domain_id:-0} 34 | if [[ $domain_id =~ ^[0-9]{1,3}$ ]]; 35 | then 36 | if ((domain_id > 232)); 37 | then 38 | echo "Invalid domain ID, cannot exceed 232" 39 | continue 40 | fi 41 | break 42 | else 43 | echo "Invalid domain ID, must be an integer (0-232)" 44 | fi 45 | done 46 | 47 | # Collect input data for each discovery server that the user wants to connect to 48 | server_ip_list=() 49 | server_id_list=() 50 | server_port_list=() 51 | server_count=0 52 | complete=0 53 | 54 | echo "Enter the information for the first discovery server" 55 | 56 | while ((! $complete)) 57 | do 58 | 59 | # Read in the Server ID 60 | while [ 1 ] 61 | do 62 | read -p "Discovery Server ID [0]: " discovery_server_id 63 | discovery_server_id=${discovery_server_id:-0} 64 | if [[ $discovery_server_id =~ ^[0-9]{1,3}$ ]]; 65 | then 66 | if ((discovery_server_id > 255)); 67 | then 68 | echo "Invalid server ID, cannot exceed 255" 69 | continue 70 | fi 71 | duplicate=0 72 | for ((i=0; i < server_count; i+=1)) 73 | do 74 | if ((server_id_list[i] == discovery_server_id)); 75 | then 76 | duplicate=1 77 | break 78 | fi 79 | done 80 | if ((duplicate)); 81 | then 82 | echo "Invalid server ID, must be unique and cannot be repeated" 83 | continue 84 | fi 85 | break 86 | else 87 | echo "Invalid server ID, must be an integer (0-255)" 88 | fi 89 | done 90 | 91 | # Read in the Server IP Address 92 | while [ 1 ] 93 | do 94 | read -p "Discovery Server IP: " discovery_ip 95 | if [[ $discovery_ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; 96 | then 97 | break 98 | else 99 | echo "Invalid IP address" 100 | fi 101 | done 102 | 103 | # Read in the Server Port 104 | while [ 1 ] 105 | do 106 | read -p "Discovery Server Port [11811]: " discovery_port 107 | discovery_port=${discovery_port:-11811} 108 | if [[ $discovery_port =~ ^[0-9]{5}$ ]]; 109 | then 110 | break 111 | else 112 | echo "Invalid port, must be an integer (10000-65535)" 113 | fi 114 | done 115 | 116 | # Prompt the user to offer the ability to correct the last server info or add additional servers 117 | while [ 1 ] 118 | do 119 | read -p "Re-enter the last server (r), add another server (a), or done (d): " option 120 | if [[ $option =~ ^[r,R].* ]]; 121 | then 122 | echo "Removing last server entry, re-enter the correct server information" 123 | break 124 | elif [[ $option =~ ^[a,A,d,D].* ]]; 125 | then 126 | # add to list to track all that have been added 127 | server_ip_list+=($discovery_ip) 128 | server_id_list+=($discovery_server_id) 129 | server_port_list+=($discovery_port) 130 | ((server_count+=1)) 131 | if [[ $option =~ ^[d,D].* ]]; 132 | then 133 | complete=1 134 | else 135 | echo "Enter the information for the next discovery server" 136 | fi 137 | break 138 | else 139 | echo "Invalid option" 140 | fi 141 | done 142 | done 143 | 144 | # Build the ROS Discovery Server environment variable string from the input data 145 | discovery_str="" 146 | complete=0 147 | count=0 148 | for ((id=0; count != server_count && id < 256; id+=1)) 149 | do 150 | found=0 151 | for ((i=0; i < server_count; i+=1)) 152 | do 153 | if ((server_id_list[i] == id)); 154 | then 155 | discovery_str="${discovery_str}${server_ip_list[i]}:${server_port_list[i]};" 156 | ((count+=1)) 157 | found=1 158 | break 159 | fi 160 | done 161 | if ((! found)); 162 | then 163 | discovery_str="${discovery_str};" 164 | fi 165 | done 166 | 167 | echo "Configuring:" 168 | echo " ROS_DOMAIN_ID=$domain_id" 169 | echo " ROS_DISCOVERY_SERVER=\"$discovery_str\"" 170 | 171 | # Make directory to hold configs 172 | sudo mkdir -p /etc/turtlebot4_discovery/ 173 | 174 | # Create setup.bash file 175 | setup_file_temp="/tmp/turtlebot4_discovery_setup.bash" 176 | echo "source /opt/ros/jazzy/setup.bash" > $setup_file_temp 177 | echo "export RMW_IMPLEMENTATION=rmw_fastrtps_cpp" >> $setup_file_temp 178 | echo "[ -t 0 ] && export ROS_SUPER_CLIENT=True || export ROS_SUPER_CLIENT=False" >> $setup_file_temp 179 | 180 | # Add user configured data to setup.bash 181 | echo "export ROS_DOMAIN_ID=$domain_id" >> $setup_file_temp 182 | echo "export ROS_DISCOVERY_SERVER=\"$discovery_str\"" >> $setup_file_temp 183 | 184 | # Move setup.bash into final location 185 | setup_file="/etc/turtlebot4_discovery/setup.bash" 186 | sudo mv $setup_file_temp $setup_file 187 | 188 | # Source setup.bash in .bashrc 189 | if ! grep -Fq "source $setup_file" ~/.bashrc 190 | then 191 | echo "source $setup_file" >> ~/.bashrc 192 | fi 193 | 194 | if [ -f "/usr/local/sbin/ip_route.sh" ]||[ -f "/etc/systemd/system/ip_route.service" ]; 195 | then 196 | prompt_YESno cleanup "\Would you like to clean up the outdated IP route? This is no longer required as of turtlebot4_robot version 1.0.3.\e[0m" 197 | if [[ $cleanup == "y" ]]; 198 | then 199 | # Delete existing route if applicable 200 | if [ -f "/usr/local/sbin/ip_route.sh" ]; 201 | then 202 | sudo rm /usr/local/sbin/ip_route.sh 203 | fi 204 | if [ -f "/etc/systemd/system/ip_route.service" ]; 205 | then 206 | # Disable and remove IP route service 207 | sudo systemctl stop ip_route.service 208 | sudo systemctl disable ip_route.service 209 | sudo rm /etc/systemd/system/ip_route.service 210 | fi 211 | fi 212 | fi 213 | 214 | echo "Source your ~/.bashrc file to apply changes" 215 | -------------------------------------------------------------------------------- /turtlebot4_setup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtlebot/turtlebot4_setup/162470e07e6d1b376ff4f55a26a2ce8d8bac6026/turtlebot4_setup/__init__.py -------------------------------------------------------------------------------- /turtlebot4_setup/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2023 Clearpath Robotics 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import copy 18 | from enum import Enum 19 | import os 20 | import re 21 | import shlex 22 | import subprocess 23 | import sys 24 | 25 | import yaml 26 | 27 | 28 | __author__ = 'Roni Kreinin' 29 | __email__ = 'rkreinin@clearpathrobotics.com' 30 | __copyright__ = 'Copyright © 2023 Clearpath Robotics. All rights reserved.' 31 | __license__ = 'Apache 2.0' 32 | 33 | 34 | class SystemOptions(str, Enum): 35 | MODEL = 'MODEL' 36 | VERSION = 'VERSION' 37 | ROS = 'ROS' 38 | HOSTNAME = 'HOSTNAME' 39 | IP = 'IP' 40 | 41 | def __str__(self): 42 | return f'{self.value}' 43 | 44 | 45 | class WifiOptions(str, Enum): 46 | SSID = 'SSID' 47 | PASSWORD = 'PASSWORD' 48 | REG_DOMAIN = 'REG_DOMAIN' 49 | WIFI_MODE = 'WIFI_MODE' 50 | BAND = 'BAND' 51 | IP = 'IP' 52 | DHCP = 'DHCP' 53 | 54 | def __str__(self): 55 | return f'{self.value}' 56 | 57 | 58 | class BashOptions(str, Enum): 59 | CYCLONEDDS_URI = 'CYCLONEDDS_URI' 60 | FASTRTPS_URI = 'FASTRTPS_DEFAULT_PROFILES_FILE' 61 | NAMESPACE = 'ROBOT_NAMESPACE' 62 | DOMAIN_ID = 'ROS_DOMAIN_ID' 63 | DISCOVERY_SERVER = 'ROS_DISCOVERY_SERVER' 64 | RMW = 'RMW_IMPLEMENTATION' 65 | DIAGNOSTICS = 'TURTLEBOT4_DIAGNOSTICS' 66 | WORKSPACE = 'WORKSPACE_SETUP' 67 | SUPER_CLIENT = 'ROS_SUPER_CLIENT' 68 | 69 | def __str__(self): 70 | return f'{self.value}' 71 | 72 | 73 | class DiscoveryOptions(str, Enum): 74 | ENABLED = 'ENABLED' 75 | PORT = 'PORT' 76 | SERVER_ID = 'SERVER_ID' 77 | OFFBOARD_IP = 'OFFBOARD_IP' 78 | OFFBOARD_PORT = 'OFFBOARD_PORT' 79 | OFFBOARD_ID = 'OFFBOARD_ID' 80 | 81 | def __str__(self): 82 | return f'{self.value}' 83 | 84 | 85 | class Conf(): 86 | setup_dir = '/etc/turtlebot4/' 87 | netplan_dir = '/etc/netplan/' 88 | 89 | default_system_conf = { 90 | SystemOptions.MODEL: 'lite', 91 | SystemOptions.VERSION: '2.0.2', 92 | SystemOptions.ROS: 'Jazzy', 93 | SystemOptions.HOSTNAME: 'turtlebot4', 94 | } 95 | 96 | default_wifi_conf = { 97 | WifiOptions.SSID: 'Turtlebot4', 98 | WifiOptions.PASSWORD: 'Turtlebot4', 99 | WifiOptions.REG_DOMAIN: 'CA', 100 | WifiOptions.WIFI_MODE: 'Access Point', 101 | WifiOptions.BAND: '5GHz', 102 | WifiOptions.IP: None, 103 | WifiOptions.DHCP: True, 104 | } 105 | 106 | default_bash_conf = { 107 | BashOptions.CYCLONEDDS_URI: setup_dir + 'cyclonedds_rpi.xml', 108 | BashOptions.FASTRTPS_URI: setup_dir + 'fastdds_rpi.xml', 109 | BashOptions.NAMESPACE: None, 110 | BashOptions.DOMAIN_ID: 0, 111 | BashOptions.DISCOVERY_SERVER: None, 112 | BashOptions.RMW: 'rmw_fastrtps_cpp', 113 | BashOptions.DIAGNOSTICS: '1', 114 | BashOptions.WORKSPACE: '/opt/ros/jazzy/setup.bash', 115 | BashOptions.SUPER_CLIENT: False 116 | } 117 | 118 | default_discovery_conf = { 119 | DiscoveryOptions.ENABLED: False, 120 | DiscoveryOptions.PORT: '11811', 121 | DiscoveryOptions.SERVER_ID: '0', 122 | DiscoveryOptions.OFFBOARD_IP: '', 123 | DiscoveryOptions.OFFBOARD_PORT: '11811', 124 | DiscoveryOptions.OFFBOARD_ID: '1', 125 | } 126 | 127 | def __init__(self) -> None: 128 | self.system_file = os.path.join(self.setup_dir, 'system') 129 | self.setup_bash_file = os.path.join(self.setup_dir, 'setup.bash') 130 | self.netplan_wifis_file = os.path.join(self.netplan_dir, '50-wifis.yaml') 131 | self.discovery_sh_file = os.path.join(self.setup_dir, 'discovery.sh') 132 | self.hostname_file = '/etc/hostname' 133 | self.fw_user_data_file = '/boot/firmware/user-data' 134 | 135 | self.system_conf = copy.deepcopy(self.default_system_conf) 136 | self.wifi_conf = copy.deepcopy(self.default_wifi_conf) 137 | self.bash_conf = copy.deepcopy(self.default_bash_conf) 138 | self.discovery_conf = copy.deepcopy(self.default_discovery_conf) 139 | 140 | subprocess.run(shlex.split('mkdir -p /tmp' + self.setup_dir)) 141 | subprocess.run(shlex.split('mkdir -p /tmp' + self.netplan_dir)) 142 | 143 | self.read() 144 | 145 | def get(self, conf): 146 | if isinstance(conf, SystemOptions): 147 | return self.system_conf.get(conf) 148 | elif isinstance(conf, WifiOptions): 149 | return self.wifi_conf.get(conf) 150 | elif isinstance(conf, BashOptions): 151 | return self.bash_conf.get(conf) 152 | elif isinstance(conf, DiscoveryOptions): 153 | return self.discovery_conf.get(conf) 154 | return None 155 | 156 | def set(self, conf, value): # noqa: A003 157 | if isinstance(conf, SystemOptions): 158 | self.system_conf[conf] = value 159 | elif isinstance(conf, WifiOptions): 160 | self.wifi_conf[conf] = value 161 | elif isinstance(conf, BashOptions): 162 | self.bash_conf[conf] = value 163 | elif isinstance(conf, DiscoveryOptions): 164 | self.discovery_conf[conf] = value 165 | 166 | def apply_default(self, conf): 167 | if conf == self.system_conf: 168 | self.system_conf = copy.deepcopy(self.default_system_conf) 169 | elif conf == self.wifi_conf: 170 | self.wifi_conf = copy.deepcopy(self.default_wifi_conf) 171 | elif conf == self.bash_conf: 172 | self.bash_conf = copy.deepcopy(self.default_bash_conf) 173 | elif conf == self.discovery_conf: 174 | self.discovery_conf = copy.deepcopy(self.default_discovery_conf) 175 | 176 | def read(self): 177 | try: 178 | self.read_system() 179 | self.read_wifi() 180 | self.read_bash() 181 | # Must come after read_bash in order to have the discovery server envar 182 | self.read_discovery() 183 | except Exception as err: 184 | print(f'Error reading configuration: {err}. Terminating') 185 | sys.exit(1) 186 | 187 | def write(self): 188 | try: 189 | self.write_system() 190 | self.write_wifi() 191 | self.write_discovery() 192 | self.write_bash() 193 | except Exception as err: 194 | print(f'Error writing configuration: {err}. Configuration may be incomplete') 195 | sys.exit(1) 196 | 197 | def read_system(self): 198 | with open(self.system_file, 'r') as f: 199 | for line in f.readlines(): 200 | for k in [SystemOptions.MODEL, SystemOptions.VERSION, SystemOptions.ROS]: 201 | if k in line: 202 | self.system_conf[k] = line.split(':')[1].strip() 203 | 204 | self.system_conf[SystemOptions.IP] = subprocess.run( 205 | shlex.split('hostname -I'), 206 | capture_output=True).stdout.decode('ascii').replace('192.168.186.3', '').strip() 207 | 208 | with open(self.hostname_file, 'r') as f: 209 | self.set(SystemOptions.HOSTNAME, f.readline().strip()) 210 | 211 | def write_system(self): 212 | system = [] 213 | with open(self.system_file, 'r') as f: 214 | system = f.readlines() 215 | for i, line in enumerate(system): 216 | is_conf = False 217 | for k in [ 218 | SystemOptions.MODEL, 219 | SystemOptions.VERSION, 220 | SystemOptions.ROS, 221 | SystemOptions.HOSTNAME, 222 | ]: 223 | if k in line: 224 | system[i] = f'{k}:{self.system_conf[k]}\n' 225 | is_conf = True 226 | break 227 | 228 | if not is_conf: 229 | system[i] = line 230 | 231 | with open('/tmp' + self.system_file, 'w') as f: 232 | f.writelines(system) 233 | subprocess.run(shlex.split('sudo mv /tmp' + self.system_file + ' ' + self.system_file)) 234 | 235 | with open('/tmp' + self.hostname_file, 'w') as f: 236 | f.write(self.get(SystemOptions.HOSTNAME)) 237 | subprocess.run(shlex.split('sudo mv /tmp' + self.hostname_file + ' ' + self.hostname_file)) 238 | 239 | # update /boot/firmware/user-data with the new hostname 240 | subprocess.run(shlex.split(f'cp {self.fw_user_data_file} /tmp/user-data')) 241 | subprocess.run(shlex.split(f'sed -i -E "s/^hostname:.+/hostname: {self.get(SystemOptions.HOSTNAME)}/" /tmp/user-data')) # noqa: E501 242 | subprocess.run( 243 | shlex.split(f'sudo mv /tmp/user-data {self.fw_user_data_file}'), 244 | stdout=subprocess.DEVNULL, 245 | stderr=subprocess.DEVNULL, 246 | ) 247 | 248 | def read_wifi(self): 249 | try: 250 | # Try to open the existing wifi configuration, but if it doesn't exist we can carry on 251 | netplan = yaml.load(open(self.netplan_wifis_file, 'r'), yaml.SafeLoader) 252 | 253 | # wlan0 Config 254 | wlan0 = netplan['network']['wifis']['wlan0'] 255 | 256 | # Get SSID 257 | self.set(WifiOptions.SSID, list(wlan0['access-points'])[0]) 258 | # SSID settings 259 | ssid_settings = wlan0['access-points'][self.get(WifiOptions.SSID)] 260 | 261 | self.set(WifiOptions.PASSWORD, ssid_settings.get('password')) 262 | 263 | if wlan0.get('addresses'): 264 | self.set(WifiOptions.IP, wlan0['addresses'][0]) 265 | else: 266 | self.set(WifiOptions.IP, None) 267 | 268 | if wlan0.get('dhcp4') is True: 269 | self.set(WifiOptions.DHCP, True) 270 | else: 271 | self.set(WifiOptions.DHCP, False) 272 | 273 | if ssid_settings.get('mode') == 'ap': 274 | self.set(WifiOptions.WIFI_MODE, 'Access Point') 275 | else: 276 | self.set(WifiOptions.WIFI_MODE, 'Client') 277 | 278 | if ssid_settings.get('band'): 279 | self.set(WifiOptions.BAND, ssid_settings.get('band')) 280 | else: 281 | self.set(WifiOptions.BAND, 'Any') 282 | except Exception: 283 | # If the wifi configuration doesn't have a wlan0 configuration, just skip this 284 | pass 285 | 286 | def write_wifi(self): 287 | ssid = self.get(WifiOptions.SSID) 288 | password = self.get(WifiOptions.PASSWORD) 289 | dhcp = self.get(WifiOptions.DHCP) 290 | wifi_mode = self.get(WifiOptions.WIFI_MODE) 291 | band = self.get(WifiOptions.BAND) 292 | ip = self.get(WifiOptions.IP) 293 | 294 | wlan0 = { 295 | 'dhcp4': dhcp, 296 | 'access-points': { 297 | ssid: {} 298 | } 299 | } 300 | 301 | if password is not None: 302 | wlan0['access-points'][ssid].update({'password': password}) 303 | 304 | if ip is not None: 305 | wlan0.update({'addresses': [ip]}) 306 | 307 | if wifi_mode == 'Access Point': 308 | wlan0['access-points'][ssid].update({'mode': 'ap'}) 309 | 310 | if band is not None and band != 'Any': 311 | wlan0['access-points'][ssid].update({'band': band}) 312 | 313 | netplan = { 314 | 'network': { 315 | 'version': 2, 316 | 'wifis': { 317 | 'renderer': 'NetworkManager', 318 | 'wlan0': wlan0, 319 | }, 320 | } 321 | } 322 | 323 | with open('/tmp' + self.netplan_wifis_file, 'w') as f: 324 | f.write('# This file was automatically created by the turtlebot4-setup tool and should not be manually modified\n\n') # noqa: E501 325 | 326 | yaml.dump(netplan, 327 | stream=open('/tmp' + self.netplan_wifis_file, 'a'), 328 | Dumper=yaml.SafeDumper, 329 | indent=4, 330 | default_flow_style=False, 331 | default_style=None) 332 | 333 | subprocess.run(shlex.split( 334 | 'sudo mv /tmp' + self.netplan_wifis_file + ' ' + self.netplan_wifis_file)) 335 | 336 | def read_bash(self): 337 | with open(self.setup_bash_file, 'r') as f: 338 | for line in f.readlines(): 339 | for k in self.bash_conf.keys(): 340 | if f'export {k}' in line: 341 | try: 342 | value = line.split('=')[1].strip().strip('\'"') 343 | if (k == BashOptions.SUPER_CLIENT): 344 | value = value.split('||')[0].strip().strip('\'"') 345 | if value == '': 346 | self.set(k, None) 347 | else: 348 | self.set(k, value) 349 | except IndexError: 350 | self.set(k, None) 351 | break 352 | 353 | def write_bash(self): 354 | bash = [] 355 | with open(self.setup_bash_file, 'r') as f: 356 | bash = f.readlines() 357 | # Loop through every bash setting 358 | for k, v in self.bash_conf.items(): 359 | # Check if the setting is currently in the setup.bash and update it 360 | found = False 361 | if v is None: 362 | v = '' 363 | for i, line in enumerate(bash): 364 | export_re = re.compile(rf'^\s*export\s+{k}=.*') 365 | if export_re.match(line): 366 | if (k == BashOptions.SUPER_CLIENT and str(v) == 'True'): 367 | # Ensure super client is only applied on user terminals 368 | bash[i] = f'[ -t 0 ] && export {k}={v} || export {k}=False\n' # noqa: 501 369 | else: 370 | # Quotations required around v to handle multiple servers 371 | # in discovery server 372 | bash[i] = f'export {k}=\"{v}\"\n' 373 | found = True 374 | 375 | # If the setting is missing from the setup.bash, add it to the beginning 376 | if not found: 377 | if (k == BashOptions.SUPER_CLIENT and str(v) == 'True'): 378 | # Ensure super client is only applied on user terminals 379 | bash.insert(0, f'[ -t 0 ] && export {k}={v} || export {k}=False\n') # noqa: 501 380 | else: 381 | # Quotations required around v to handle multiple servers 382 | # in discovery server 383 | bash.insert(0, f'export {k}=\"{v}\"\n') 384 | 385 | with open('/tmp' + self.setup_bash_file, 'w') as f: 386 | f.writelines(bash) 387 | subprocess.run(shlex.split(f'sudo mv /tmp{self.setup_bash_file} {self.setup_bash_file}')) 388 | 389 | for k, v in self.bash_conf.items(): 390 | if v is None: 391 | os.environ[k] = '' 392 | else: 393 | os.environ[k] = str(v) 394 | 395 | def read_discovery(self): 396 | discovery_server = self.get(BashOptions.DISCOVERY_SERVER) 397 | if discovery_server is None or discovery_server == '': 398 | self.set(DiscoveryOptions.ENABLED, False) 399 | else: 400 | self.set(DiscoveryOptions.ENABLED, True) 401 | try: 402 | servers = discovery_server.split(';') 403 | for i, s in enumerate(servers): 404 | s = s.strip() 405 | if s: 406 | server = s.split(':') 407 | if (server[0].strip('"') == '127.0.0.1'): 408 | self.set(DiscoveryOptions.SERVER_ID, i) 409 | if len(server) > 1: 410 | self.set(DiscoveryOptions.PORT, int(server[1].strip('\'"'))) 411 | else: 412 | self.set(DiscoveryOptions.PORT, 11811) 413 | else: 414 | self.set(DiscoveryOptions.OFFBOARD_ID, i) 415 | self.set(DiscoveryOptions.OFFBOARD_IP, server[0].strip('\'"')) 416 | if len(server) > 1: 417 | self.set( 418 | DiscoveryOptions.OFFBOARD_PORT, int(server[1].strip('\'"'))) 419 | else: 420 | self.set(DiscoveryOptions.OFFBOARD_PORT, 11811) 421 | except Exception: 422 | self.discovery_conf = self.default_discovery_conf 423 | 424 | def write_discovery(self): 425 | if self.get(DiscoveryOptions.ENABLED) is True: 426 | self.set(BashOptions.DISCOVERY_SERVER, self.get_discovery_str()) 427 | self.set(BashOptions.RMW, 'rmw_fastrtps_cpp') 428 | self.set(BashOptions.SUPER_CLIENT, True) 429 | 430 | with open('/tmp' + self.discovery_sh_file, 'w') as f: 431 | f.write('#!/bin/bash\n') 432 | f.write('# This file was automatically created by the turtlebot4-setup tool and should not be manually modified\n\n') # noqa: E501 433 | f.write(f'source {self.get(BashOptions.WORKSPACE)}\n') 434 | f.write(f'fastdds discovery -i {self.get(DiscoveryOptions.SERVER_ID)} -p {self.get(DiscoveryOptions.PORT)}') # noqa: E501 435 | subprocess.run(shlex.split( 436 | 'sudo mv /tmp' + self.discovery_sh_file + ' ' + self.discovery_sh_file)) 437 | else: 438 | self.set(BashOptions.DISCOVERY_SERVER, None) 439 | self.set(BashOptions.SUPER_CLIENT, False) 440 | 441 | self.write_bash() 442 | 443 | def get_discovery_str(self) -> str: 444 | discovery_str = '' 445 | servers = [{ 446 | 'id': self.get(DiscoveryOptions.SERVER_ID), 447 | 'ip': '127.0.0.1', 448 | 'port': self.get(DiscoveryOptions.PORT), 449 | }] 450 | offboard_ip = self.get(DiscoveryOptions.OFFBOARD_IP) 451 | if offboard_ip: 452 | servers.append({ 453 | 'id': self.get(DiscoveryOptions.OFFBOARD_ID), 454 | 'ip': offboard_ip, 455 | 'port': self.get(DiscoveryOptions.OFFBOARD_PORT) 456 | }) 457 | 458 | servers.sort(key=lambda s: int(s['id'])) 459 | 460 | i = 0 461 | for s in servers: 462 | while i < int(s['id']): 463 | discovery_str += ';' 464 | i += 1 465 | discovery_str += f"{s['ip']}:{s['port']};" 466 | i += 1 467 | return discovery_str 468 | 469 | def get_create3_server_str(self) -> str: 470 | # Create3 should only point at the local server on the pi 471 | discovery_str = '' 472 | for i in range(int(self.get(DiscoveryOptions.SERVER_ID))): 473 | discovery_str += ';' 474 | discovery_str += f'192.168.186.3:{self.get(DiscoveryOptions.PORT)}' 475 | return discovery_str 476 | -------------------------------------------------------------------------------- /turtlebot4_setup/menu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2023 Clearpath Robotics 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | 19 | from typing import Callable, List, Union 20 | 21 | from pygments import formatters, highlight, lexers 22 | from pygments.util import ClassNotFound 23 | 24 | from simple_term_menu_vendor.simple_term_menu import TerminalMenu 25 | 26 | 27 | __author__ = 'Roni Kreinin' 28 | __email__ = 'rkreinin@clearpathrobotics.com' 29 | __copyright__ = 'Copyright © 2023 Clearpath Robotics. All rights reserved.' 30 | __license__ = 'Apache 2.0' 31 | 32 | 33 | class MenuEntry(): 34 | 35 | def __init__(self, entry: Union[str, Callable], function) -> None: 36 | self.function = function 37 | self.entry = entry 38 | 39 | if isinstance(entry, str): 40 | self.name = entry 41 | elif isinstance(entry, Callable): 42 | self.name = entry() 43 | 44 | def update(self): 45 | if isinstance(self.entry, Callable): 46 | self.name = self.entry() 47 | 48 | def select(self): 49 | self.function() 50 | 51 | 52 | class Menu(): 53 | menu_cursor = '> ' 54 | menu_cursor_style = ('fg_yellow', 'bold') 55 | menu_style = ('bg_black', 'fg_yellow') 56 | menu = None 57 | 58 | def __init__(self, title: Union[str, Callable], menu_entries: List[MenuEntry]) -> None: 59 | self.title = title 60 | self.menu_entries = menu_entries 61 | self.menu_sel = 0 62 | self.menu = self.create_term_menu() 63 | self.menu_exit = False 64 | 65 | def update_title(self): 66 | if isinstance(self.title, str): 67 | self.name = self.title + '\nPress Q, Esc, or CTRL+C to go back.\n' 68 | elif isinstance(self.title, Callable): 69 | self.name = self.title() + '\nPress Q, Esc, or CTRL+C to go back.\n' 70 | 71 | max_len = max(len(line) for line in self.name.split('\n')) 72 | self.name += '-' * max_len 73 | 74 | def create_term_menu(self): 75 | menu_entries = [] 76 | for e in self.menu_entries: 77 | e.update() 78 | menu_entries.append(e.name) 79 | 80 | self.update_title() 81 | 82 | return TerminalMenu( 83 | menu_entries, 84 | title=self.name, 85 | menu_cursor=self.menu_cursor, 86 | menu_cursor_style=self.menu_cursor_style, 87 | menu_highlight_style=self.menu_style, 88 | cycle_cursor=True, 89 | clear_screen=True, 90 | skip_empty_entries=True) 91 | 92 | def refresh_term_menu(self, increment=0): 93 | self.menu = self.create_term_menu() 94 | if self.menu_sel is not None: 95 | for i in range(0, self.menu_sel + increment): 96 | if self.menu_entries[i].name != '': 97 | self.menu._view.increment_active_index() 98 | 99 | def reset_term_menu(self): 100 | self.menu = self.create_term_menu() 101 | self.menu_sel = 0 102 | 103 | def exit(self): # noqa: A003 104 | self.menu_exit = True 105 | 106 | def show(self, reset=True): 107 | self.menu_exit = False 108 | if reset: 109 | self.reset_term_menu() 110 | while not self.menu_exit: 111 | self.menu_sel = self.menu.show() 112 | if self.menu_sel is None or self.menu_sel >= len(self.menu_entries): 113 | break 114 | else: 115 | self.menu_entries[self.menu_sel].select() 116 | self.refresh_term_menu() 117 | 118 | 119 | class OptionsMenu(Menu): 120 | 121 | def __init__(self, title: Union[str, Callable], menu_entries: List[str], default_option=None) -> None: # noqa: E501 122 | self.option = default_option 123 | self.menu_entries = [] 124 | 125 | for e in menu_entries: 126 | self.menu_entries.append(MenuEntry(e, self.set_option)) 127 | 128 | super().__init__(title, self.menu_entries) 129 | 130 | if default_option is not None: 131 | for i, e in enumerate(menu_entries): 132 | if e == str(default_option): 133 | self.menu_sel = i 134 | self.refresh_term_menu() 135 | 136 | def set_option(self): 137 | self.option = self.menu_entries[self.menu_sel].name 138 | self.exit() 139 | 140 | def show(self): 141 | super().show(reset=False) 142 | return self.option 143 | 144 | 145 | class HelpMenu(Menu): 146 | # Help -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 147 | title = """ 148 | _ _ _ 149 | | || |___| |_ __ 150 | | __ / -_) | '_ \\ 151 | |_||_\\___|_| .__/ 152 | |_| 153 | """ 154 | 155 | def __init__(self, text: str, display_help_title=True) -> None: 156 | if display_help_title: 157 | super().__init__(self.title + text, []) 158 | else: 159 | super().__init__(text, []) 160 | 161 | 162 | class Prompt(): 163 | 164 | def __init__(self, prompt: str, default_response=None, note=None, response_type=str) -> None: 165 | self.prompt = prompt 166 | self.default_response = default_response 167 | self.note = note 168 | self.response_type = response_type 169 | 170 | def show(self): 171 | response = None 172 | 173 | if self.note is not None: 174 | print(self.note) 175 | print('Press CTRL+C to return without an input.') 176 | max_len = 0 177 | for line in self.note.split('\n'): 178 | max_len = max(max_len, len(line)) 179 | print('-' * max_len) 180 | 181 | try: 182 | response = input(self.prompt) 183 | except KeyboardInterrupt: 184 | return self.default_response 185 | 186 | try: 187 | self.response_type(response) 188 | except ValueError: 189 | if self.response_type == int and response == '': 190 | return self.default_response 191 | 192 | print('Invalid input [{0}]. {1} required.'.format(response, self.response_type)) 193 | return self.show() 194 | 195 | if response == '': 196 | response = None 197 | 198 | return response 199 | 200 | 201 | class PreviewMenu(): 202 | 203 | def __init__(self, directories: List[str]) -> None: 204 | self.directories = directories 205 | self.menu = TerminalMenu( 206 | self.list_files(), 207 | preview_command=self.highlight_file, 208 | preview_size=0.75) 209 | 210 | def show(self): 211 | self.menu.show() 212 | 213 | def list_files(self): 214 | files = [] 215 | for directory in self.directories: 216 | for file in os.listdir(directory): 217 | if os.path.isfile(os.path.join(directory, file)): 218 | files.append(os.path.join(directory, file)) 219 | return files 220 | 221 | def highlight_file(self, filepath): 222 | try: 223 | lexer = lexers.get_lexer_for_filename(filepath, stripnl=False, stripall=False) 224 | except ClassNotFound: 225 | lexer = lexers.get_lexer_by_name('text', stripnl=False, stripall=False) 226 | formatter = formatters.TerminalFormatter(bg='dark') # dark or light 227 | 228 | try: 229 | with open(filepath, 'r') as f: 230 | file_content = f.read() 231 | except PermissionError: 232 | file_content = 'Permission denied.\nPlease check file permissions' 233 | except FileNotFoundError: 234 | file_content = f'{filepath} was deleted' 235 | except Exception as err: 236 | file_content = f'Error reading {filepath}:\n{err}' 237 | 238 | highlighted_file_content = highlight(file_content, lexer, formatter) 239 | return highlighted_file_content 240 | 241 | 242 | class ErrorPrompt(Menu): 243 | # Error -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 244 | title = """ 245 | ___ 246 | | __|_ _ _ _ ___ _ _ 247 | | _|| '_| '_/ _ \\ '_| 248 | |___|_| |_| \\___/_| 249 | 250 | """ 251 | 252 | def __init__(self, text: str, display_help_title=True) -> None: 253 | if display_help_title: 254 | super().__init__(self.title + text, []) 255 | else: 256 | super().__init__(text, []) 257 | -------------------------------------------------------------------------------- /turtlebot4_setup/ros_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2023 Clearpath Robotics 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import shlex 19 | import subprocess 20 | 21 | import robot_upstart 22 | from turtlebot4_setup.conf import BashOptions, Conf, DiscoveryOptions, SystemOptions 23 | from turtlebot4_setup.menu import ErrorPrompt, Menu, MenuEntry, OptionsMenu, Prompt 24 | 25 | 26 | __author__ = 'Roni Kreinin' 27 | __email__ = 'rkreinin@clearpathrobotics.com' 28 | __copyright__ = 'Copyright © 2023 Clearpath Robotics. All rights reserved.' 29 | __license__ = 'Apache 2.0' 30 | 31 | 32 | class RosSetup(): 33 | # ROS Setup -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 34 | title = """ 35 | ___ ___ ___ ___ _ 36 | | _ \\/ _ \\/ __| / __| ___| |_ _ _ _ __ 37 | | / (_) \\__ \\ \\__ \\/ -_) _| || | '_ \\ 38 | |_|_\\\\___/|___/ |___/\\___|\\__|\\_,_| .__/ 39 | |_| 40 | """ 41 | 42 | setup_dir = '/etc/turtlebot4/' 43 | 44 | def __init__(self, conf: Conf) -> None: 45 | self.conf = conf 46 | 47 | self.discovery_server_menu = DiscoveryServer(self.conf) 48 | self.bash_setup_menu = BashSetup(self.conf) 49 | self.robot_upstart_menu = RobotUpstart(self.conf) 50 | 51 | self.entries = [MenuEntry('Bash Setup', self.bash_setup_menu.show), 52 | MenuEntry('Discovery Server', function=self.discovery_server_menu.show), 53 | MenuEntry('Robot Upstart', self.robot_upstart_menu.show)] 54 | 55 | self.menu = Menu(self.title, self.entries) 56 | 57 | def show(self): 58 | self.menu.show() 59 | 60 | 61 | class BashSetup(): 62 | # Bash Setup -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 63 | title = """ 64 | ___ _ ___ _ 65 | | _ ) __ _ __| |_ / __| ___| |_ _ _ _ __ 66 | | _ \\/ _` (_-< ' \\ \\__ \\/ -_) _| || | '_ \\ 67 | |___/\\__,_/__/_||_| |___/\\___|\\__|\\_,_| .__/ 68 | |_| 69 | """ 70 | 71 | def __init__(self, conf: Conf) -> None: 72 | self.conf = conf 73 | 74 | self.entries = [MenuEntry(entry=self.format_entry(BashOptions.NAMESPACE), 75 | function=self.set_robot_namespace), 76 | MenuEntry(entry=self.format_entry(BashOptions.DOMAIN_ID), 77 | function=self.set_ros_domain_id), 78 | MenuEntry(entry=self.format_entry(BashOptions.RMW), 79 | function=self.set_rmw_implementation), 80 | MenuEntry(entry=self.format_entry(BashOptions.DIAGNOSTICS), 81 | function=self.set_turtlebot4_diagnostics), 82 | MenuEntry(entry=self.format_entry(BashOptions.WORKSPACE), 83 | function=self.set_workspace_setup), 84 | MenuEntry(entry=self.format_entry(BashOptions.CYCLONEDDS_URI), 85 | function=self.set_cyclonedds_uri), 86 | MenuEntry(entry=self.format_entry(BashOptions.FASTRTPS_URI), 87 | function=self.set_fastrtps_default_profiles_file), 88 | MenuEntry('', None), 89 | MenuEntry(entry='Apply Defaults', function=self.apply_defaults), 90 | MenuEntry(entry='Save', function=self.save_settings), ] 91 | 92 | self.menu = Menu(self.title, self.entries) 93 | 94 | def format_entry(self, option: BashOptions): 95 | if option == BashOptions.DIAGNOSTICS: 96 | return lambda: '{0}{1}[{2}]'.format( 97 | option, 98 | ' ' * (35-len(option)), 99 | 'Enabled' if self.conf.get(option) == '1' else 'Disabled') 100 | else: 101 | return lambda: '{0}{1}[{2}]'.format( 102 | option, 103 | ' ' * (35-len(option)), 104 | '' if self.conf.get(option) is None else self.conf.get(option)) 105 | 106 | def show(self): 107 | self.conf.read() 108 | self.menu.show() 109 | 110 | def set_rmw_implementation(self): 111 | options = OptionsMenu(title=BashOptions.RMW, 112 | menu_entries=['rmw_fastrtps_cpp', 'rmw_cyclonedds_cpp'], 113 | default_option=self.conf.get(BashOptions.RMW)) 114 | self.conf.set(BashOptions.RMW, options.show()) 115 | 116 | def set_ros_domain_id(self): 117 | p = Prompt(prompt='{0} [{1}]: '.format( 118 | BashOptions.DOMAIN_ID, 119 | self.conf.get(BashOptions.DOMAIN_ID)), 120 | default_response=self.conf.get(BashOptions.DOMAIN_ID), 121 | response_type=int, 122 | note='ROS Domain ID (0-101) or (215-232)') 123 | domain_id = p.show() 124 | domain_id = max(0, min(int(domain_id), 232)) 125 | if (domain_id > 101 and domain_id < 215): 126 | domain_id = 101 127 | self.conf.set(BashOptions.DOMAIN_ID, domain_id) 128 | 129 | def set_cyclonedds_uri(self): 130 | p = Prompt(prompt='{0} [{1}]: '.format( 131 | BashOptions.CYCLONEDDS_URI, 132 | self.conf.get(BashOptions.CYCLONEDDS_URI)), 133 | default_response=self.conf.get(BashOptions.CYCLONEDDS_URI), 134 | note='Full path to .xml file') 135 | self.conf.set(BashOptions.CYCLONEDDS_URI, p.show()) 136 | 137 | def set_fastrtps_default_profiles_file(self): 138 | p = Prompt(prompt='{0} [{1}]: '.format( 139 | BashOptions.FASTRTPS_URI, 140 | self.conf.get(BashOptions.FASTRTPS_URI)), 141 | default_response=self.conf.get(BashOptions.FASTRTPS_URI), 142 | note='Full path to .xml file') 143 | self.conf.set(BashOptions.FASTRTPS_URI, p.show()) 144 | 145 | def set_workspace_setup(self): 146 | p = Prompt(prompt='{0} [{1}]: '.format( 147 | BashOptions.WORKSPACE, 148 | self.conf.get(BashOptions.WORKSPACE)), 149 | default_response=self.conf.get(BashOptions.WORKSPACE), 150 | note='Full path to setup.bash file') 151 | self.conf.set(BashOptions.WORKSPACE, p.show()) 152 | 153 | def set_robot_namespace(self): 154 | p = Prompt(prompt='{0} [{1}]: '.format( 155 | BashOptions.NAMESPACE, 156 | '' if self.conf.get(BashOptions.NAMESPACE) is None else 157 | self.conf.get(BashOptions.NAMESPACE)), 158 | default_response=self.conf.get(BashOptions.NAMESPACE), 159 | note='ROS2 namespace') 160 | # Add '/' if needed 161 | ns = p.show() 162 | if ns is not None and ns[0] != '/': 163 | ns = '/' + ns 164 | self.conf.set(BashOptions.NAMESPACE, ns) 165 | 166 | def set_turtlebot4_diagnostics(self): 167 | options = OptionsMenu( 168 | title=BashOptions.DIAGNOSTICS, 169 | menu_entries=['Enabled', 'Disabled'], 170 | default_option='Enabled' if self.conf.get(BashOptions.DIAGNOSTICS) == '1' else 'Disabled') # noqa: E501 171 | self.conf.set(BashOptions.DIAGNOSTICS, '1' if options.show() == 'Enabled' else '0') 172 | 173 | def save_settings(self): 174 | self.conf.write() 175 | self.menu.exit() 176 | 177 | def apply_defaults(self): 178 | self.conf.apply_default(self.conf.bash_conf) 179 | 180 | 181 | class DiscoveryServer(): 182 | # Discovery Server -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 183 | title = """ 184 | ___ _ ___ 185 | | \\(_)___ __ _____ _____ _ _ _ _ / __| ___ _ ___ _____ _ _ 186 | | |) | (_- None: 192 | self.conf = configs 193 | 194 | self.entries = [ 195 | MenuEntry( 196 | entry=self.format_entry('Enabled', DiscoveryOptions.ENABLED), 197 | function=self.set_enabled), 198 | MenuEntry( 199 | entry=self.format_entry('Onboard Server - Port', DiscoveryOptions.PORT), 200 | function=self.set_port), 201 | MenuEntry( 202 | entry=self.format_entry('Onboard Server - Server ID', DiscoveryOptions.SERVER_ID), 203 | function=self.set_server_id), 204 | MenuEntry( 205 | entry=self.format_entry('Offboard Server - IP', DiscoveryOptions.OFFBOARD_IP), 206 | function=self.set_offboard_ip), 207 | MenuEntry( 208 | entry=self.format_entry('Offboard Server - Port', DiscoveryOptions.OFFBOARD_PORT), 209 | function=self.set_offboard_port), 210 | MenuEntry( 211 | entry=self.format_entry('Offboard Server - Server ID', DiscoveryOptions.OFFBOARD_ID), # noqa: E501 212 | function=self.set_offboard_server_id), 213 | MenuEntry('', None), 214 | MenuEntry(entry='Apply Defaults', function=self.apply_defaults), 215 | MenuEntry(entry='Save', function=self.save_settings)] 216 | 217 | self.menu = Menu(title=self.title, menu_entries=self.entries) 218 | 219 | def format_entry(self, name, opt: DiscoveryOptions): 220 | return lambda: '{0}{1}[{2}]'.format( 221 | name, 222 | ' ' * (32 - len(name)), 223 | self.conf.get(opt)) 224 | 225 | def show(self): 226 | self.menu.show() 227 | 228 | def set_enabled(self): 229 | options = OptionsMenu(title='Fast-DDS Discovery Server', 230 | menu_entries=['True', 'False'], 231 | default_option=self.conf.get(DiscoveryOptions.ENABLED)) 232 | self.conf.set(DiscoveryOptions.ENABLED, options.show() == 'True') 233 | 234 | def set_port(self): 235 | p = Prompt(prompt='Port [{0}]: '.format(self.conf.get(DiscoveryOptions.PORT)), 236 | default_response=self.conf.get(DiscoveryOptions.PORT), 237 | response_type=int, 238 | note='Onboard Discovery Server Port (10000-65535)') 239 | port = p.show() 240 | port = max(10000, min(int(port), 65535)) 241 | self.conf.set(DiscoveryOptions.PORT, port) 242 | 243 | def set_server_id(self): 244 | p = Prompt(prompt='Server ID [{0}]: '.format(self.conf.get(DiscoveryOptions.SERVER_ID)), 245 | default_response=self.conf.get(DiscoveryOptions.SERVER_ID), 246 | response_type=int, 247 | note='Onboard Discovery Server ID (0-255)') 248 | server_id = p.show() 249 | server_id = max(0, min(int(server_id), 255)) 250 | if (self.conf.get(DiscoveryOptions.OFFBOARD_IP) and (server_id == int(self.conf.get(DiscoveryOptions.OFFBOARD_ID)))): # noqa: 501 251 | return 252 | self.conf.set(DiscoveryOptions.SERVER_ID, server_id) 253 | 254 | def set_offboard_ip(self): 255 | p = Prompt(prompt='IP [{0}]: '.format(self.conf.get(DiscoveryOptions.OFFBOARD_IP)), 256 | default_response=self.conf.get(DiscoveryOptions.OFFBOARD_IP), 257 | note='Offboard Discovery Server IP (Leave blank to disable)') 258 | ip_addr = p.show() 259 | if ip_addr: 260 | ip_addr = ip_addr.strip().strip('\'"') 261 | else: 262 | ip_addr = '' 263 | self.conf.set(DiscoveryOptions.OFFBOARD_IP, ip_addr) 264 | 265 | def set_offboard_port(self): 266 | p = Prompt(prompt='Port [{0}]: '.format(self.conf.get(DiscoveryOptions.OFFBOARD_PORT)), 267 | default_response=self.conf.get(DiscoveryOptions.OFFBOARD_PORT), 268 | response_type=int, 269 | note='Offboard Discovery Server Port (10000-65535)') 270 | port = p.show() 271 | port = max(10000, min(int(port), 65535)) 272 | self.conf.set(DiscoveryOptions.OFFBOARD_PORT, port) 273 | 274 | def set_offboard_server_id(self): 275 | p = Prompt(prompt='Server ID [{0}]: '.format(self.conf.get(DiscoveryOptions.OFFBOARD_ID)), 276 | default_response=self.conf.get(DiscoveryOptions.OFFBOARD_ID), 277 | response_type=int, 278 | note='Offboard Discovery Server ID (0-255) - Cannot be the same as the onboard server') # noqa: 501 279 | server_id = p.show() 280 | server_id = max(0, min(int(server_id), 255)) 281 | if (server_id == int(self.conf.get(DiscoveryOptions.SERVER_ID))): 282 | return 283 | self.conf.set(DiscoveryOptions.OFFBOARD_ID, server_id) 284 | 285 | def apply_defaults(self): 286 | self.conf.apply_default(self.conf.discovery_conf) 287 | 288 | def save_settings(self): 289 | self.conf.write_discovery() 290 | self.menu.exit() 291 | 292 | 293 | class RobotUpstart(): 294 | # Robot Upstart -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 295 | title = """ 296 | ___ _ _ _ _ _ _ 297 | | _ \\___| |__ ___| |_ | | | |_ __ __| |_ __ _ _ _| |_ 298 | | / _ \\ '_ \\/ _ \\ _| | |_| | '_ (_-< _/ _` | '_| _| 299 | |_|_\\___/_.__/\\___/\\__| \\___/| .__/__/\\__\\__,_|_| \\__| 300 | |_| 301 | """ 302 | 303 | def __init__(self, configs: Conf) -> None: 304 | self.conf = configs 305 | self.entries = [MenuEntry(entry='Restart', 306 | function=self.restart), 307 | MenuEntry(entry='Start', 308 | function=self.start), 309 | MenuEntry(entry='Stop', 310 | function=self.stop), 311 | MenuEntry(entry='Install', 312 | function=self.install), 313 | MenuEntry(entry='Uninstall', 314 | function=self.uninstall), 315 | MenuEntry(entry='', 316 | function=None), 317 | MenuEntry(entry='Status', 318 | function=self.view_service_status)] 319 | 320 | self.menu = Menu(self.title, self.entries) 321 | 322 | def show(self): 323 | self.menu.show() 324 | 325 | def view_service_status(self): 326 | try: 327 | subprocess.run(shlex.split('sudo systemctl status turtlebot4.service')) 328 | except KeyboardInterrupt: 329 | pass 330 | 331 | def stop(self): 332 | subprocess.run(shlex.split('sudo systemctl stop turtlebot4.service')) 333 | 334 | def start(self): 335 | subprocess.run(shlex.split('sudo systemctl start turtlebot4.service')) 336 | 337 | def restart(self): 338 | subprocess.run(shlex.split('sudo systemctl restart turtlebot4.service')) 339 | 340 | def daemon_reload(self): 341 | subprocess.run(shlex.split('sudo systemctl daemon-reload')) 342 | 343 | def install(self): 344 | try: 345 | self.uninstall() 346 | 347 | rmw = os.environ['RMW_IMPLEMENTATION'] 348 | if rmw == 'rmw_fastrtps_cpp': 349 | rmw_config = os.environ['FASTRTPS_DEFAULT_PROFILES_FILE'] 350 | else: 351 | rmw_config = os.environ['CYCLONEDDS_URI'] 352 | 353 | turtlebot4_job = robot_upstart.Job( 354 | name='turtlebot4', 355 | workspace_setup=os.environ['ROBOT_SETUP'], 356 | rmw=rmw, 357 | rmw_config=rmw_config, 358 | systemd_after='network-online.target') 359 | 360 | turtlebot4_job.symlink = True 361 | turtlebot4_job.add( 362 | package='turtlebot4_bringup', 363 | filename=f'launch/{self.conf.get(SystemOptions.MODEL)}.launch.py' 364 | ) 365 | turtlebot4_job.install() 366 | 367 | if self.conf.get(DiscoveryOptions.ENABLED): 368 | discovery_job = robot_upstart.Job(workspace_setup=os.environ['ROBOT_SETUP']) 369 | discovery_job.install(Provider=TurtleBot4Extras) 370 | subprocess.run(shlex.split('sudo systemctl restart discovery.service')) 371 | 372 | self.daemon_reload() 373 | 374 | except KeyError as err: 375 | ErrorPrompt(f'Failed to install systemd job:\n{err} is not defined').show() 376 | except Exception as err: 377 | ErrorPrompt(f'Failed to install systemd job:\n{err}').show() 378 | 379 | def uninstall(self): 380 | try: 381 | self.stop() 382 | 383 | # Uninstall Turtlebot4 Service 384 | turtlebot4_job = robot_upstart.Job( 385 | name='turtlebot4', 386 | workspace_setup=os.environ['ROBOT_SETUP']) 387 | turtlebot4_job.uninstall() 388 | 389 | # Uninstall Discovery Server Service 390 | if os.path.exists('/lib/systemd/system/discovery.service'): 391 | subprocess.run(shlex.split( 392 | 'sudo systemctl stop discovery.service'), capture_output=True) 393 | discovery_job = robot_upstart.Job(workspace_setup=os.environ['ROBOT_SETUP']) 394 | discovery_job.uninstall(Provider=TurtleBot4Extras) 395 | 396 | self.daemon_reload() 397 | except KeyError as err: 398 | ErrorPrompt(f'Failed to uninstall existing systemd job:\n{err} is not defined').show() 399 | except Exception as err: 400 | ErrorPrompt(f'Failed to uninstall existing systemd job:\n{err}').show() 401 | 402 | 403 | class TurtleBot4Extras(robot_upstart.providers.Generic): 404 | 405 | def post_install(self): 406 | pass 407 | 408 | def generate_install(self): 409 | with open('/etc/turtlebot4/discovery.conf') as f: 410 | discovery_conf_contents = f.read() 411 | discovery_conf_contents = self.fix_conf_username(discovery_conf_contents) 412 | with open('/etc/turtlebot4/discovery.sh') as f: 413 | discovery_sh_contents = f.read() 414 | return { 415 | '/lib/systemd/system/discovery.service': { 416 | 'content': discovery_conf_contents, 417 | 'mode': 0o644 418 | }, 419 | '/usr/sbin/discovery': { 420 | 'content': discovery_sh_contents, 421 | 'mode': 0o755 422 | }, 423 | '/etc/systemd/system/multi-user.target.wants/discovery.service': { 424 | 'symlink': '/lib/systemd/system/discovery.service' 425 | }} 426 | 427 | def generate_uninstall(self): 428 | return { 429 | '/lib/systemd/system/discovery.service': { 430 | 'remove': True 431 | }, 432 | '/usr/sbin/discovery': { 433 | 'remove': True 434 | }, 435 | '/etc/systemd/system/multi-user.target.wants/discovery.service': { 436 | 'remove': True 437 | }} 438 | 439 | def fix_conf_username(self, discovery_conf_contents): 440 | """ 441 | Replace the `User=ubuntu` text in the configuration with the current username. 442 | 443 | @return The modified config file contents 444 | """ 445 | if os.getlogin() == 'ubuntu': 446 | # no changes needed! 447 | return discovery_conf_contents 448 | 449 | return discovery_conf_contents.replace('User=ubuntu', f'User={os.getlogin()}') 450 | -------------------------------------------------------------------------------- /turtlebot4_setup/turtlebot4_setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2023 Clearpath Robotics 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import copy 18 | import os 19 | import subprocess 20 | import shlex 21 | 22 | from turtlebot4_setup.conf import Conf, SystemOptions, BashOptions, WifiOptions, DiscoveryOptions 23 | from turtlebot4_setup.menu import Menu, MenuEntry, OptionsMenu, Prompt, HelpMenu, PreviewMenu 24 | from turtlebot4_setup.ros_setup import RosSetup 25 | from turtlebot4_setup.wifi import WifiSetup 26 | 27 | 28 | __author__ = 'Roni Kreinin' 29 | __email__ = 'rkreinin@clearpathrobotics.com' 30 | __copyright__ = 'Copyright © 2023 Clearpath Robotics. All rights reserved.' 31 | __license__ = 'Apache 2.0' 32 | 33 | 34 | class Turtlebot4Setup(): 35 | # TurtleBot4 Setup -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 36 | title = """ 37 | _____ _ _ ___ _ _ _ ___ _ 38 | |_ _| _ _ _| |_| |___| _ ) ___| |_| | | / __| ___| |_ _ _ _ __ 39 | | || || | '_| _| / -_) _ \\/ _ \\ _|_ _| \\__ \\/ -_) _| || | '_ \\ 40 | |_| \\_,_|_| \\__|_\\___|___/\\___/\\__| |_| |___/\\___|\\__|\\_,_| .__/ 41 | |_| 42 | """ 43 | 44 | def __init__(self) -> None: 45 | self.conf = Conf() 46 | self.initial_conf = copy.deepcopy(self.conf) 47 | self.wifi = WifiSetup(self.conf) 48 | self.ros = RosSetup(self.conf) 49 | self.entries = [MenuEntry(entry='ROS Setup', function=self.ros.show), 50 | MenuEntry(entry='Wi-Fi Setup', function=self.wifi.run), 51 | MenuEntry(entry='Bluetooth Setup', function=self.bluetooth), 52 | MenuEntry('', None), 53 | MenuEntry(entry='View Settings', function=self.view_settings), 54 | MenuEntry(entry='Apply Settings', function=self.apply_settings), 55 | MenuEntry(entry='Reset Create3', function=self.apply_create3), 56 | MenuEntry('', None), 57 | MenuEntry(entry='About', function=self.about), 58 | MenuEntry(entry='Help', function=self.help), 59 | MenuEntry(entry='Exit', function=self.exit)] 60 | self.menu = Menu(self.title, self.entries) 61 | 62 | def bluetooth(self): 63 | subprocess.run(shlex.split('sudo bluetoothctl')) 64 | 65 | def update(self): 66 | o = OptionsMenu('Update TurtleBot4 Packages?.\n', 67 | ['Yes', 'No'], default_option='No') 68 | 69 | if o.show() == 'Yes': 70 | subprocess.run(shlex.split('sudo apt update')) 71 | subprocess.run(shlex.split('sudo apt install ros-jazzy-turtlebot4-setup')) 72 | input() 73 | 74 | def view_settings(self): 75 | PreviewMenu([self.conf.setup_dir, self.conf.netplan_dir]).show() 76 | 77 | def get_settings_diff(self, options): 78 | diff = [] 79 | 80 | if options is SystemOptions or \ 81 | options is BashOptions or \ 82 | options is WifiOptions or \ 83 | options is DiscoveryOptions: 84 | for option in options: 85 | # None and empty string are equivalent 86 | if self.conf.get(option) == '' and self.initial_conf.get(option) == None or \ 87 | self.conf.get(option) == None and self.initial_conf.get(option) == '': 88 | pass 89 | elif (str(self.conf.get(option)) != str(self.initial_conf.get(option))): 90 | diff.append(option) 91 | 92 | return diff 93 | 94 | def settings_diff(self): 95 | text = '' 96 | 97 | diff = self.get_settings_diff(SystemOptions) 98 | if len(diff) > 0: 99 | text += '\nSystem Settings:\n' 100 | for option in diff: 101 | text += ' {0}: {1} -> {2}\n'.format( 102 | option.value, self.initial_conf.get(option), self.conf.get(option)) 103 | 104 | diff = self.get_settings_diff(BashOptions) 105 | if len(diff) > 0: 106 | text += '\nBash Settings:\n' 107 | for option in diff: 108 | text += ' {0}: {1} -> {2}\n'.format( 109 | option.value, self.initial_conf.get(option), self.conf.get(option)) 110 | 111 | diff = self.get_settings_diff(WifiOptions) 112 | if len(diff) > 0: 113 | text += '\nWi-Fi Settings:\n' 114 | for option in diff: 115 | text += ' {0}: {1} -> {2}\n'.format( 116 | option.value, self.initial_conf.get(option), self.conf.get(option)) 117 | 118 | diff = self.get_settings_diff(DiscoveryOptions) 119 | if len(diff) > 0: 120 | text += '\nDiscovery Server Settings:\n' 121 | for option in diff: 122 | text += ' {0}: {1} -> {2}\n'.format( 123 | option.value, self.initial_conf.get(option), self.conf.get(option)) 124 | 125 | if text == '': 126 | text = 'No changes made.\n' 127 | # Apply Settings -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 128 | text = """ 129 | _ _ ___ _ _ _ 130 | /_\\ _ __ _ __| |_ _ / __| ___| |_| |_(_)_ _ __ _ ___ 131 | / _ \\| '_ \\ '_ \\ | || | \\__ \\/ -_) _| _| | ' \\/ _` (_-< 132 | /_/ \\_\\ .__/ .__/_|\\_, | |___/\\___|\\__|\\__|_|_||_\\__, /__/ 133 | |_| |_| |__/ |___/ \n\n""" + text 134 | 135 | text += '\nApply these settings?\n' 136 | 137 | text += '\n**Notes**\n' 138 | text += '- Changes applied to ROS_DOMAIN_ID, ROBOT_NAMESPACE, RMW_IMPLEMENTATION,\n' 139 | text += ' or ROS_DISCOVERY_SERVER will be applied to the Create 3 as well.\n' 140 | text += '- Changes applied to Wi-Fi will cause SSH sessions to hang.\n' 141 | 142 | return text 143 | 144 | def apply_settings(self): 145 | apply_menu = OptionsMenu(self.settings_diff, ['Yes', 'No'], default_option='No') 146 | if apply_menu.show() == 'Yes': 147 | error, msg = self.apply_ros_settings() 148 | if error: 149 | options = OptionsMenu(title='Error: Unable to set Create3 options.' + 150 | 'Please ensure that the Create3 is fully booted and apply again.\n\n Details:\n' + msg, 151 | menu_entries=['Okay']) 152 | options.show() 153 | return 154 | self.apply_wifi_settings() 155 | self.initial_conf = copy.deepcopy(self.conf) 156 | 157 | def apply_ros_settings(self): 158 | reinstall_job = False 159 | update_create3 = False 160 | 161 | # If one of Domain ID, Namespace, or RMW was changed, apply changes to Create 3 162 | for option in self.get_settings_diff(BashOptions): 163 | if option in [BashOptions.DOMAIN_ID, BashOptions.NAMESPACE, BashOptions.RMW]: 164 | update_create3 = True 165 | reinstall_job = True 166 | 167 | if len(self.get_settings_diff(DiscoveryOptions)) > 0: 168 | update_create3 = True 169 | reinstall_job = True 170 | 171 | for option in self.get_settings_diff(SystemOptions): 172 | if option is SystemOptions.MODEL: 173 | reinstall_job = True 174 | 175 | if update_create3: 176 | (error, result) = self.update_create3() 177 | if error: 178 | return (error, result) 179 | 180 | if reinstall_job: 181 | self.ros.robot_upstart_menu.install() 182 | self.ros.robot_upstart_menu.start() 183 | 184 | return (0, "Success") 185 | 186 | def apply_wifi_settings(self): 187 | # Run netplan apply if WiFi options have changed 188 | if len(self.get_settings_diff(WifiOptions)) > 0: 189 | subprocess.run(shlex.split('sudo netplan apply')) 190 | os.system('sudo reboot') 191 | 192 | def create3_diff(self): 193 | # Reset Create3 -- https://patorjk.com/software/taag/#p=display&v=0&f=Small 194 | text = """ 195 | ___ _ ___ _ ____ 196 | | _ \\___ ___ ___| |_ / __|_ _ ___ __ _| |_ ___|__ / 197 | | / -_|_- None: 38 | self.conf = configs 39 | 40 | self.conf.read() 41 | 42 | self.entries = [ 43 | MenuEntry(entry=self.format_entry('Wi-Fi Mode', WifiOptions.WIFI_MODE), 44 | function=self.set_wifi_mode), 45 | MenuEntry(entry=self.format_entry('SSID', WifiOptions.SSID), 46 | function=self.set_ssid), 47 | MenuEntry(entry=self.format_entry('Password', WifiOptions.PASSWORD), 48 | function=self.set_password), 49 | # TODO(rkreinin): Set Reg Domain in 22.04 50 | # MenuEntry(entry=self.format_entry('Regulatory Domain', WifiOptions.REG_DOMAIN), 51 | # function=self.set_reg_domain), 52 | MenuEntry(entry=self.format_entry('Band', WifiOptions.BAND), 53 | function=self.set_band), 54 | MenuEntry(entry=self.format_entry('IP Address', WifiOptions.IP), 55 | function=self.set_ip_address), 56 | MenuEntry(entry=self.format_entry('DHCP', WifiOptions.DHCP), 57 | function=self.set_dhcp), 58 | MenuEntry('', None), 59 | MenuEntry(entry='Apply Defaults', function=self.apply_defaults), 60 | MenuEntry(entry='Save', function=self.save_settings), 61 | ] 62 | 63 | self.menu = Menu(self.title, self.entries) 64 | 65 | def format_entry(self, name, opt: WifiOptions): 66 | return lambda: '{0}{1}[{2}]'.format( 67 | name, 68 | ' ' * (22 - len(name)), 69 | '' if self.conf.get(opt) is None else self.conf.get(opt)) 70 | 71 | def run(self): 72 | self.conf.read() 73 | self.menu.show() 74 | 75 | def apply_defaults(self): 76 | self.conf.apply_default(self.conf.wifi_conf) 77 | 78 | def set_ssid(self): 79 | p = Prompt(prompt='SSID ({0}): '.format(self.conf.get(WifiOptions.SSID)), 80 | default_response=self.conf.get(WifiOptions.SSID), 81 | note='Wi-Fi Network SSID') 82 | self.conf.set(WifiOptions.SSID, p.show()) 83 | 84 | def set_password(self): 85 | p = Prompt(prompt='Password ({0}): '.format(self.conf.get(WifiOptions.PASSWORD)), 86 | default_response=self.conf.get(WifiOptions.PASSWORD), 87 | note='Wi-Fi Network Password') 88 | self.conf.set(WifiOptions.PASSWORD, p.show()) 89 | 90 | def set_reg_domain(self): 91 | p = Prompt(prompt='Regulatory Domain ({0}): '.format(self.conf.get(WifiOptions.REG_DOMAIN)), # noqa: 501 92 | default_response=self.conf.get(WifiOptions.REG_DOMAIN), 93 | note='Wireless regulatory domain. \n' + 94 | 'Common options:\n' + 95 | 'USA: US\nCanada: CA\nUK: GB\n' + 96 | 'Germany: DE\nJapan: JP3\nSpain: ES') 97 | self.conf.set(WifiOptions.REG_DOMAIN, p.show()) 98 | 99 | def set_wifi_mode(self): 100 | options = OptionsMenu(title='Wi-Fi Mode', 101 | menu_entries=['Client', 'Access Point'], 102 | default_option=self.conf.get(WifiOptions.WIFI_MODE)) 103 | self.conf.set(WifiOptions.WIFI_MODE, options.show()) 104 | 105 | def set_band(self): 106 | options = OptionsMenu(title='Band', 107 | menu_entries=['5GHz', '2.4GHz', 'Any'], 108 | default_option=self.conf.get(WifiOptions.BAND)) 109 | self.conf.set(WifiOptions.BAND, options.show()) 110 | 111 | def set_ip_address(self): 112 | p = Prompt(prompt='IP Address ({0}): '.format(self.conf.get(WifiOptions.IP)), 113 | default_response=self.conf.get(WifiOptions.IP), 114 | note='IP Address with CIDR. e.g. 192.168.0.12/24') 115 | self.conf.set(WifiOptions.IP, p.show()) 116 | 117 | def set_dhcp(self): 118 | options = OptionsMenu(title='DHCP', 119 | menu_entries=['True', 'False'], 120 | default_option=self.conf.get(WifiOptions.DHCP)) 121 | self.conf.set(WifiOptions.DHCP, options.show() == 'True') 122 | 123 | def save_settings(self): 124 | self.conf.write() 125 | self.menu.exit() 126 | -------------------------------------------------------------------------------- /udev/50-turtlebot4.rules: -------------------------------------------------------------------------------- 1 | SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK="RPLIDAR", MODE="0666" -------------------------------------------------------------------------------- /udev/60-logitech.rules: -------------------------------------------------------------------------------- 1 | # https://gist.github.com/ulidtko/2599111ce747a4339f246d7c6a89220b 2 | # Linux udev rule file for Logitech F710 gamepads. 3 | # Written by: Max in year 2020. 4 | # SPDX-License-Identifier: MIT 5 | # 6 | # ================================================================================ 7 | # On the gamepad, put the D ↔ X switch into the right, X position ("XInput Mode")! 8 | # ================================================================================ 9 | # 10 | # You can test the gamepad using jstest or jstest-gtk. 11 | # You can test force-feedback (rumble, vibration) using fftest. 12 | # The "vibration" gamepad button toggles force-feedback, independently from PC. 13 | # 14 | # The gamepad works mostly alright -- except axis mapping is borked by default. 15 | # This udev rules file fixes that: 16 | # 17 | # • Ensure you have /bin/jscal tool on your system. 18 | # • Put this file under /etc/udev/rules.d/ and re-plug the usb receiver. 19 | # 20 | # lsusb: ID 046d:c21f Logitech, Inc. F710 Wireless Gamepad [XInput Mode] 21 | 22 | ACTION=="add", SUBSYSTEM=="input", KERNEL=="js?", ATTRS{name}=="Logitech Gamepad F710", \ 23 | RUN+="/bin/jscal --set-mappings 8,0,1,3,4,2,5,16,17,0 /dev/$name" 24 | 25 | #-- NOTE: I'm running this fine with 2 gamepads and mere name-based matching. 26 | #-- Use the two below lines instead if you insist on USB VID:PID pair. 27 | # ACTION=="add", SUBSYSTEM=="input", KERNEL=="js?", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c21f", \ 28 | # RUN+="/bin/jscal --set-mappings 8,0,1,3,4,2,5,16,17,0 /dev/$name" 29 | 30 | #-- To see the complete button/axis map, use: jscal -q /dev/input/js0 31 | #-- Other diagnostic spells: 32 | # udevadm monitor # plug/unplug 33 | # udevadm monitor -a | grep -A20 js0 34 | # udevadm info /dev/input/js0 35 | 36 | #-- Took me freaking 2 hours to get right. Ah yes... games on Linux. -------------------------------------------------------------------------------- /udev/80-movidius.rules: -------------------------------------------------------------------------------- 1 | SUBSYSTEM=="usb", ATTRS{idVendor}=="03e7", MODE="0666" 2 | -------------------------------------------------------------------------------- /udev/99-gpio.rules: -------------------------------------------------------------------------------- 1 | SUBSYSTEM=="bcm2835-gpiomem", KERNEL=="gpiomem", GROUP="dialout", MODE="0660" 2 | SUBSYSTEM=="i2c-dev", KERNEL=="i2c*", GROUP="dialout", MODE="0666" 3 | SUBSYSTEM=="spidev", KERNEL=="spidev*", GROUP="dialout", MODE="0660" 4 | SUBSYSTEM=="gpio", KERNEL=="gpiochip*", GROUP="dialout", MODE="0666" 5 | --------------------------------------------------------------------------------