├── turtlebot4_setup
├── __init__.py
├── wifi.py
├── menu.py
├── turtlebot4_setup
├── conf.py
└── ros_setup.py
├── .gitignore
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── 0-troubleshooting-redirect.yml
│ ├── 2-feature.yml
│ └── 1-bug.yml
├── CODEOWNERS
├── workflows
│ └── ci.yml
└── PULL_REQUEST_TEMPLATE.md
├── udev
├── 80-movidius.rules
├── 50-turtlebot4.rules
├── 99-gpio.rules
└── 60-logitech.rules
├── etc
├── turtlebot4
│ ├── system
│ ├── discovery.sh
│ ├── firmware
│ │ ├── cmdline.txt
│ │ └── config.txt
│ ├── discovery.conf
│ ├── fastdds_rpi.xml
│ ├── setup.bash
│ ├── cyclonedds_rpi.xml
│ ├── aliases.bash
│ ├── chrony.conf
│ └── fastdds_discovery_create3.xml
├── rc.local
├── motd
├── netplan
│ ├── 50-wifis.yaml
│ └── 40-ethernets.yaml
└── systemd
│ └── system
│ └── webserver.service
├── scripts
├── swap_off.sh
├── swap_on.sh
├── create_update.sh
├── jazzy.sh
├── sd_flash.sh
└── turtlebot4_setup.sh
├── boot
└── firmware
│ ├── cmdline.txt
│ └── config.txt
├── debian
├── install
└── postinst
├── install_config
└── logind
│ └── 70-clearpath-standard-logind.conf
├── CONTRIBUTING.md
├── CMakeLists.txt
├── package.xml
├── README.md
├── turtlebot4_discovery
└── configure_discovery.sh
├── CHANGELOG.rst
└── LICENSE
/turtlebot4_setup/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | __pycache__/
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
--------------------------------------------------------------------------------
/udev/80-movidius.rules:
--------------------------------------------------------------------------------
1 | SUBSYSTEM=="usb", ATTRS{idVendor}=="03e7", MODE="0666"
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Default all changes will request review from:
2 | * @roni-kreinin
--------------------------------------------------------------------------------
/etc/turtlebot4/system:
--------------------------------------------------------------------------------
1 | MODEL:lite
2 | VERSION:2.0.2
3 | ROS:Jazzy
4 | HOSTNAME:turtlebot4
--------------------------------------------------------------------------------
/scripts/swap_off.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | sudo swapoff -v /swapfile
4 | sudo rm /swapfile
5 |
--------------------------------------------------------------------------------
/etc/turtlebot4/discovery.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | source /opt/ros/jazzy/setup.bash
3 | fastdds discovery -i 0 -p 11811
4 |
--------------------------------------------------------------------------------
/udev/50-turtlebot4.rules:
--------------------------------------------------------------------------------
1 | SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK="RPLIDAR", MODE="0666"
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/motd:
--------------------------------------------------------------------------------
1 | _____ _ _ _ _ _ _
2 | |_ _| _ _ _| |_| |___| |__ ___| |_| | |
3 | | || || | '_| _| / -_) '_ \/ _ \ _|_ _|
4 | |_| \_,_|_| \__|_\___|_.__/\___/\__| |_|
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/turtlebot4/fastdds_rpi.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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#
--------------------------------------------------------------------------------
/.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/etc/turtlebot4/cyclonedds_rpi.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | true
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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]
--------------------------------------------------------------------------------
/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]
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | Prior to starting make sure your Create® 3 is updated to the `I.*.*` firmware; older versions of the firmware are not compatible with ROS 2 Jazzy.
8 | You can find [firmware updates and instructions on the iRobot website](https://edu.irobot.com/create3-update).
9 |
10 | To access the RaspberryPi on the TurtleBot4 you will need a 2.5mm hex key to remove the top bolts.
11 | The RaspberryPi SD card can be difficult to find. It is located on the side of the RaspberryPi facing the Create3 on the side opposite the USB and Ethernet ports.
12 |
13 | # Create an image manually
14 |
15 | Follow these instructions if you wish to create a Turtlebot4 image manually.
16 |
17 | ## Create an Ubuntu Image
18 |
19 | First install the [Raspberry Pi Imager](https://www.raspberrypi.com/software/) from the website or using the Ubuntu Snap store.
20 |
21 | - Ensure your Raspberry Pi 4 is not powered before removing the flashed SD card.
22 | - Insert your SD card into your PC and run the Raspberry Pi Imager. Follow the instructions to install Ubuntu 24.04 Server (64-bit) onto the SD card.
23 | - If you use this method **you must configure the operating system customizations prior to imaging the SD card.** Please use the following settings:
24 | - Set the default login and password to `ubuntu` / `ubuntu`
25 | - Enable SSH
26 | - Set the default wifi network, password, and country to your home network
27 | - 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.
28 |
29 | ### Ethernet Setup
30 |
31 | - Connect the Raspberry Pi to your Network with an Ethernet cable.
32 | - Boot the Raspberry Pi.
33 | - Find the Raspberry Pi's IP using your router's portal.
34 | - SSH into the Raspberry Pi using the IP address.
35 | ```bash
36 | ssh ubuntu@xxx.xxx.xxx.xxx
37 | ```
38 | - The default login is `ubuntu` and password is `ubuntu`. You will be prompted to change your password.
39 |
40 | ### HDMI Setup
41 |
42 | - Connect a keyboard to the Raspberry Pi via USB.
43 | - Connect a monitor to the Raspberry Pi via the HDMI0 port.
44 | - Boot the Raspberry Pi.
45 | - The default login is `ubuntu` and password is `ubuntu`. You will be prompted to change your password.
46 |
47 | ## Manually configure Wi-Fi
48 |
49 | Once you are logged into the Raspberry Pi, configure the Wi-Fi:
50 |
51 | ```bash
52 | sudo nano /etc/netplan/50-cloud-init.yaml
53 | ```
54 | Add the following lines:
55 | ```bash
56 | wifis:
57 | wlan0:
58 | optional: true
59 | access-points:
60 | "YOUR_WIFI_SSID":
61 | password: "YOUR_WIFI_PASSWORD"
62 | dhcp4: true
63 | ```
64 | Note: Ensure that `wifis:` is aligned with the existing `ethernets:` line. All indentations should be 4 spaces. Do not use tabs.
65 | - Reboot the Raspberry Pi. It should now be connected to your Wi-Fi.
66 | - Find the Raspberry Pi's IP using your router's portal.
67 | - SSH into the Raspberry Pi using the IP address.
68 | ```bash
69 | ssh ubuntu@xxx.xxx.xxx.xxx
70 | ```
71 |
72 | ## Download and run the setup script
73 |
74 | ```
75 | wget -qO - https://raw.githubusercontent.com/turtlebot/turtlebot4_setup/jazzy/scripts/turtlebot4_setup.sh | bash
76 | ```
77 |
78 | 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.
79 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/turtlebot4_setup/wifi.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 | from turtlebot4_setup.conf import Conf, WifiOptions
18 | from turtlebot4_setup.menu import Menu, MenuEntry, OptionsMenu, Prompt
19 |
20 |
21 | __author__ = 'Roni Kreinin'
22 | __email__ = 'rkreinin@clearpathrobotics.com'
23 | __copyright__ = 'Copyright © 2023 Clearpath Robotics. All rights reserved.'
24 | __license__ = 'Apache 2.0'
25 |
26 |
27 | class WifiSetup():
28 | # WiFi Setup -- https://patorjk.com/software/taag/#p=display&v=0&f=Small
29 | title = """
30 | __ ___ ___ _ ___ _
31 | \\ \\ / (_) __(_) / __| ___| |_ _ _ _ __
32 | \\ \\/\\/ /| | _|| | \\__ \\/ -_) _| || | '_ \\
33 | \\_/\\_/ |_|_| |_| |___/\\___|\\__|\\_,_| .__/
34 | |_|
35 | """
36 |
37 | def __init__(self, configs: Conf) -> 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | | / -_|_- -_) _| | (__| '_/ -_) _` | _/ -_)|_ \\
198 | |_|_\\___/__/\\___|\\__| \\___|_| \\___\\__,_|\\__\\___|___/\n\n"""
199 |
200 | text += '\nReset Create3?\n'
201 |
202 | text += '\n**Notes**\n'
203 | text += '- ROS_DOMAIN_ID, ROBOT_NAMESPACE, RMW_IMPLEMENTATION will be\n'
204 | text += ' reapplied to the Create3.'
205 |
206 | return text
207 |
208 | def apply_create3(self):
209 | apply_menu = OptionsMenu(self.create3_diff, ['Yes', 'No'], default_option='No')
210 | if apply_menu.show() == 'Yes':
211 | error, msg = self.update_create3()
212 | if error:
213 | options = OptionsMenu(title='Error: Unable to set Create3 options.' +
214 | 'Please ensure that the Create3 is fully booted and apply again.\n\n Details:\n' + msg,
215 | menu_entries=['Okay'])
216 | options.show()
217 | return
218 |
219 | def update_create3(self):
220 | ros_domain_id = 'ros_domain_id=' + os.environ[BashOptions.DOMAIN_ID]
221 | ros_namespace = '&ros_namespace=' + os.environ[BashOptions.NAMESPACE]
222 | ros_namespace += '/_do_not_use'
223 | rmw_implementation = '&rmw_implementation=' + os.environ[BashOptions.RMW]
224 |
225 | discovery_server = f'&fast_discovery_server_value={self.conf.get_create3_server_str()}'
226 |
227 | create3_rmw_profile = 'config='
228 | if self.conf.get(DiscoveryOptions.ENABLED):
229 | discovery_server_enabled = '&fast_discovery_server_enabled'
230 | create3_rmw_profile_file = os.path.join(self.conf.setup_dir, 'fastdds_discovery_create3.xml')
231 | with open(create3_rmw_profile_file) as f:
232 | create3_rmw_profile += f.read()
233 | else:
234 | discovery_server_enabled = ''
235 |
236 | command = shlex.split(
237 | 'curl -d "{0}{1}{2}{3}{4}"'.format(ros_domain_id,
238 | ros_namespace,
239 | rmw_implementation,
240 | discovery_server,
241 | discovery_server_enabled)) + \
242 | shlex.split('-X POST http://192.168.186.2/ros-config-save-main')
243 |
244 | result = subprocess.run(command, capture_output=True)
245 |
246 | # If the curl command fails then return and do not set any more settings.
247 | if (result.returncode != 0):
248 | return (result.returncode, "Error writing ROS settings to Create3\n\n" + result.stderr.decode("utf-8"))
249 |
250 | # Set create3 rmw profile
251 | command = shlex.split(f'curl -d {shlex.quote(create3_rmw_profile)} -X POST http://192.168.186.2/rmw-profile-override-save')
252 |
253 | result = subprocess.run(command, capture_output=True)
254 |
255 | # If the curl command fails then return and indicate the error.
256 | if (result.returncode != 0):
257 | return (result.returncode, "Error writing RMW XML Profile to Create3\n\n" + result.stderr.decode("utf-8"))
258 |
259 | # Set time syncing to Raspberry PI
260 | config = f'config=server 192.168.186.3 prefer iburst minpoll 4 maxpoll 6 # Use RPi4 server'
261 | command = shlex.split(f'curl -d "{config}" -X POST http://192.168.186.2/beta-ntp-conf-save')
262 |
263 | result = subprocess.run(command, capture_output=True)
264 |
265 | # If the curl command fails then return and indicate the error.
266 | if (result.returncode != 0):
267 | return (result.returncode, "Error writing NTP settings to Create3\n\n" + result.stderr.decode("utf-8"))
268 |
269 | # Reboot the Create3
270 | result = subprocess.run(shlex.split('curl -X POST http://192.168.186.2/api/reboot'), capture_output=True)
271 |
272 | # If the curl command fails then return and indicate the error.
273 | if (result.returncode != 0):
274 | return (result.returncode, "Error requesting Create3 to reboot\n\n" + result.stderr.decode("utf-8"))
275 |
276 | return (0, 'Success')
277 |
278 | def run(self):
279 | self.menu.show()
280 | self.initial_conf.write()
281 |
282 | def exit(self):
283 | self.menu.exit()
284 | self.initial_conf.write()
285 |
286 | def about(self):
287 | self.about_menu = Menu(lambda:
288 | """
289 | TurtleBot 4 Open Source Robotics Platform.
290 |
291 | Model: {0}
292 | Version: {1}
293 | ROS: {2}
294 | Hostname: {3}
295 | IP: {4}
296 | The TurtleBot 4 was created in a partnership between Open Robotics and Clearpath Robotics.
297 | """.format(self.conf.get(SystemOptions.MODEL),
298 | self.conf.get(SystemOptions.VERSION),
299 | self.conf.get(SystemOptions.ROS),
300 | self.conf.get(SystemOptions.HOSTNAME),
301 | self.conf.get(SystemOptions.IP)),
302 | [MenuEntry(entry='Model', function=self.set_model),
303 | MenuEntry(entry='Hostname', function=self.set_hostname),
304 | MenuEntry('', None),
305 | MenuEntry(entry='Save', function=self.save)])
306 | self.about_menu.show()
307 | self.about_menu.refresh_term_menu()
308 |
309 | def set_model(self):
310 | o = OptionsMenu('TurtleBot 4 Model\n',
311 | ['standard', 'lite'],
312 | default_option=self.conf.get(SystemOptions.MODEL))
313 |
314 | self.conf.set(SystemOptions.MODEL, o.show())
315 |
316 | def set_hostname(self):
317 | p = Prompt(prompt='Hostname [{0}]: '.format(self.conf.get(SystemOptions.HOSTNAME)),
318 | default_response=self.conf.get(SystemOptions.HOSTNAME),
319 | note='RPi4 Hostname')
320 | self.conf.set(SystemOptions.HOSTNAME, p.show())
321 |
322 | def save(self):
323 | self.conf.write()
324 | self.about_menu.exit()
325 |
326 | def help(self):
327 | help_menu = HelpMenu(
328 | 'Visit the TurtleBot 4 User Manual for more details on usage. \n' +
329 | 'https://turtlebot.github.io/turtlebot4-user-manual/')
330 | help_menu.show()
331 |
332 |
333 | def main():
334 | setup = Turtlebot4Setup()
335 | setup.run()
336 |
337 |
338 | if __name__ == '__main__':
339 | main()
340 |
--------------------------------------------------------------------------------
/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/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 | | |) | (_- _/ _ \\ V / -_) '_| || | \\__ \\/ -_) '_\\ V / -_) '_|
187 | |___/|_/__/\\__\\___/\\_/\\___|_| \\_, | |___/\\___|_| \\_/\\___|_|
188 | |__/
189 | """
190 |
191 | def __init__(self, configs: Conf) -> 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 |
--------------------------------------------------------------------------------