From f492f802ff3204da9a086486e3a05bf479cd441d Mon Sep 17 00:00:00 2001 From: Stan Baek Date: Wed, 28 Feb 2024 21:46:27 -0700 Subject: [PATCH] Update documentation --- .buildinfo | 8 +- Appendix/GitSetup.html | 1338 +++++----- Appendix/MasterSetup.html | 1538 +++++------ Appendix/RobotSetup.html | 2342 ++++++++-------- Module00.0_IntroductionGIT/IntroGit.html | 679 ----- Module00.1_RobotSetup/RobotSetup.html | 1124 -------- Module00.2_MasterSetup/MasterSetup.html | 759 ------ Module10_FinalProject/FinalProject.html | 1238 ++++----- Module1_ROS/ICE1_ListenerTalker.html | 1604 +++++------ Module1_ROS/ROS.html | 1476 +++++------ Module2_Linux/Linux.html | 1764 ++++++------ Module3_Python3/ICE3_ClientServer.html | 1564 +++++------ Module3_Python3/Python3.html | 1998 +++++++------- Module3_Python3/ROS.html | 1268 ++++----- Module3_Python3/RPSComputer.html | 1194 ++++----- Module4_DrivingTheRobot/DrivingTheRobot.html | 1312 ++++----- .../ICE4_DrivingTheRobot.html | 1330 +++++----- Module5_CustomMessages/CustomMessages.html | 1958 +++++++------- .../Lab1_CustomMessages.html | 1284 ++++----- Module6_IMU/IMU.html | 1402 +++++----- Module6_IMU/Lab2_IMU.html | 1360 +++++----- Module7_LaunchFile/LaunchFile.html | 1572 +++++------ Module8_LIDAR/LIDAR.html | 1499 +++++------ Module8_LIDAR/Lab3_LIDAR.html | 1300 ++++----- Module9_CV/Lab4_ComputerVision.html | 1620 +++++------ Module9_CV/computer_vision.html | 2360 ++++++++--------- .../burger_usbcam_mount.stl | Bin .../turtlebot_controller.py | 114 +- .../mouse_client_OO.py | 216 +- _images/EdgeDet.png | Bin _images/GradEx2.png | Bin _images/GradEx3.png | Bin _images/GradTrig.png | Bin _images/GradientEx.png | Bin _images/Grayscale.JPG | Bin _images/HOG_Features.JPG | Bin _images/HOG_Histogram.JPG | Bin _images/IMU.jpg | Bin _images/LDS02.jpeg | Bin _images/PixelCont.png | Bin _images/RGB.JPG | Bin _images/RGB_Gray.png | Bin _images/RGB_Tuple.JPG | Bin _images/bot.PNG | Bin _images/callibrate.png | Bin _images/callibration.png | Bin _images/camera_mount.jpg | Bin _images/clone.PNG | Bin _images/fan.jpg | Bin _images/firmware.png | Bin 172807 -> 0 bytes _images/installer1.png | Bin _images/installer2.png | Bin _images/installer3.png | Bin _images/installer4.png | Bin _images/lds_small.png | Bin 107096 -> 0 bytes _images/lidarscan_example.png | Bin 0 -> 29179 bytes _images/load.png | Bin _images/mouse_info_topic.png | Bin _images/rectangle.png | Bin _images/rplidar.png | Bin _images/rqt_graph.png | Bin _images/settings-sidebar-ssh-keys.png | Bin _images/ssh-add-ssh-key.png | Bin _images/ssh1.png | Bin _images/ssh11.png | Bin 34573 -> 0 bytes _images/ssh2.png | Bin _images/ssh21.png | Bin 17145 -> 0 bytes _images/ssh3.png | Bin _images/ssh31.png | Bin 11964 -> 0 bytes _images/swap1.png | Bin 14852 -> 0 bytes _images/swap3.png | Bin 10295 -> 0 bytes _images/swap4.png | Bin 20284 -> 0 bytes _images/swap5.png | Bin 12523 -> 0 bytes _images/swap6.png | Bin 12275 -> 0 bytes _images/userbar-account-settings.png | Bin _images/wifi1.png | Bin 34443 -> 0 bytes _images/wifi2.png | Bin 45607 -> 0 bytes _images/wifi3.png | Bin _images/wifi4.png | Bin 35592 -> 0 bytes _images/xml.png | Bin _sources/Appendix/GitSetup.md | 198 +- _sources/Appendix/MasterSetup.md | 430 +-- _sources/Appendix/RobotSetup.md | 1382 +++++----- .../Module00.0_IntroductionGIT/IntroGit.md | 99 - _sources/Module00.1_RobotSetup/RobotSetup.md | 656 ----- .../Module00.2_MasterSetup/MasterSetup.md | 215 -- .../Module10_FinalProject/FinalProject.md | 112 +- _sources/Module1_ROS/ICE1_ListenerTalker.md | 484 ++-- _sources/Module1_ROS/ROS.md | 452 ++-- _sources/Module2_Linux/Linux.md | 782 +++--- _sources/Module3_Python3/ICE3_ClientServer.md | 396 +-- _sources/Module3_Python3/Python3.md | 872 +++--- _sources/Module3_Python3/ROS.md | 220 +- _sources/Module3_Python3/RPSComputer.md | 102 +- .../DrivingTheRobot.md | 158 +- .../ICE4_DrivingTheRobot.md | 234 +- .../Module5_CustomMessages/CustomMessages.md | 732 ++--- .../Lab1_CustomMessages.md | 140 +- _sources/Module6_IMU/IMU.md | 290 +- _sources/Module6_IMU/Lab2_IMU.md | 212 +- _sources/Module7_LaunchFile/LaunchFile.md | 506 ++-- _sources/Module8_LIDAR/LIDAR.md | 385 +-- _sources/Module8_LIDAR/Lab3_LIDAR.md | 136 +- _sources/Module9_CV/Lab4_ComputerVision.md | 480 ++-- _sources/Module9_CV/computer_vision.md | 1378 +++++----- _sources/faq.md | 58 +- _sources/intro.md | 78 +- _sources/python.md | 26 +- _sources/resources.md | 24 - _sources/schedule.md | 98 +- _sources/syllabus.md | 188 +- ...e.4045f2051d55cab465a707391d5b2007.min.css | 2 +- _sphinx_design_static/design-tabs.js | 54 +- .../_sphinx_javascript_frameworks_compat.js | 0 _static/basic.css | 1858 ++++++------- _static/check-solid.svg | 0 _static/clipboard.min.js | 0 _static/copy-button.svg | 0 _static/copybutton.css | 0 _static/copybutton.js | 494 ++-- _static/copybutton_funcs.js | 0 ...e.4045f2051d55cab465a707391d5b2007.min.css | 2 +- _static/design-tabs.js | 54 +- _static/doctools.js | 0 _static/documentation_options.js | 26 +- _static/ece387.png | Bin _static/favicon.ico | Bin _static/file.png | Bin _static/images/logo_binder.svg | 0 _static/images/logo_colab.png | Bin _static/images/logo_deepnote.svg | 0 _static/images/logo_jupyterhub.svg | 0 _static/jquery-3.6.0.js | 0 _static/jquery.js | 0 _static/language_data.js | 398 +-- _static/locales/ar/LC_MESSAGES/booktheme.mo | Bin _static/locales/ar/LC_MESSAGES/booktheme.po | 0 _static/locales/bg/LC_MESSAGES/booktheme.mo | Bin _static/locales/bg/LC_MESSAGES/booktheme.po | 0 _static/locales/bn/LC_MESSAGES/booktheme.mo | Bin _static/locales/bn/LC_MESSAGES/booktheme.po | 0 _static/locales/ca/LC_MESSAGES/booktheme.mo | Bin _static/locales/ca/LC_MESSAGES/booktheme.po | 0 _static/locales/cs/LC_MESSAGES/booktheme.mo | Bin _static/locales/cs/LC_MESSAGES/booktheme.po | 0 _static/locales/da/LC_MESSAGES/booktheme.mo | Bin _static/locales/da/LC_MESSAGES/booktheme.po | 0 _static/locales/de/LC_MESSAGES/booktheme.mo | Bin _static/locales/de/LC_MESSAGES/booktheme.po | 0 _static/locales/el/LC_MESSAGES/booktheme.mo | Bin _static/locales/el/LC_MESSAGES/booktheme.po | 0 _static/locales/eo/LC_MESSAGES/booktheme.mo | Bin _static/locales/eo/LC_MESSAGES/booktheme.po | 0 _static/locales/es/LC_MESSAGES/booktheme.mo | Bin _static/locales/es/LC_MESSAGES/booktheme.po | 0 _static/locales/et/LC_MESSAGES/booktheme.mo | Bin _static/locales/et/LC_MESSAGES/booktheme.po | 0 _static/locales/fi/LC_MESSAGES/booktheme.mo | Bin _static/locales/fi/LC_MESSAGES/booktheme.po | 0 _static/locales/fr/LC_MESSAGES/booktheme.mo | Bin _static/locales/fr/LC_MESSAGES/booktheme.po | 0 _static/locales/hr/LC_MESSAGES/booktheme.mo | Bin _static/locales/hr/LC_MESSAGES/booktheme.po | 0 _static/locales/id/LC_MESSAGES/booktheme.mo | Bin _static/locales/id/LC_MESSAGES/booktheme.po | 0 _static/locales/it/LC_MESSAGES/booktheme.mo | Bin _static/locales/it/LC_MESSAGES/booktheme.po | 0 _static/locales/iw/LC_MESSAGES/booktheme.mo | Bin _static/locales/iw/LC_MESSAGES/booktheme.po | 0 _static/locales/ja/LC_MESSAGES/booktheme.mo | Bin _static/locales/ja/LC_MESSAGES/booktheme.po | 0 _static/locales/ko/LC_MESSAGES/booktheme.mo | Bin _static/locales/ko/LC_MESSAGES/booktheme.po | 0 _static/locales/lt/LC_MESSAGES/booktheme.mo | Bin _static/locales/lt/LC_MESSAGES/booktheme.po | 0 _static/locales/lv/LC_MESSAGES/booktheme.mo | Bin _static/locales/lv/LC_MESSAGES/booktheme.po | 0 _static/locales/ml/LC_MESSAGES/booktheme.mo | Bin _static/locales/ml/LC_MESSAGES/booktheme.po | 0 _static/locales/mr/LC_MESSAGES/booktheme.mo | Bin _static/locales/mr/LC_MESSAGES/booktheme.po | 0 _static/locales/ms/LC_MESSAGES/booktheme.mo | Bin _static/locales/ms/LC_MESSAGES/booktheme.po | 0 _static/locales/nl/LC_MESSAGES/booktheme.mo | Bin _static/locales/nl/LC_MESSAGES/booktheme.po | 0 _static/locales/no/LC_MESSAGES/booktheme.mo | Bin _static/locales/no/LC_MESSAGES/booktheme.po | 0 _static/locales/pl/LC_MESSAGES/booktheme.mo | Bin _static/locales/pl/LC_MESSAGES/booktheme.po | 0 _static/locales/pt/LC_MESSAGES/booktheme.mo | Bin _static/locales/pt/LC_MESSAGES/booktheme.po | 0 _static/locales/ro/LC_MESSAGES/booktheme.mo | Bin _static/locales/ro/LC_MESSAGES/booktheme.po | 0 _static/locales/ru/LC_MESSAGES/booktheme.mo | Bin _static/locales/ru/LC_MESSAGES/booktheme.po | 0 _static/locales/sk/LC_MESSAGES/booktheme.mo | Bin _static/locales/sk/LC_MESSAGES/booktheme.po | 0 _static/locales/sl/LC_MESSAGES/booktheme.mo | Bin _static/locales/sl/LC_MESSAGES/booktheme.po | 0 _static/locales/sr/LC_MESSAGES/booktheme.mo | Bin _static/locales/sr/LC_MESSAGES/booktheme.po | 0 _static/locales/sv/LC_MESSAGES/booktheme.mo | Bin _static/locales/sv/LC_MESSAGES/booktheme.po | 0 _static/locales/ta/LC_MESSAGES/booktheme.mo | Bin _static/locales/ta/LC_MESSAGES/booktheme.po | 0 _static/locales/te/LC_MESSAGES/booktheme.mo | Bin _static/locales/te/LC_MESSAGES/booktheme.po | 0 _static/locales/tg/LC_MESSAGES/booktheme.mo | Bin _static/locales/tg/LC_MESSAGES/booktheme.po | 0 _static/locales/th/LC_MESSAGES/booktheme.mo | Bin _static/locales/th/LC_MESSAGES/booktheme.po | 0 _static/locales/tl/LC_MESSAGES/booktheme.mo | Bin _static/locales/tl/LC_MESSAGES/booktheme.po | 0 _static/locales/tr/LC_MESSAGES/booktheme.mo | Bin _static/locales/tr/LC_MESSAGES/booktheme.po | 0 _static/locales/uk/LC_MESSAGES/booktheme.mo | Bin _static/locales/uk/LC_MESSAGES/booktheme.po | 0 _static/locales/ur/LC_MESSAGES/booktheme.mo | Bin _static/locales/ur/LC_MESSAGES/booktheme.po | 0 _static/locales/vi/LC_MESSAGES/booktheme.mo | Bin _static/locales/vi/LC_MESSAGES/booktheme.po | 0 .../locales/zh_CN/LC_MESSAGES/booktheme.mo | Bin .../locales/zh_CN/LC_MESSAGES/booktheme.po | 0 .../locales/zh_TW/LC_MESSAGES/booktheme.mo | Bin .../locales/zh_TW/LC_MESSAGES/booktheme.po | 0 _static/minus.png | Bin ...69c37c29e427902b24a333a5f9fcb2f0b3ac41.css | 0 _static/plus.png | Bin _static/pygments.css | 302 +-- _static/sbt-webpack-macros.html | 0 _static/scripts/bootstrap.js | 0 _static/scripts/bootstrap.js.LICENSE.txt | 0 _static/scripts/bootstrap.js.map | 0 _static/scripts/pydata-sphinx-theme.js | 0 _static/scripts/pydata-sphinx-theme.js.map | 0 _static/scripts/sphinx-book-theme.js | 0 _static/scripts/sphinx-book-theme.js.map | 0 _static/searchtools.js | 0 _static/sphinx-thebe.css | 0 _static/sphinx-thebe.js | 0 _static/styles/bootstrap.css | 0 _static/styles/pydata-sphinx-theme.css | 0 _static/styles/sphinx-book-theme.css | 0 _static/styles/theme.css | 0 _static/tabs.css | 0 _static/tabs.js | 9 + _static/togglebutton.css | 0 _static/togglebutton.js | 0 _static/underscore-1.13.1.js | 0 _static/underscore.js | 0 _static/vendor/fontawesome/6.1.2/LICENSE.txt | 0 .../vendor/fontawesome/6.1.2/css/all.min.css | 0 .../6.1.2/webfonts/fa-brands-400.ttf | Bin .../6.1.2/webfonts/fa-brands-400.woff2 | Bin .../6.1.2/webfonts/fa-regular-400.ttf | Bin .../6.1.2/webfonts/fa-regular-400.woff2 | Bin .../6.1.2/webfonts/fa-solid-900.ttf | Bin .../6.1.2/webfonts/fa-solid-900.woff2 | Bin .../6.1.2/webfonts/fa-v4compatibility.ttf | Bin .../6.1.2/webfonts/fa-v4compatibility.woff2 | Bin _static/webpack-macros.html | 0 faq.html | 1144 ++++---- genindex.html | 886 +++---- index.html | 2 +- intro.html | 1224 ++++----- objects.inv | Bin python.html | 1150 ++++---- resources.html | 586 ---- schedule.html | 1406 +++++----- search.html | 910 +++---- searchindex.js | 2 +- syllabus.html | 1476 +++++------ 272 files changed, 28300 insertions(+), 32429 deletions(-) mode change 100755 => 100644 .buildinfo mode change 100755 => 100644 Appendix/GitSetup.html mode change 100755 => 100644 Appendix/MasterSetup.html mode change 100755 => 100644 Appendix/RobotSetup.html delete mode 100755 Module00.0_IntroductionGIT/IntroGit.html delete mode 100755 Module00.1_RobotSetup/RobotSetup.html delete mode 100755 Module00.2_MasterSetup/MasterSetup.html mode change 100755 => 100644 Module10_FinalProject/FinalProject.html mode change 100755 => 100644 Module1_ROS/ICE1_ListenerTalker.html mode change 100755 => 100644 Module1_ROS/ROS.html mode change 100755 => 100644 Module2_Linux/Linux.html mode change 100755 => 100644 Module3_Python3/ICE3_ClientServer.html mode change 100755 => 100644 Module3_Python3/Python3.html mode change 100755 => 100644 Module3_Python3/ROS.html mode change 100755 => 100644 Module3_Python3/RPSComputer.html mode change 100755 => 100644 Module4_DrivingTheRobot/DrivingTheRobot.html mode change 100755 => 100644 Module4_DrivingTheRobot/ICE4_DrivingTheRobot.html mode change 100755 => 100644 Module5_CustomMessages/CustomMessages.html mode change 100755 => 100644 Module5_CustomMessages/Lab1_CustomMessages.html mode change 100755 => 100644 Module6_IMU/IMU.html mode change 100755 => 100644 Module6_IMU/Lab2_IMU.html mode change 100755 => 100644 Module7_LaunchFile/LaunchFile.html mode change 100755 => 100644 Module8_LIDAR/LIDAR.html mode change 100755 => 100644 Module8_LIDAR/Lab3_LIDAR.html mode change 100755 => 100644 Module9_CV/Lab4_ComputerVision.html mode change 100755 => 100644 Module9_CV/computer_vision.html mode change 100755 => 100644 _downloads/3a3cd348ff4a2bc81ddea473f4ae906f/burger_usbcam_mount.stl mode change 100755 => 100644 _downloads/6fe569f9320a6bd641887792fd967ce2/turtlebot_controller.py mode change 100755 => 100644 _downloads/cb5daeab4a0e775f3d6d21655764a2e9/mouse_client_OO.py mode change 100755 => 100644 _images/EdgeDet.png mode change 100755 => 100644 _images/GradEx2.png mode change 100755 => 100644 _images/GradEx3.png mode change 100755 => 100644 _images/GradTrig.png mode change 100755 => 100644 _images/GradientEx.png mode change 100755 => 100644 _images/Grayscale.JPG mode change 100755 => 100644 _images/HOG_Features.JPG mode change 100755 => 100644 _images/HOG_Histogram.JPG mode change 100755 => 100644 _images/IMU.jpg mode change 100755 => 100644 _images/LDS02.jpeg mode change 100755 => 100644 _images/PixelCont.png mode change 100755 => 100644 _images/RGB.JPG mode change 100755 => 100644 _images/RGB_Gray.png mode change 100755 => 100644 _images/RGB_Tuple.JPG mode change 100755 => 100644 _images/bot.PNG mode change 100755 => 100644 _images/callibrate.png mode change 100755 => 100644 _images/callibration.png mode change 100755 => 100644 _images/camera_mount.jpg mode change 100755 => 100644 _images/clone.PNG mode change 100755 => 100644 _images/fan.jpg delete mode 100755 _images/firmware.png mode change 100755 => 100644 _images/installer1.png mode change 100755 => 100644 _images/installer2.png mode change 100755 => 100644 _images/installer3.png mode change 100755 => 100644 _images/installer4.png delete mode 100755 _images/lds_small.png create mode 100644 _images/lidarscan_example.png mode change 100755 => 100644 _images/load.png mode change 100755 => 100644 _images/mouse_info_topic.png mode change 100755 => 100644 _images/rectangle.png mode change 100755 => 100644 _images/rplidar.png mode change 100755 => 100644 _images/rqt_graph.png mode change 100755 => 100644 _images/settings-sidebar-ssh-keys.png mode change 100755 => 100644 _images/ssh-add-ssh-key.png mode change 100755 => 100644 _images/ssh1.png delete mode 100755 _images/ssh11.png mode change 100755 => 100644 _images/ssh2.png delete mode 100755 _images/ssh21.png mode change 100755 => 100644 _images/ssh3.png delete mode 100755 _images/ssh31.png delete mode 100755 _images/swap1.png delete mode 100755 _images/swap3.png delete mode 100755 _images/swap4.png delete mode 100755 _images/swap5.png delete mode 100755 _images/swap6.png mode change 100755 => 100644 _images/userbar-account-settings.png delete mode 100755 _images/wifi1.png delete mode 100755 _images/wifi2.png mode change 100755 => 100644 _images/wifi3.png delete mode 100755 _images/wifi4.png mode change 100755 => 100644 _images/xml.png mode change 100755 => 100644 _sources/Appendix/GitSetup.md mode change 100755 => 100644 _sources/Appendix/MasterSetup.md mode change 100755 => 100644 _sources/Appendix/RobotSetup.md delete mode 100755 _sources/Module00.0_IntroductionGIT/IntroGit.md delete mode 100755 _sources/Module00.1_RobotSetup/RobotSetup.md delete mode 100755 _sources/Module00.2_MasterSetup/MasterSetup.md mode change 100755 => 100644 _sources/Module10_FinalProject/FinalProject.md mode change 100755 => 100644 _sources/Module1_ROS/ICE1_ListenerTalker.md mode change 100755 => 100644 _sources/Module1_ROS/ROS.md mode change 100755 => 100644 _sources/Module2_Linux/Linux.md mode change 100755 => 100644 _sources/Module3_Python3/ICE3_ClientServer.md mode change 100755 => 100644 _sources/Module3_Python3/Python3.md mode change 100755 => 100644 _sources/Module3_Python3/ROS.md mode change 100755 => 100644 _sources/Module3_Python3/RPSComputer.md mode change 100755 => 100644 _sources/Module4_DrivingTheRobot/DrivingTheRobot.md mode change 100755 => 100644 _sources/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.md mode change 100755 => 100644 _sources/Module5_CustomMessages/CustomMessages.md mode change 100755 => 100644 _sources/Module5_CustomMessages/Lab1_CustomMessages.md mode change 100755 => 100644 _sources/Module6_IMU/IMU.md mode change 100755 => 100644 _sources/Module6_IMU/Lab2_IMU.md mode change 100755 => 100644 _sources/Module7_LaunchFile/LaunchFile.md mode change 100755 => 100644 _sources/Module8_LIDAR/LIDAR.md mode change 100755 => 100644 _sources/Module8_LIDAR/Lab3_LIDAR.md mode change 100755 => 100644 _sources/Module9_CV/Lab4_ComputerVision.md mode change 100755 => 100644 _sources/Module9_CV/computer_vision.md mode change 100755 => 100644 _sources/faq.md mode change 100755 => 100644 _sources/intro.md mode change 100755 => 100644 _sources/python.md delete mode 100755 _sources/resources.md mode change 100755 => 100644 _sources/schedule.md mode change 100755 => 100644 _sources/syllabus.md mode change 100755 => 100644 _sphinx_design_static/design-style.4045f2051d55cab465a707391d5b2007.min.css mode change 100755 => 100644 _sphinx_design_static/design-tabs.js mode change 100755 => 100644 _static/_sphinx_javascript_frameworks_compat.js mode change 100755 => 100644 _static/basic.css mode change 100755 => 100644 _static/check-solid.svg mode change 100755 => 100644 _static/clipboard.min.js mode change 100755 => 100644 _static/copy-button.svg mode change 100755 => 100644 _static/copybutton.css mode change 100755 => 100644 _static/copybutton.js mode change 100755 => 100644 _static/copybutton_funcs.js mode change 100755 => 100644 _static/design-style.4045f2051d55cab465a707391d5b2007.min.css mode change 100755 => 100644 _static/design-tabs.js mode change 100755 => 100644 _static/doctools.js mode change 100755 => 100644 _static/documentation_options.js mode change 100755 => 100644 _static/ece387.png mode change 100755 => 100644 _static/favicon.ico mode change 100755 => 100644 _static/file.png mode change 100755 => 100644 _static/images/logo_binder.svg mode change 100755 => 100644 _static/images/logo_colab.png mode change 100755 => 100644 _static/images/logo_deepnote.svg mode change 100755 => 100644 _static/images/logo_jupyterhub.svg mode change 100755 => 100644 _static/jquery-3.6.0.js mode change 100755 => 100644 _static/jquery.js mode change 100755 => 100644 _static/language_data.js mode change 100755 => 100644 _static/locales/ar/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ar/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/bg/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/bg/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/bn/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/bn/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ca/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ca/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/cs/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/cs/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/da/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/da/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/de/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/de/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/el/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/el/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/eo/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/eo/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/es/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/es/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/et/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/et/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/fi/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/fi/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/fr/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/fr/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/hr/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/hr/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/id/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/id/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/it/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/it/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/iw/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/iw/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ja/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ja/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ko/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ko/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/lt/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/lt/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/lv/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/lv/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ml/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ml/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/mr/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/mr/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ms/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ms/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/nl/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/nl/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/no/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/no/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/pl/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/pl/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/pt/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/pt/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ro/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ro/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ru/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ru/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/sk/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/sk/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/sl/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/sl/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/sr/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/sr/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/sv/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/sv/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ta/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ta/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/te/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/te/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/tg/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/tg/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/th/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/th/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/tl/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/tl/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/tr/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/tr/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/uk/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/uk/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/ur/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/ur/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/vi/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/vi/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/zh_CN/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/zh_CN/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/locales/zh_TW/LC_MESSAGES/booktheme.mo mode change 100755 => 100644 _static/locales/zh_TW/LC_MESSAGES/booktheme.po mode change 100755 => 100644 _static/minus.png mode change 100755 => 100644 _static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css mode change 100755 => 100644 _static/plus.png mode change 100755 => 100644 _static/pygments.css mode change 100755 => 100644 _static/sbt-webpack-macros.html mode change 100755 => 100644 _static/scripts/bootstrap.js mode change 100755 => 100644 _static/scripts/bootstrap.js.LICENSE.txt mode change 100755 => 100644 _static/scripts/bootstrap.js.map mode change 100755 => 100644 _static/scripts/pydata-sphinx-theme.js mode change 100755 => 100644 _static/scripts/pydata-sphinx-theme.js.map mode change 100755 => 100644 _static/scripts/sphinx-book-theme.js mode change 100755 => 100644 _static/scripts/sphinx-book-theme.js.map mode change 100755 => 100644 _static/searchtools.js mode change 100755 => 100644 _static/sphinx-thebe.css mode change 100755 => 100644 _static/sphinx-thebe.js mode change 100755 => 100644 _static/styles/bootstrap.css mode change 100755 => 100644 _static/styles/pydata-sphinx-theme.css mode change 100755 => 100644 _static/styles/sphinx-book-theme.css mode change 100755 => 100644 _static/styles/theme.css mode change 100755 => 100644 _static/tabs.css mode change 100755 => 100644 _static/tabs.js mode change 100755 => 100644 _static/togglebutton.css mode change 100755 => 100644 _static/togglebutton.js mode change 100755 => 100644 _static/underscore-1.13.1.js mode change 100755 => 100644 _static/underscore.js mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/LICENSE.txt mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/css/all.min.css mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/webfonts/fa-brands-400.ttf mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/webfonts/fa-brands-400.woff2 mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/webfonts/fa-regular-400.ttf mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/webfonts/fa-regular-400.woff2 mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/webfonts/fa-solid-900.ttf mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/webfonts/fa-solid-900.woff2 mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/webfonts/fa-v4compatibility.ttf mode change 100755 => 100644 _static/vendor/fontawesome/6.1.2/webfonts/fa-v4compatibility.woff2 mode change 100755 => 100644 _static/webpack-macros.html mode change 100755 => 100644 faq.html mode change 100755 => 100644 genindex.html mode change 100755 => 100644 index.html mode change 100755 => 100644 intro.html mode change 100755 => 100644 objects.inv mode change 100755 => 100644 python.html delete mode 100755 resources.html mode change 100755 => 100644 schedule.html mode change 100755 => 100644 search.html mode change 100755 => 100644 searchindex.js mode change 100755 => 100644 syllabus.html diff --git a/.buildinfo b/.buildinfo old mode 100755 new mode 100644 index 16b71e5..817328b --- a/.buildinfo +++ b/.buildinfo @@ -1,4 +1,4 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: f7fac6807dbc9a08f49d5b4b63bc1b9d -tags: 645f666f9bcd5a90fca523b33c5a78b7 +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 8e797e08234635f9396beb70db152ca4 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/Appendix/GitSetup.html b/Appendix/GitSetup.html old mode 100755 new mode 100644 index 5b39eea..bd3eac2 --- a/Appendix/GitSetup.html +++ b/Appendix/GitSetup.html @@ -1,670 +1,670 @@ - - - - - - - - - - - - Module 0: Setting Up GIT Repositories — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Module 0: Setting Up GIT Repositories#

-
-

Purpose#

-

Setup GIT repositories and access on the Master.

-
-
-

Create a repo within the GitHub Classroom:#

-
    -
  1. Browse to github.com and create a GitHub account if you do not already have one. It is useful if your username is something that identifies you (e.g., bneff1013).

  2. -
  3. One student per group do the following on your personal computer:

    -
      -
    1. First we are going to setup the repo for the Master. This will allow your instructor to see all of your commits throughout the semester.

    2. -
    3. Browse to https://classroom.github.com/a/OoH0u_XW

    4. -
    5. Select “Accept this assignment”

    6. -
    7. You may need to hit refresh, but eventually it will provide you a link to the repository.

    8. -
    9. Browse to your repository.

    10. -
    11. Note the url for your repository (save this link, it is the best way to check if your repo is updated).

    12. -
    13. Go to Settings -> Manage access -> and “Invite teams or people”.

    14. -
    15. Provide access to your team member using their GitHub user name.

    16. -
    17. Now we need to do the exact same thing to setup the repo for the robot.

    18. -
    19. Browse to https://classroom.github.com/a/0MPq4TNE and repeat steps c-h.

    20. -
    -
  4. -
-
-
-

Enable SSH connection to your GitHub account#

-
    -
  1. Open a terminal on your Master (ctrl+alt+t).

  2. -
  3. The same student as step 1.1.2 do the following:

    -
      -
    1. Generate a new SSH key, substituting your GitHub email address:

      -
      ssh-keygen -t ed25519 -C "your_email@example.com"
      -
      -
      -
    2. -
    3. When you’re prompted to “Enter a file in which to save the key,” click enter.

    4. -
    5. At the prompt, type a secure passphrase.

    6. -
    7. Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

      -
      eval "$(ssh-agent -s)"
      -ssh-add ~/.ssh/id_ed25519
      -
      -
      -
    8. -
    9. Open the public key:

      -
      nano ~/.ssh/id_ed25519.pub
      -
      -
      -
    10. -
    11. Select the contents of the file (maximize the window and ensure it has your GIT email at the end), right click, and select copy.

    12. -
    13. Open a web browser and sign in to your GitHub account.

    14. -
    15. In the upper-right corner of any page, click your profile photo, then click Settings. -logo

    16. -
    17. In the user settings sidebar, click SSH and GPG keys. -logo

    18. -
    19. Click New SSH key -logo

    20. -
    21. In the “Title” field, add a descriptive label for the new key, such as “MasterX”.

    22. -
    23. Paste your key into the “Key” field (contents of the .pub file).

    24. -
    25. Click Add SSH key.

    26. -
    27. If prompted, confirm your GitHub password.

    28. -
    29. Create a secure shell connection to your Robot (password is dfec3141)

      -
      ssh pi@robotX
      -
      -
      -
    30. -
    31. Repeat steps a-f on your Robot and j-n on your Master.

    32. -
    -
  4. -
-
-
-

Clone repository to your master.#

-
    -
  1. On the Master, open the GitHub repository and copy your repo address using the SSH mode: -logo

  2. -
  3. Open a terminal and browse to your workspace source folder:

    -
    cd ~/master_ws/src/
    -
    -
    -
  4. -
  5. Clone your repo using the username and password used when you generated the SSH key, replacing USERNAME with your GitHub username:

    -
    git clone git@github.com:ECE387/ece387_master_spring202X-USERNAME.git
    -
    -
    -
  6. -
  7. Update your git email address and the last name for you and your team mate.

    -
    git config --global user.email "you@example.com"
    -git config --global user.name "Lastname1 Lastname2"
    -
    -
    -
  8. -
-
-
-

Clone repository to your robot.#

-
    -
  1. Create a secure shell connection to your robot:

    -
    ssh pi@robotX
    -
    -
    -
  2. -
  3. Ensure you are in the ROS robot workspace src directory.

    -
    cd robot_ws/src
    -
    -
    -
  4. -
  5. Clone the robot repository:

    -
    git clone git@github.com:ECE387/ece387_robot_spring202X-USERNAME.git
    -
    -
    -
  6. -
  7. Update your git email address and the last name for you and your team mate.

    -
    git config --global user.email "you@example.com"
    -git config --global user.name "Lastname1 Lastname2"
    -
    -
    -
  8. -
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Module 0: Setting Up GIT Repositories — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Module 0: Setting Up GIT Repositories#

+
+

Purpose#

+

Setup GIT repositories and access on the Master.

+
+
+

Create a repo within the GitHub Classroom:#

+
    +
  1. Browse to github.com and create a GitHub account if you do not already have one. It is useful if your username is something that identifies you (e.g., bneff1013).

  2. +
  3. One student per group do the following on your personal computer:

    +
      +
    1. First we are going to setup the repo for the Master. This will allow your instructor to see all of your commits throughout the semester.

    2. +
    3. Browse to https://classroom.github.com/a/OoH0u_XW

    4. +
    5. Select “Accept this assignment”

    6. +
    7. You may need to hit refresh, but eventually it will provide you a link to the repository.

    8. +
    9. Browse to your repository.

    10. +
    11. Note the url for your repository (save this link, it is the best way to check if your repo is updated).

    12. +
    13. Go to Settings -> Manage access -> and “Invite teams or people”.

    14. +
    15. Provide access to your team member using their GitHub user name.

    16. +
    17. Now we need to do the exact same thing to setup the repo for the robot.

    18. +
    19. Browse to https://classroom.github.com/a/0MPq4TNE and repeat steps c-h.

    20. +
    +
  4. +
+
+
+

Enable SSH connection to your GitHub account#

+
    +
  1. Open a terminal on your Master (ctrl+alt+t).

  2. +
  3. The same student as step 1.1.2 do the following:

    +
      +
    1. Generate a new SSH key, substituting your GitHub email address:

      +
      ssh-keygen -t ed25519 -C "your_email@example.com"
      +
      +
      +
    2. +
    3. When you’re prompted to “Enter a file in which to save the key,” click enter.

    4. +
    5. At the prompt, type a secure passphrase.

    6. +
    7. Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

      +
      eval "$(ssh-agent -s)"
      +ssh-add ~/.ssh/id_ed25519
      +
      +
      +
    8. +
    9. Open the public key:

      +
      nano ~/.ssh/id_ed25519.pub
      +
      +
      +
    10. +
    11. Select the contents of the file (maximize the window and ensure it has your GIT email at the end), right click, and select copy.

    12. +
    13. Open a web browser and sign in to your GitHub account.

    14. +
    15. In the upper-right corner of any page, click your profile photo, then click Settings. +logo

    16. +
    17. In the user settings sidebar, click SSH and GPG keys. +logo

    18. +
    19. Click New SSH key +logo

    20. +
    21. In the “Title” field, add a descriptive label for the new key, such as “MasterX”.

    22. +
    23. Paste your key into the “Key” field (contents of the .pub file).

    24. +
    25. Click Add SSH key.

    26. +
    27. If prompted, confirm your GitHub password.

    28. +
    29. Create a secure shell connection to your Robot (password is dfec3141)

      +
      ssh pi@robotX
      +
      +
      +
    30. +
    31. Repeat steps a-f on your Robot and j-n on your Master.

    32. +
    +
  4. +
+
+
+

Clone repository to your master.#

+
    +
  1. On the Master, open the GitHub repository and copy your repo address using the SSH mode: +logo

  2. +
  3. Open a terminal and browse to your workspace source folder:

    +
    cd ~/master_ws/src/
    +
    +
    +
  4. +
  5. Clone your repo using the username and password used when you generated the SSH key, replacing USERNAME with your GitHub username:

    +
    git clone git@github.com:ECE387/ece387_master_spring202X-USERNAME.git
    +
    +
    +
  6. +
  7. Update your git email address and the last name for you and your team mate.

    +
    git config --global user.email "you@example.com"
    +git config --global user.name "Lastname1 Lastname2"
    +
    +
    +
  8. +
+
+
+

Clone repository to your robot.#

+
    +
  1. Create a secure shell connection to your robot:

    +
    ssh pi@robotX
    +
    +
    +
  2. +
  3. Ensure you are in the ROS robot workspace src directory.

    +
    cd robot_ws/src
    +
    +
    +
  4. +
  5. Clone the robot repository:

    +
    git clone git@github.com:ECE387/ece387_robot_spring202X-USERNAME.git
    +
    +
    +
  6. +
  7. Update your git email address and the last name for you and your team mate.

    +
    git config --global user.email "you@example.com"
    +git config --global user.name "Lastname1 Lastname2"
    +
    +
    +
  8. +
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Appendix/MasterSetup.html b/Appendix/MasterSetup.html old mode 100755 new mode 100644 index 3b0f160..5906354 --- a/Appendix/MasterSetup.html +++ b/Appendix/MasterSetup.html @@ -1,770 +1,770 @@ - - - - - - - - - - - - Master Setup — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Master Setup#

-

This guide will walk through the steps to install Ubuntu Desktop 20.04 LTS, ROS Noetic, and all dependencies on a desktop computer. This computer system is utilized in the United States Air Force Academy’s Electrical and Computer Engineering department in an embedded network with the ground robot, a TurtleBot3 Burger. The master system is used to host roscore, utilize ROS GUI tools, and create secure connections with the TurtleBot3. This guide is adapted from the TurtleBot3 e-Manual.

-
-
-

Hardware#

-

For our application, we are using Intel NUC Kits but these instructions will work on any AMD64 architecture.

-
-
-

Software#

-
-

Download Ubuntu and flash USB#

-

For the desktop machine you will first need to download Ubuntu Desktop 20.04 LTS.

-

Once downloaded, follow the instructions to create a bootable Ubuntu USB stick within Ubuntu. The guide provides links to create USB sticks from Windows and macOS as well.

-

Once the bootable USB stick is created, follow the guide to Install Ubuntu desktop selecting a useful computer name such as master0. The NUC requires you to press and hold F10 on startup to boot from a USB stick.

- -
-

Setup GitHub SSH Keys#

-

The following assumes you already have a GitHub account.

-

Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login:

-
cd
-ssh-keygen -t ed25519 -C "github@email.com"
-
-
-

When prompted to “Enter a file in which to save the key”, hit enter.

-

At the prompt, type a secure password.

-

Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

-
eval "$(ssh-agent -s)"
-ssh-add ~/.ssh/id_ed25519
-
-
-

Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account).

-
nano ~/.ssh/id_ed25519.pub
-
-
-

Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email).

-

Open a web browser and sign in to your GitHub account.

-

In the upper-right corner of any page, click your profile photo, then click Settings:

-

logo

-

In the user settings sidebar, click SSH and GPG keys:

-

logo

-

Click New SSH key:

-

logo

-

In the “Title” field, add a descriptive label for the new key, such as “master0”.

-

Paste your key into the “Key” field (contents of the .pub file).

-

Click Add SSH key.

- - -
-
-

Update Alternatives#

-

Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the “python” command instead of “python3” so we need to create a new executable, “/usr/bin/python” that will call Python3 (basically use the command “python” to call Python3):

-
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10
-
-
-
-
-

Additional Software#

-
sudo apt install jupyter-notebook
-jupyter contrib nbextension install --user
-
-
- - -
-
-
-

ROS Noetic#

-

At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the master. All of these instructions are adapted from the ROS wiki. ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal.

-
-

Installation#

-

Accept software from packages.ros.org:

-
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
-
-
-

Set up keys:

-
sudo apt install curl # if you haven't already installed curl
-curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
-
-
-

Install ROS Noetic:

-
sudo apt update
-sudo apt -y install ros-noetic-desktop-full
-
-
-

The full version provides theminimum packaging, build, communications libraries, GUI tools, and 2D/3D simulation and perception packages.

-

Install ROS dependencies for building packages:

-
sudo apt -y install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip xterm build-essential
-
-
-

Initialize rosdep

-
sudo rosdep init
-rosdep update
-
-
-

Source the ROS setup file:

-
source /opt/ros/noetic/setup.bash
-
-
-

Create your ROS workspace:

-
mkdir -p ~/master_ws/src
-cd ~/master_ws/
-catkin_make
-
-
-

Setup ROS environment variables and setup scripts within the ~/.bashrc file. Open the ~/.bashrc file with your favorite command line editor and add the following to the bottom:

-
source /opt/ros/noetic/setup.bash
-source ~/master_ws/devel/setup.bash
-export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share
-export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes
-export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master
-export EDITOR='nano -w' # replace with editor of choice used with rosed command
-export TURTLEBOT3_MODEL=burger
-export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR
-
-
-

Any time you make changes to your ~/.bashrc file you must source it:

-
source ~/.bashrc
-
-
-
-
-

Dependencies#

-

There are a number of ROS packages required to operate the TurtleBot3.

-
-
ROS Dependencies#
-
sudo apt-get install ros-noetic-joy ros-noetic-teleop-twist-joy \
-  ros-noetic-teleop-twist-keyboard ros-noetic-laser-proc \
-  ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \
-  ros-noetic-rosserial-python ros-noetic-rosserial-client \
-  ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \
-  ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \
-  ros-noetic-compressed-image-transport ros-noetic-rqt* ros-noetic-rviz \
-  ros-noetic-gmapping ros-noetic-navigation ros-noetic-interactive-markers
-
-
-
-
-
TurtleBot3 Dependencies#
-
sudo apt install ros-noetic-dynamixel-sdk ros-noetic-turtlebot3-msgs ros-noetic-turtlebot3
-
-
-
-
-
Simulation:#
-
cd ~/master_ws/src
-git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git
-
-
-
-
-
ECE387 Curriculum#
-
git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git
-
-
-

The ece387_curriculum package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool:

-
cd ~/master_ws
-rosdep install --from-paths src --ignore-src -r -y
-
-
-

This may take a while.

-

Now we can make and source our workspace:

-
catkin_make
-source ~/.bashrc
-
-
-

The last set of dependencies we need to install are Python dependencies. These are listed within our ece387_curriculum package and can be installed using the pip3 tool:

-
roscd ece387_curriculum
-pip3 install -r requirements.txt
-
-
-
-

📝️ Note: the “dlib” package will take quite a while to install.

-
- -
-
-
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Master Setup — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Master Setup#

+

This guide will walk through the steps to install Ubuntu Desktop 20.04 LTS, ROS Noetic, and all dependencies on a desktop computer. This computer system is utilized in the United States Air Force Academy’s Electrical and Computer Engineering department in an embedded network with the ground robot, a TurtleBot3 Burger. The master system is used to host roscore, utilize ROS GUI tools, and create secure connections with the TurtleBot3. This guide is adapted from the TurtleBot3 e-Manual.

+
+
+

Hardware#

+

For our application, we are using Intel NUC Kits but these instructions will work on any AMD64 architecture.

+
+
+

Software#

+
+

Download Ubuntu and flash USB#

+

For the desktop machine you will first need to download Ubuntu Desktop 20.04 LTS.

+

Once downloaded, follow the instructions to create a bootable Ubuntu USB stick within Ubuntu. The guide provides links to create USB sticks from Windows and macOS as well.

+

Once the bootable USB stick is created, follow the guide to Install Ubuntu desktop selecting a useful computer name such as master0. The NUC requires you to press and hold F10 on startup to boot from a USB stick.

+ +
+

Setup GitHub SSH Keys#

+

The following assumes you already have a GitHub account.

+

Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login:

+
cd
+ssh-keygen -t ed25519 -C "github@email.com"
+
+
+

When prompted to “Enter a file in which to save the key”, hit enter.

+

At the prompt, type a secure password.

+

Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

+
eval "$(ssh-agent -s)"
+ssh-add ~/.ssh/id_ed25519
+
+
+

Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account).

+
nano ~/.ssh/id_ed25519.pub
+
+
+

Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email).

+

Open a web browser and sign in to your GitHub account.

+

In the upper-right corner of any page, click your profile photo, then click Settings:

+

logo

+

In the user settings sidebar, click SSH and GPG keys:

+

logo

+

Click New SSH key:

+

logo

+

In the “Title” field, add a descriptive label for the new key, such as “master0”.

+

Paste your key into the “Key” field (contents of the .pub file).

+

Click Add SSH key.

+ + +
+
+

Update Alternatives#

+

Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the “python” command instead of “python3” so we need to create a new executable, “/usr/bin/python” that will call Python3 (basically use the command “python” to call Python3):

+
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10
+
+
+
+
+

Additional Software#

+
sudo apt install jupyter-notebook
+jupyter contrib nbextension install --user
+
+
+ + +
+
+
+

ROS Noetic#

+

At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the master. All of these instructions are adapted from the ROS wiki. ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal.

+
+

Installation#

+

Accept software from packages.ros.org:

+
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
+
+
+

Set up keys:

+
sudo apt install curl # if you haven't already installed curl
+curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
+
+
+

Install ROS Noetic:

+
sudo apt update
+sudo apt -y install ros-noetic-desktop-full
+
+
+

The full version provides theminimum packaging, build, communications libraries, GUI tools, and 2D/3D simulation and perception packages.

+

Install ROS dependencies for building packages:

+
sudo apt -y install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip xterm build-essential
+
+
+

Initialize rosdep

+
sudo rosdep init
+rosdep update
+
+
+

Source the ROS setup file:

+
source /opt/ros/noetic/setup.bash
+
+
+

Create your ROS workspace:

+
mkdir -p ~/master_ws/src
+cd ~/master_ws/
+catkin_make
+
+
+

Setup ROS environment variables and setup scripts within the ~/.bashrc file. Open the ~/.bashrc file with your favorite command line editor and add the following to the bottom:

+
source /opt/ros/noetic/setup.bash
+source ~/master_ws/devel/setup.bash
+export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share
+export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes
+export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master
+export EDITOR='nano -w' # replace with editor of choice used with rosed command
+export TURTLEBOT3_MODEL=burger
+export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR
+
+
+

Any time you make changes to your ~/.bashrc file you must source it:

+
source ~/.bashrc
+
+
+
+
+

Dependencies#

+

There are a number of ROS packages required to operate the TurtleBot3.

+
+
ROS Dependencies#
+
sudo apt-get install ros-noetic-joy ros-noetic-teleop-twist-joy \
+  ros-noetic-teleop-twist-keyboard ros-noetic-laser-proc \
+  ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \
+  ros-noetic-rosserial-python ros-noetic-rosserial-client \
+  ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \
+  ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \
+  ros-noetic-compressed-image-transport ros-noetic-rqt* ros-noetic-rviz \
+  ros-noetic-gmapping ros-noetic-navigation ros-noetic-interactive-markers
+
+
+
+
+
TurtleBot3 Dependencies#
+
sudo apt install ros-noetic-dynamixel-sdk ros-noetic-turtlebot3-msgs ros-noetic-turtlebot3
+
+
+
+
+
Simulation:#
+
cd ~/master_ws/src
+git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git
+
+
+
+
+
ECE387 Curriculum#
+
git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git
+
+
+

The ece387_curriculum package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool:

+
cd ~/master_ws
+rosdep install --from-paths src --ignore-src -r -y
+
+
+

This may take a while.

+

Now we can make and source our workspace:

+
catkin_make
+source ~/.bashrc
+
+
+

The last set of dependencies we need to install are Python dependencies. These are listed within our ece387_curriculum package and can be installed using the pip3 tool:

+
roscd ece387_curriculum
+pip3 install -r requirements.txt
+
+
+
+

📝️ Note: the “dlib” package will take quite a while to install.

+
+ +
+
+
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Appendix/RobotSetup.html b/Appendix/RobotSetup.html old mode 100755 new mode 100644 index 85ed99a..1e46a56 --- a/Appendix/RobotSetup.html +++ b/Appendix/RobotSetup.html @@ -1,1172 +1,1172 @@ - - - - - - - - - - - - Robot Setup — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Robot Setup#

-

This guide will walk through the steps to install Ubuntu Server 20.04 LTS, ROS Noetic, and all dependencies on a Raspberry Pi 4 B. This Pi is then embedded within the Robotis TurtleBot3 Burger along with a USB Camera. The robotics system, TurtleBot3, is utilized in the United States Air Force Academy’s Electrical and Computer Engineering department to teach undergraduate students robotics. You can follow the below steps or a Raspberry Pi image can be provided by emailing Steven Beyer (sbeyer@beyersbots.com). This guide is adapted from the TurtleBot3 e-Manual.

-
-

Hardware#

-

Below is a list of recommended hardware and links. Other off-the-shelf components can replace the ones below.

-
    -
  • TurtleBot3

  • -
  • USB Camera (Any USB Cam will work, this is the one we use)

  • -
  • 128 GB High Speed MicroSD card

  • -
  • Monitor, mouse, and keyboard

  • -
  • If using an older version of the TurtleBot3 with a Jetson Nano or Raspberry Pi 3 B+ you will need to purchase a Raspberry Pi 4 Model B (preferably with 8 GB of RAM))

  • -
-
-

Hardware Assembly#

-

Follow the Robotis e-Manual for hardware assembly stopping after installing the Raspberry Pi.

-
-
-

Raspberry Pi#

-

A Raspberry Pi 4 B with 8 GB of RAM is used throughout this curriculum. Ensure heat sinks are propertly installed on the Pi such as these from CanaKit.

-

Also, a small fan can be installed to help with cooling. We used this 3D printed bracket to mount the fan.

-../_images/fan.jpg -
-
-

Camera#

-

After installing the Raspberry pi level of the TurtleBot3 you need to install the USB Camera Mount prior to finishing the robot build. The mount used in this course can be found in the curriculum material and is installed on two of the front standoffs on the TurtleBot3.

-../_images/camera_mount.jpg -
-
-
-

Software#

-
-

Download Ubuntu and flash MicroSD card#

-

There are multiple ways to download and install Ubuntu 20 to a MicroSD card, but the Raspberry Pi Imager is one of the easiest. Instructions for installing the imager on your operating system can be found on the Raspberry Pi OS software page.

-

Once installed, start the imager and select the “CHOOSE OS” button.

-../_images/installer1.png -
-

Scroll down the menu and select “Other general purpose OS”. -

-../_images/installer2.png -
-Next, select "Ubuntu". -../_images/installer3.png -
-Lastly, scroll and select the latest 64-bit version of "Ubuntu Server 22.04 LTS". -
-../_images/installer4.png -
-

Now that you have the correct image selected, you need to choose the correct storage device that corresponds to the MicroSD card. Select “CHOOSE STORAGE”.

-
-

Warning

-

This process will overwrite the drive, so ensure you select the correct device! You can select “CHOOSE STORAGE” before inserting the MicroSD card, then insert it, and the card will be the new drive that pops up.

-
-

Once you are sure the correct drive is selected, click “WRITE”.

-

Once complete you should have an Ubuntu SD card! Ensure your Raspberry Pi is powered off, connected to a monitor, keyboard, and mouse, and insert the SD card.

-
-
-

Ubuntu Setup#

-
-

Login and changing password#

-

Once Ubuntu boots up you will be prompted to enter the login and password. It may take a few minutes on first boot to configure the default username and password, so if login fails, try again after a few minutes. The default username is ubuntu and password is ubuntu.

-

On first login, you will be prompted to change the password. Enter the current password, ubuntu, and then enter a new password twice.

-
-
-

Changing username (optional)#

-

I like to change the username to “pi” so I remember that this machine is a Raspberry Pi. This is optional and you can change the username to anything you like.

-

First, add a temp user:

-
sudo adduser temp
-
-
-

Enter an easy to remember password, and then hit enter until you are back at the terminal prompt.

-

Add the temp user to the sudo group:

-
sudo adduser temp sudo
-
-
-

Log out of ubuntu user:

-
exit
-
-
-

Login to temp user account.

-

Change the ubuntu username to the new username:

-
sudo usermod -l newUsername ubuntu
-sudo usermod -d /home/newHomeDir -m newUsername
-
-
-

For example:

-
sudo usermod -l pi ubuntu
-sudo suermod -d /home/pi -m pi
-
-
-

Log out of temp user and log in with new username and password (the password is still the same as the password you set for the ubuntu user).

-

Delete the temp user:

-
sudo deluser temp
-sudo rm -r /home/temp
-
-
-

Now at the terminal prompt you should see pi@ubuntu: and if you type pwd you should see /home/pi (with pi replaced with the username you chose).

-
-
-

Change hostname#

-

If you have multiple robots on your network it is good to give each a unique hostname. We number each robot from 0-n and each robot has a corresponding hostname (e.g., robot0).

-

Change the hostname with the command line editor of your choice.

-
sudo nano /etc/hostname
-
-
-

Replace ubuntu with the hostname of choice, such as robot0. Save and exit.

-

The new hostname will not take effect until reboot. Don’t reboot yet, though! We have a couple more things to accomplish before reboot.

-
-
-

Set up Wi-Fi#

-

Until a desktop GUI is installed we have to work with the command line to set up the Wi-Fi. This is the most reliable method I have found and we will delete these changes once a GUI is installed.

-

First, determine the name of your Wi-Fi network adapter by typing ip link (for the Raspberry Pi version of Ubuntu Server 20.04 LTS it is typically wlan0).

-
pi@ubuntu:~$ ip link
-1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
-    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
-2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000
-    link/ether e4:5f:01:15:5b:30 brd ff:ff:ff:ff:ff:ff
-3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000
-    link/ether e4:5f:01:15:5b:31 brd ff:ff:ff:ff:ff:ff
-
-
-

Open the /etc/netplan/50-cloud-init.yaml file in your favorite browser:

-
sudo nano /etc/netplan/50-cloud-init.yaml
-
-
-

Edit the file so it looks like the below (use spaces and not tabs) replacing wlan0 with your wireless network interface and using your SSID and password:

-
# This file is generated from information provided by the datasource.  Changes
-# to it will not persist across an instance reboot.  To disable cloud-init's  
-# network configuration capabilities, write a file
-# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
-# network: {config: disabled}
-network:
-    ethernets:
-        eth0:
-            dhcp4: true
-            optional: true
-    version: 2
-    wifis:
-        wlan0:
-             optional: true
-             access-points:
-                 "YOUR-SSID":
-                     password: "YOUR-PASSWORD"
-             dhcp4: true
-
-
-

Save and exit.

-

Apply your changes using the following command:

-
sudo netplan apply
-
-
-

Alternatively, you can reboot your system and the changes will be automatically applied once the system boots.

-

Optional: It may be beneficial to setup a static IP address. To do this you need to determine your subnet and gateway.

-

Determine subnet and gateway addresses:

-
ubuntu@ubuntu:~$ ip route
-default via 192.168.0.1 dev wlan0 proto static 
-192.168.0.1/24 dev wlan0 proto kernel scope link src 192.168.0.201
-
-
-

Set static IP within subnet range:

-
# This file is generated from information provided by the datasource.  Changes
-# to it will not persist across an instance reboot.  To disable cloud-init's  
-# network configuration capabilities, write a file
-# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
-# network: {config: disabled}
-network:
-    ethernets:
-        eth0:
-            dhcp4: true
-            optional: true
-    version: 2
-    wifis:
-        wlan0:
-             dhcp4: no
-             access-points:
-                 "robotics_5GHz":
-                     password: "YOUR-PASSWORD"
-             addresses:
-                 - 192.168.4.208/24
-             routes:
-                 - to: default
-                   via: 192.168.4.1
-             nameservers:
-                 addresses: [192.168.4.1, 8.8.8.8, 1.1.1.1]
-             optional: true
-
-
-
-
-

Disable Automatic Updates#

-

Ubuntu will attempt to apply system updates in the background. This has caused issues in the past with ROS dependencies and keys. Disabling automatic updates allows you to control when Ubuntu installs updates. While this is not a good habit for general computer security, it is fine for this application of an embedded robotics system. Ensure you periodically update and upgrade your system.

-

Open the auto updater configuration file using sudoedit:

-
sudoedit /etc/apt/apt.conf.d/20auto-upgrades
-
-
-

Change the content from:

-
APT::Periodic::Update-Package-Lists "1";
-APT::Periodic::Unattended-Upgrade "1";
-
-
-

to:

-
APT::Periodic::Update-Package-Lists "0";
-APT::Periodic::Unattended-Upgrade "0";
-APT::Periodic::AutocleanInterval "0";
-APT::Periodic::Download-Upgradeable-Packages "0";
-
-
-

Set the systemd to prevent boot-up delay even if there is no network at startup. Run the command below to set mask the systemd process using the following command.

-
$ systemctl mask systemd-networkd-wait-online.service
-
-
-

Disable Suspend and Hibernation

-
$ sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
-
-
-

Reboot the Raspberry Pi.

-
$ reboot
-
-
-
-
-

Enable SSH and generate new keys#

-
sudo ssh-keygen -A
-sudo systemctl start ssh
-
-
-
-
-

Add Swap Space (optional)#

-

The Raspberry Pi 4 B used in our course has 8 GB of RAM. Swap Space might not be necessary, but with a larger SD card it is beneficial.

-

You can check that there is no active swap using the free utility:

-
pi@ubuntu:~$ free -h
-               total        used        free      shared  buff/cache   available
-Mem:           7.6Gi       201Mi       7.1Gi       3.0Mi       328Mi       7.3Gi
-Swap:             0B          0B          0B
-
-
-

The fallocate program can be used to create a swap:

-
sudo fallocate -l 2G /swapfile
-
-
-

If it was created correctly, you should see the below:

-
pi@ubuntu:~$ ls -lh /swapfile
--rw------- 1 root root 2.0G Aug 19 17:30 /swapfile
-
-
-

Make the file only accessible to root by typing:

-
sudo chmod 600 /swapfile
-
-
-

Verify the permissions by typing the following:

-
pi@ubuntu:~$ ls -lh /swapfile 
--rw------- 1 root root 2.0G Aug 19 17:28 /swapfile
-Now only root user has read and write flags enabled.
-
-
-

You can set the file as swap space by typing the following:

-
pi@ubuntu:~$ sudo mkswap /swapfile
-Setting up swapspace version 1, size = 2 GiB (2147479552 bytes)
-no label, UUID=b5bc4abf-2bce-419e-870d-4d44a7a05778
-
-
-

Then turn on the swap file:

-
sudo swapon /swapfile
-
-
-

To verify that this worked you can type the following:

-
pi@ubuntu:~$ sudo swapon --show
-NAME      TYPE SIZE USED PRIO
-/swapfile file   2G   0B   -2
-
-
-

This swap will only last until reboot, so to make it permanent at it to the fstab file:

-
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
-
-
-

Now it is time to reboot by typing

-
sudo reboot
-
-
-
-
-

Verify changes#

-

After reboot and you log in your new hostname should be listed at the terminal (e.g., pi@robot0). Additionally, you should be connected to Wi-Fi and have an IP Address. You can confirm by typing the following and observing the IP address in the output:

-

logo

-

You can now use this IP address to create a remote secure shell into the TurtleBot3 using either the IP address or hostname if your network provides Dynamic DNS. From another machine connected to your network type one of the following:

-
ssh username@IP_ADDRESS
-
-
-

or

-
ssh username@HOSTNAME
-
-
-

Lastly, ensure your swap space is still active by typing the following and observing the output:

-
pi@robot99:~$ free -h
-               total        used        free      shared  buff/cache   available
-Mem:           7.6Gi       224Mi       6.5Gi       3.0Mi       903Mi       7.3Gi
-Swap:          2.0Gi          0B       2.0Gi
-
-
-
-
-

Update and Upgrade#

-

Since we turned off automatic updates, you should periodically update and upgrade. You can use this single command to accomplish both while accepting all upgrades:

-
sudo apt update && sudo apt -y upgrade
-
-
-
-
-

Install Ubuntu Desktop (optional)#

-

A desktop GUI is not necessary for a remote machine like the USAFABot and will take up about 1.4 GB of RAM to run. I include directions for installing the Ubuntu GNOME 3 desktop environment for completeness and flexibility. The following will install the environment while confirming the installation:

-
sudo apt -y install ubuntu-desktop
-
-
-
-
-

Network Settings#

-

If you do install the Ubuntu Desktop and want to use the GUI to setup the Wi-Fi network then you need to remove the settings included in the /etc/netplan/50-cloud-init.yaml file. It should look like the original file when complete:

-
# This file is generated from information provided by the datasource.  Changes
-# to it will not persist across an instance reboot.  To disable cloud-init's  
-# network configuration capabilities, write a file
-# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
-# network: {config: disabled}
-network:
-    ethernets:
-        eth0:
-            dhcp4: true
-            optional: true
-    version: 2
-    wifis:
-        wlan0:
-             dhcp4: true
-             optional: true
-
-
-

You can now use the GUI interface in the top right of the screen to set up a Wi-Fi connection.

-
-
-

Setup GitHub SSH Keys#

-

The following assumes you already have a GitHub account.

-

Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login:

-
cd
-ssh-keygen -t ed25519 -C "github@email.com"
-
-
-

When prompted to “Enter a file in which to save the key”, hit enter.

-

At the prompt, type a secure password.

-

Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

-
eval "$(ssh-agent -s)"
-ssh-add ~/.ssh/id_ed25519
-
-
-

Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account).

-
nano ~/.ssh/id_ed25519.pub
-
-
-

Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email).

-

Open a web browser and sign in to your GitHub account.

-

In the upper-right corner of any page, click your profile photo, then click Settings:

-../_images/ssh1.png -
-

In the user settings sidebar, click SSH and GPG keys:

-
-../_images/ssh2.png -
-

Click New SSH key: -

-../_images/ssh3.png -
-

In the Title field, add a descriptive label for the new key, such as “robot0”.

-

Paste your key into the Key field (contents of the .pub file).

-

Click Add SSH key.

-
-
-

Update Alternatives#

-

Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the “python” command instead of “python3” so we need to create a new executable, “/usr/bin/python” that will call the Python3 (basically use the command “python” to call Python3):

-
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10
-
-
-
-
-
-

ROS Noetic#

-

At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the TurtleBot3. All of these instructions are adapted from the ROS wiki. ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal.

-
-

Installation#

-

Accept software from packages.ros.org:

-
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
-
-
-

Set up keys:

-
sudo apt install curl # if you haven't already installed curl
-curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
-
-
-

Install ROS Noetic:

-
sudo apt update
-sudo apt -y install ros-noetic-ros-base
-
-
-

The base version provides the Bare Bones of ROS to include minimum packaging, build, and communications libraries. No GUI tools are installed. As the Raspberry Pi is embedded into the USAFABot it is ideal to keep overhead as low as possible. Many of the GUI tools will be ran on the main machine.

-

Install ROS dependencies for building packages:

-
sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip build-essential
-
-
-

Initialize rosdep

-
sudo rosdep init
-rosdep update
-
-
-

Source the ROS setup file:

-
source /opt/ros/noetic/setup.bash
-
-
-

Create your ROS workspace:

-
mkdir -p ~/robot_ws/src
-cd ~/robot_ws/
-catkin_make
-
-
-

Setup ROS environment variables and setup scripts within the ~/.bashrc file. Open the ~/.bashrc file with your favorite command line editor and add the following to the bottom:

-
source /opt/ros/noetic/setup.bash
-source ~/robot_ws/devel/setup.bash
-export ROS_PACKAGE_PATH=~/robot_ws/src:/opt/ros/noetic/share
-export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes
-export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master
-export EDITOR='nano -w' # replace with editor of choice used with rosed command
-export TURTLEBOT3_MODEL=burger
-export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR
-
-
-

Any time you make changes to your ~/.bashrc file you must source it:

-
source ~/.bashrc
-
-
-
-
-

Dependencies#

-

There are a number of ROS packages required to operate the TurtleBot3.

-
-
ROS Dependencies#
-
sudo apt-get install ros-noetic-laser-proc ros-noetic-hls-lfcd-lds-driver \
-  ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \
-  ros-noetic-rosserial-python ros-noetic-rosserial-client \
-  ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \
-  ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \
-  ros-noetic-compressed-image-transport ros-noetic-gmapping \
-  ros-noetic-navigation ros-noetic-interactive-markers
-
-
-

NOTE: We may not need the following packages on the robot.

-
sudo apt-get install ros-noetic-rqt* ros-noetic-rviz 
-
-
-
-
-
TurtleBot3 Dependencies#
-
sudo apt install libudev-dev ros-noetic-turtlebot3-msgs
-cd ~/robot_ws/src
-git clone -b develop https://github.com/ROBOTIS-GIT/ld08_driver.git
-git clone https://github.com/ROBOTIS-GIT/turtlebot3.git
-git clone https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git
-
-
-
-
-
ECE387 Curriculum#
-
git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git
-
-
-

The ece387_curriculum package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool:

-
cd ~/robot_ws
-rosdep install --from-paths src --ignore-src -r -y
-
-
-

This will take a while.

-

Now we can make and source our workspace:

-
cd ~/robot_ws
-catkin_make
-source ~/.bashrc
-
-
-

The last set of dependencies we need to install are Python dependencies. These are listed within our ece387_curriculum package and can be installed using the pip3 tool:

-
roscd ece387_curriculum
-pip3 install -r requirements.txt
-
-
-
-

📝️ Note: the “dlib” package will take quite a while to install.

-
- - -
-
-
-
-

Updating OpenCR firmware#

-

The last step is updating the firmware for the OpenCR controller board.

-

Install required packages on the Raspberry Pi

-
sudo dpkg --add-architecture armhf
-sudo apt-get update
-sudo apt-get install libc6:armhf
-
-
-

Setup the OpenCR model name:

-
export OPENCR_PORT=/dev/ttyACM0
-export OPENCR_MODEL=burger_noetic
-
-
-

Download the firmware and loader, then extract the file:

-
wget https://github.com/ROBOTIS-GIT/OpenCR-Binaries/raw/master/turtlebot3/ROS1/latest/opencr_update.tar.bz2 
-tar -xvf opencr_update.tar.bz2 
-rm -rf ./opencr_update.tar.bz2
-
-
-

Upload firmware to the OpenCR:

-
cd ./opencr_update
-./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr
-
-
-

A successful firmware upload for TurtleBot3 Burger will look like:

-
pi@robot8: ~
-$ cd opencr_update/
-pi@robot8: ~/opencr_update
-$ ./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr
-aarch64
-arm
-OpenCR Update Start..
-opencr_ld_shell ver 1.0.0
-opencr_ld_main
-[  ] file name          : burger_noetic.opencr 
-[  ] file size          : 183 KB
-[  ] fw_name            : burger_noetic
-[  ] fw_ver             : 1.2.6
-[OK] Open port          : /dev/ttyACM0
-[  ]
-[  ] Board Name         : OpenCR R1.0
-[  ] Board Ver          : 0x17020800
-[  ] Board Rev          : 0x00000000
-[OK] flash_erase        : 0.99s
-[OK] flash_write        : 1.60s 
-[OK] CRC Check          : 12A5C20 12A5C20 , 0.005000 sec
-[OK] Download
-[OK] jump_to_fw
-
-
-

If not successful, attempt the debug methods in the OpenCR Setup guide.

-
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Robot Setup — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Robot Setup#

+

This guide will walk through the steps to install Ubuntu Server 20.04 LTS, ROS Noetic, and all dependencies on a Raspberry Pi 4 B. This Pi is then embedded within the Robotis TurtleBot3 Burger along with a USB Camera. The robotics system, TurtleBot3, is utilized in the United States Air Force Academy’s Electrical and Computer Engineering department to teach undergraduate students robotics. You can follow the below steps or a Raspberry Pi image can be provided by emailing Steven Beyer (sbeyer@beyersbots.com). This guide is adapted from the TurtleBot3 e-Manual.

+
+

Hardware#

+

Below is a list of recommended hardware and links. Other off-the-shelf components can replace the ones below.

+
    +
  • TurtleBot3

  • +
  • USB Camera (Any USB Cam will work, this is the one we use)

  • +
  • 128 GB High Speed MicroSD card

  • +
  • Monitor, mouse, and keyboard

  • +
  • If using an older version of the TurtleBot3 with a Jetson Nano or Raspberry Pi 3 B+ you will need to purchase a Raspberry Pi 4 Model B (preferably with 8 GB of RAM))

  • +
+
+

Hardware Assembly#

+

Follow the Robotis e-Manual for hardware assembly stopping after installing the Raspberry Pi.

+
+
+

Raspberry Pi#

+

A Raspberry Pi 4 B with 8 GB of RAM is used throughout this curriculum. Ensure heat sinks are propertly installed on the Pi such as these from CanaKit.

+

Also, a small fan can be installed to help with cooling. We used this 3D printed bracket to mount the fan.

+../_images/fan.jpg +
+
+

Camera#

+

After installing the Raspberry pi level of the TurtleBot3 you need to install the USB Camera Mount prior to finishing the robot build. The mount used in this course can be found in the curriculum material and is installed on two of the front standoffs on the TurtleBot3.

+../_images/camera_mount.jpg +
+
+
+

Software#

+
+

Download Ubuntu and flash MicroSD card#

+

There are multiple ways to download and install Ubuntu 20 to a MicroSD card, but the Raspberry Pi Imager is one of the easiest. Instructions for installing the imager on your operating system can be found on the Raspberry Pi OS software page.

+

Once installed, start the imager and select the “CHOOSE OS” button.

+../_images/installer1.png +
+

Scroll down the menu and select “Other general purpose OS”. +

+../_images/installer2.png +
+Next, select "Ubuntu". +../_images/installer3.png +
+Lastly, scroll and select the latest 64-bit version of "Ubuntu Server 22.04 LTS". +
+../_images/installer4.png +
+

Now that you have the correct image selected, you need to choose the correct storage device that corresponds to the MicroSD card. Select “CHOOSE STORAGE”.

+
+

Warning

+

This process will overwrite the drive, so ensure you select the correct device! You can select “CHOOSE STORAGE” before inserting the MicroSD card, then insert it, and the card will be the new drive that pops up.

+
+

Once you are sure the correct drive is selected, click “WRITE”.

+

Once complete you should have an Ubuntu SD card! Ensure your Raspberry Pi is powered off, connected to a monitor, keyboard, and mouse, and insert the SD card.

+
+
+

Ubuntu Setup#

+
+

Login and changing password#

+

Once Ubuntu boots up you will be prompted to enter the login and password. It may take a few minutes on first boot to configure the default username and password, so if login fails, try again after a few minutes. The default username is ubuntu and password is ubuntu.

+

On first login, you will be prompted to change the password. Enter the current password, ubuntu, and then enter a new password twice.

+
+
+

Changing username (optional)#

+

I like to change the username to “pi” so I remember that this machine is a Raspberry Pi. This is optional and you can change the username to anything you like.

+

First, add a temp user:

+
sudo adduser temp
+
+
+

Enter an easy to remember password, and then hit enter until you are back at the terminal prompt.

+

Add the temp user to the sudo group:

+
sudo adduser temp sudo
+
+
+

Log out of ubuntu user:

+
exit
+
+
+

Login to temp user account.

+

Change the ubuntu username to the new username:

+
sudo usermod -l newUsername ubuntu
+sudo usermod -d /home/newHomeDir -m newUsername
+
+
+

For example:

+
sudo usermod -l pi ubuntu
+sudo suermod -d /home/pi -m pi
+
+
+

Log out of temp user and log in with new username and password (the password is still the same as the password you set for the ubuntu user).

+

Delete the temp user:

+
sudo deluser temp
+sudo rm -r /home/temp
+
+
+

Now at the terminal prompt you should see pi@ubuntu: and if you type pwd you should see /home/pi (with pi replaced with the username you chose).

+
+
+

Change hostname#

+

If you have multiple robots on your network it is good to give each a unique hostname. We number each robot from 0-n and each robot has a corresponding hostname (e.g., robot0).

+

Change the hostname with the command line editor of your choice.

+
sudo nano /etc/hostname
+
+
+

Replace ubuntu with the hostname of choice, such as robot0. Save and exit.

+

The new hostname will not take effect until reboot. Don’t reboot yet, though! We have a couple more things to accomplish before reboot.

+
+
+

Set up Wi-Fi#

+

Until a desktop GUI is installed we have to work with the command line to set up the Wi-Fi. This is the most reliable method I have found and we will delete these changes once a GUI is installed.

+

First, determine the name of your Wi-Fi network adapter by typing ip link (for the Raspberry Pi version of Ubuntu Server 20.04 LTS it is typically wlan0).

+
pi@ubuntu:~$ ip link
+1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
+    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
+2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000
+    link/ether e4:5f:01:15:5b:30 brd ff:ff:ff:ff:ff:ff
+3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000
+    link/ether e4:5f:01:15:5b:31 brd ff:ff:ff:ff:ff:ff
+
+
+

Open the /etc/netplan/50-cloud-init.yaml file in your favorite browser:

+
sudo nano /etc/netplan/50-cloud-init.yaml
+
+
+

Edit the file so it looks like the below (use spaces and not tabs) replacing wlan0 with your wireless network interface and using your SSID and password:

+
# This file is generated from information provided by the datasource.  Changes
+# to it will not persist across an instance reboot.  To disable cloud-init's  
+# network configuration capabilities, write a file
+# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
+# network: {config: disabled}
+network:
+    ethernets:
+        eth0:
+            dhcp4: true
+            optional: true
+    version: 2
+    wifis:
+        wlan0:
+             optional: true
+             access-points:
+                 "YOUR-SSID":
+                     password: "YOUR-PASSWORD"
+             dhcp4: true
+
+
+

Save and exit.

+

Apply your changes using the following command:

+
sudo netplan apply
+
+
+

Alternatively, you can reboot your system and the changes will be automatically applied once the system boots.

+

Optional: It may be beneficial to setup a static IP address. To do this you need to determine your subnet and gateway.

+

Determine subnet and gateway addresses:

+
ubuntu@ubuntu:~$ ip route
+default via 192.168.0.1 dev wlan0 proto static 
+192.168.0.1/24 dev wlan0 proto kernel scope link src 192.168.0.201
+
+
+

Set static IP within subnet range:

+
# This file is generated from information provided by the datasource.  Changes
+# to it will not persist across an instance reboot.  To disable cloud-init's  
+# network configuration capabilities, write a file
+# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
+# network: {config: disabled}
+network:
+    ethernets:
+        eth0:
+            dhcp4: true
+            optional: true
+    version: 2
+    wifis:
+        wlan0:
+             dhcp4: no
+             access-points:
+                 "robotics_5GHz":
+                     password: "YOUR-PASSWORD"
+             addresses:
+                 - 192.168.4.208/24
+             routes:
+                 - to: default
+                   via: 192.168.4.1
+             nameservers:
+                 addresses: [192.168.4.1, 8.8.8.8, 1.1.1.1]
+             optional: true
+
+
+
+
+

Disable Automatic Updates#

+

Ubuntu will attempt to apply system updates in the background. This has caused issues in the past with ROS dependencies and keys. Disabling automatic updates allows you to control when Ubuntu installs updates. While this is not a good habit for general computer security, it is fine for this application of an embedded robotics system. Ensure you periodically update and upgrade your system.

+

Open the auto updater configuration file using sudoedit:

+
sudoedit /etc/apt/apt.conf.d/20auto-upgrades
+
+
+

Change the content from:

+
APT::Periodic::Update-Package-Lists "1";
+APT::Periodic::Unattended-Upgrade "1";
+
+
+

to:

+
APT::Periodic::Update-Package-Lists "0";
+APT::Periodic::Unattended-Upgrade "0";
+APT::Periodic::AutocleanInterval "0";
+APT::Periodic::Download-Upgradeable-Packages "0";
+
+
+

Set the systemd to prevent boot-up delay even if there is no network at startup. Run the command below to set mask the systemd process using the following command.

+
$ systemctl mask systemd-networkd-wait-online.service
+
+
+

Disable Suspend and Hibernation

+
$ sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
+
+
+

Reboot the Raspberry Pi.

+
$ reboot
+
+
+
+
+

Enable SSH and generate new keys#

+
sudo ssh-keygen -A
+sudo systemctl start ssh
+
+
+
+
+

Add Swap Space (optional)#

+

The Raspberry Pi 4 B used in our course has 8 GB of RAM. Swap Space might not be necessary, but with a larger SD card it is beneficial.

+

You can check that there is no active swap using the free utility:

+
pi@ubuntu:~$ free -h
+               total        used        free      shared  buff/cache   available
+Mem:           7.6Gi       201Mi       7.1Gi       3.0Mi       328Mi       7.3Gi
+Swap:             0B          0B          0B
+
+
+

The fallocate program can be used to create a swap:

+
sudo fallocate -l 2G /swapfile
+
+
+

If it was created correctly, you should see the below:

+
pi@ubuntu:~$ ls -lh /swapfile
+-rw------- 1 root root 2.0G Aug 19 17:30 /swapfile
+
+
+

Make the file only accessible to root by typing:

+
sudo chmod 600 /swapfile
+
+
+

Verify the permissions by typing the following:

+
pi@ubuntu:~$ ls -lh /swapfile 
+-rw------- 1 root root 2.0G Aug 19 17:28 /swapfile
+Now only root user has read and write flags enabled.
+
+
+

You can set the file as swap space by typing the following:

+
pi@ubuntu:~$ sudo mkswap /swapfile
+Setting up swapspace version 1, size = 2 GiB (2147479552 bytes)
+no label, UUID=b5bc4abf-2bce-419e-870d-4d44a7a05778
+
+
+

Then turn on the swap file:

+
sudo swapon /swapfile
+
+
+

To verify that this worked you can type the following:

+
pi@ubuntu:~$ sudo swapon --show
+NAME      TYPE SIZE USED PRIO
+/swapfile file   2G   0B   -2
+
+
+

This swap will only last until reboot, so to make it permanent at it to the fstab file:

+
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
+
+
+

Now it is time to reboot by typing

+
sudo reboot
+
+
+
+
+

Verify changes#

+

After reboot and you log in your new hostname should be listed at the terminal (e.g., pi@robot0). Additionally, you should be connected to Wi-Fi and have an IP Address. You can confirm by typing the following and observing the IP address in the output:

+

logo

+

You can now use this IP address to create a remote secure shell into the TurtleBot3 using either the IP address or hostname if your network provides Dynamic DNS. From another machine connected to your network type one of the following:

+
ssh username@IP_ADDRESS
+
+
+

or

+
ssh username@HOSTNAME
+
+
+

Lastly, ensure your swap space is still active by typing the following and observing the output:

+
pi@robot99:~$ free -h
+               total        used        free      shared  buff/cache   available
+Mem:           7.6Gi       224Mi       6.5Gi       3.0Mi       903Mi       7.3Gi
+Swap:          2.0Gi          0B       2.0Gi
+
+
+
+
+

Update and Upgrade#

+

Since we turned off automatic updates, you should periodically update and upgrade. You can use this single command to accomplish both while accepting all upgrades:

+
sudo apt update && sudo apt -y upgrade
+
+
+
+
+

Install Ubuntu Desktop (optional)#

+

A desktop GUI is not necessary for a remote machine like the USAFABot and will take up about 1.4 GB of RAM to run. I include directions for installing the Ubuntu GNOME 3 desktop environment for completeness and flexibility. The following will install the environment while confirming the installation:

+
sudo apt -y install ubuntu-desktop
+
+
+
+
+

Network Settings#

+

If you do install the Ubuntu Desktop and want to use the GUI to setup the Wi-Fi network then you need to remove the settings included in the /etc/netplan/50-cloud-init.yaml file. It should look like the original file when complete:

+
# This file is generated from information provided by the datasource.  Changes
+# to it will not persist across an instance reboot.  To disable cloud-init's  
+# network configuration capabilities, write a file
+# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
+# network: {config: disabled}
+network:
+    ethernets:
+        eth0:
+            dhcp4: true
+            optional: true
+    version: 2
+    wifis:
+        wlan0:
+             dhcp4: true
+             optional: true
+
+
+

You can now use the GUI interface in the top right of the screen to set up a Wi-Fi connection.

+
+
+

Setup GitHub SSH Keys#

+

The following assumes you already have a GitHub account.

+

Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login:

+
cd
+ssh-keygen -t ed25519 -C "github@email.com"
+
+
+

When prompted to “Enter a file in which to save the key”, hit enter.

+

At the prompt, type a secure password.

+

Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

+
eval "$(ssh-agent -s)"
+ssh-add ~/.ssh/id_ed25519
+
+
+

Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account).

+
nano ~/.ssh/id_ed25519.pub
+
+
+

Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email).

+

Open a web browser and sign in to your GitHub account.

+

In the upper-right corner of any page, click your profile photo, then click Settings:

+../_images/ssh1.png +
+

In the user settings sidebar, click SSH and GPG keys:

+
+../_images/ssh2.png +
+

Click New SSH key: +

+../_images/ssh3.png +
+

In the Title field, add a descriptive label for the new key, such as “robot0”.

+

Paste your key into the Key field (contents of the .pub file).

+

Click Add SSH key.

+
+
+

Update Alternatives#

+

Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the “python” command instead of “python3” so we need to create a new executable, “/usr/bin/python” that will call the Python3 (basically use the command “python” to call Python3):

+
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10
+
+
+
+
+
+

ROS Noetic#

+

At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the TurtleBot3. All of these instructions are adapted from the ROS wiki. ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal.

+
+

Installation#

+

Accept software from packages.ros.org:

+
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
+
+
+

Set up keys:

+
sudo apt install curl # if you haven't already installed curl
+curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
+
+
+

Install ROS Noetic:

+
sudo apt update
+sudo apt -y install ros-noetic-ros-base
+
+
+

The base version provides the Bare Bones of ROS to include minimum packaging, build, and communications libraries. No GUI tools are installed. As the Raspberry Pi is embedded into the USAFABot it is ideal to keep overhead as low as possible. Many of the GUI tools will be ran on the main machine.

+

Install ROS dependencies for building packages:

+
sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip build-essential
+
+
+

Initialize rosdep

+
sudo rosdep init
+rosdep update
+
+
+

Source the ROS setup file:

+
source /opt/ros/noetic/setup.bash
+
+
+

Create your ROS workspace:

+
mkdir -p ~/robot_ws/src
+cd ~/robot_ws/
+catkin_make
+
+
+

Setup ROS environment variables and setup scripts within the ~/.bashrc file. Open the ~/.bashrc file with your favorite command line editor and add the following to the bottom:

+
source /opt/ros/noetic/setup.bash
+source ~/robot_ws/devel/setup.bash
+export ROS_PACKAGE_PATH=~/robot_ws/src:/opt/ros/noetic/share
+export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes
+export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master
+export EDITOR='nano -w' # replace with editor of choice used with rosed command
+export TURTLEBOT3_MODEL=burger
+export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR
+
+
+

Any time you make changes to your ~/.bashrc file you must source it:

+
source ~/.bashrc
+
+
+
+
+

Dependencies#

+

There are a number of ROS packages required to operate the TurtleBot3.

+
+
ROS Dependencies#
+
sudo apt-get install ros-noetic-laser-proc ros-noetic-hls-lfcd-lds-driver \
+  ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \
+  ros-noetic-rosserial-python ros-noetic-rosserial-client \
+  ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \
+  ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \
+  ros-noetic-compressed-image-transport ros-noetic-gmapping \
+  ros-noetic-navigation ros-noetic-interactive-markers
+
+
+

NOTE: We may not need the following packages on the robot.

+
sudo apt-get install ros-noetic-rqt* ros-noetic-rviz 
+
+
+
+
+
TurtleBot3 Dependencies#
+
sudo apt install libudev-dev ros-noetic-turtlebot3-msgs
+cd ~/robot_ws/src
+git clone -b develop https://github.com/ROBOTIS-GIT/ld08_driver.git
+git clone https://github.com/ROBOTIS-GIT/turtlebot3.git
+git clone https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git
+
+
+
+
+
ECE387 Curriculum#
+
git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git
+
+
+

The ece387_curriculum package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool:

+
cd ~/robot_ws
+rosdep install --from-paths src --ignore-src -r -y
+
+
+

This will take a while.

+

Now we can make and source our workspace:

+
cd ~/robot_ws
+catkin_make
+source ~/.bashrc
+
+
+

The last set of dependencies we need to install are Python dependencies. These are listed within our ece387_curriculum package and can be installed using the pip3 tool:

+
roscd ece387_curriculum
+pip3 install -r requirements.txt
+
+
+
+

📝️ Note: the “dlib” package will take quite a while to install.

+
+ + +
+
+
+
+

Updating OpenCR firmware#

+

The last step is updating the firmware for the OpenCR controller board.

+

Install required packages on the Raspberry Pi

+
sudo dpkg --add-architecture armhf
+sudo apt-get update
+sudo apt-get install libc6:armhf
+
+
+

Setup the OpenCR model name:

+
export OPENCR_PORT=/dev/ttyACM0
+export OPENCR_MODEL=burger_noetic
+
+
+

Download the firmware and loader, then extract the file:

+
wget https://github.com/ROBOTIS-GIT/OpenCR-Binaries/raw/master/turtlebot3/ROS1/latest/opencr_update.tar.bz2 
+tar -xvf opencr_update.tar.bz2 
+rm -rf ./opencr_update.tar.bz2
+
+
+

Upload firmware to the OpenCR:

+
cd ./opencr_update
+./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr
+
+
+

A successful firmware upload for TurtleBot3 Burger will look like:

+
pi@robot8: ~
+$ cd opencr_update/
+pi@robot8: ~/opencr_update
+$ ./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr
+aarch64
+arm
+OpenCR Update Start..
+opencr_ld_shell ver 1.0.0
+opencr_ld_main
+[  ] file name          : burger_noetic.opencr 
+[  ] file size          : 183 KB
+[  ] fw_name            : burger_noetic
+[  ] fw_ver             : 1.2.6
+[OK] Open port          : /dev/ttyACM0
+[  ]
+[  ] Board Name         : OpenCR R1.0
+[  ] Board Ver          : 0x17020800
+[  ] Board Rev          : 0x00000000
+[OK] flash_erase        : 0.99s
+[OK] flash_write        : 1.60s 
+[OK] CRC Check          : 12A5C20 12A5C20 , 0.005000 sec
+[OK] Download
+[OK] jump_to_fw
+
+
+

If not successful, attempt the debug methods in the OpenCR Setup guide.

+
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module00.0_IntroductionGIT/IntroGit.html b/Module00.0_IntroductionGIT/IntroGit.html deleted file mode 100755 index 340b842..0000000 --- a/Module00.0_IntroductionGIT/IntroGit.html +++ /dev/null @@ -1,679 +0,0 @@ - - - - - - - - - - - - Module 0: Setting Up GIT Repositories — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Module 0: Setting Up GIT Repositories#

-
-

Purpose#

-

Setup GIT repositories and access on the Master.

-
-
-

Create a repo within the GitHub Classroom:#

-
    -
  1. Browse to github.com and create a GitHub account if you do not already have one. It is useful if your username is something that identifies you (e.g., bneff1013).

  2. -
  3. One student per group do the following on your personal computer:

    -
      -
    1. First we are going to setup the repo for the Master. This will allow your instructor to see all of your commits throughout the semester.

    2. -
    3. Browse to https://classroom.github.com/a/OoH0u_XW

    4. -
    5. Select “Accept this assignment”

    6. -
    7. You may need to hit refresh, but eventually it will provide you a link to the repository.

    8. -
    9. Browse to your repository.

    10. -
    11. Note the url for your repository (save this link, it is the best way to check if your repo is updated).

    12. -
    13. Go to Settings -> Manage access -> and “Invite teams or people”.

    14. -
    15. Provide access to your team member using their GitHub user name.

    16. -
    17. Now we need to do the exact same thing to setup the repo for the robot.

    18. -
    19. Browse to https://classroom.github.com/a/0MPq4TNE and repeat steps c-h.

    20. -
    -
  4. -
-
-
-

Enable SSH connection to your GitHub account#

-
    -
  1. Open a terminal on your Master (ctrl+alt+t).

  2. -
  3. The same student as step 1.1.2 do the following:

    -
      -
    1. Generate a new SSH key, substituting your GitHub email address:

      -
      ssh-keygen -t ed25519 -C "your_email@example.com"
      -
      -
      -
    2. -
    3. When you’re prompted to “Enter a file in which to save the key,” click enter.

    4. -
    5. At the prompt, type a secure passphrase.

    6. -
    7. Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

      -
      eval "$(ssh-agent -s)"
      -ssh-add ~/.ssh/id_ed25519
      -
      -
      -
    8. -
    9. Open the public key:

      -
      nano ~/.ssh/id_ed25519.pub
      -
      -
      -
    10. -
    11. Select the contents of the file (maximize the window and ensure it has your GIT email at the end), right click, and select copy.

    12. -
    13. Open a web browser and sign in to your GitHub account.

    14. -
    15. In the upper-right corner of any page, click your profile photo, then click Settings. -logo

    16. -
    17. In the user settings sidebar, click SSH and GPG keys. -logo

    18. -
    19. Click New SSH key -logo

    20. -
    21. In the “Title” field, add a descriptive label for the new key, such as “MasterX”.

    22. -
    23. Paste your key into the “Key” field (contents of the .pub file).

    24. -
    25. Click Add SSH key.

    26. -
    27. If prompted, confirm your GitHub password.

    28. -
    29. Create a secure shell connection to your Robot (password is dfec3141)

      -
      ssh pi@robotX
      -
      -
      -
    30. -
    31. Repeat steps a-f on your Robot and j-n on your Master.

    32. -
    -
  4. -
-
-
-

Clone repository to your master.#

-
    -
  1. On the Master, open the GitHub repository and copy your repo address using the SSH mode: -logo

  2. -
  3. Open a terminal and browse to your workspace source folder:

    -
    cd ~/master_ws/src/
    -
    -
    -
  4. -
  5. Clone your repo using the username and password used when you generated the SSH key, replacing USERNAME with your GitHub username:

    -
    git clone git@github.com:ECE387/ece387_master_spring202X-USERNAME.git
    -
    -
    -
  6. -
  7. Update your git email address and the last name for you and your team mate.

    -
    git config --global user.email "you@example.com"
    -git config --global user.name "Lastname1 Lastname2"
    -
    -
    -
  8. -
-
-
-

Clone repository to your robot.#

-
    -
  1. Create a secure shell connection to your robot:

    -
    ssh pi@robotX
    -
    -
    -
  2. -
  3. Ensure you are in the ROS robot workspace src directory.

    -
    cd robot_ws/src
    -
    -
    -
  4. -
  5. Clone the robot repository:

    -
    git clone git@github.com:ECE387/ece387_robot_spring202X-USERNAME.git
    -
    -
    -
  6. -
  7. Update your git email address and the last name for you and your team mate.

    -
    git config --global user.email "you@example.com"
    -git config --global user.name "Lastname1 Lastname2"
    -
    -
    -
  8. -
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - - \ No newline at end of file diff --git a/Module00.1_RobotSetup/RobotSetup.html b/Module00.1_RobotSetup/RobotSetup.html deleted file mode 100755 index 51db744..0000000 --- a/Module00.1_RobotSetup/RobotSetup.html +++ /dev/null @@ -1,1124 +0,0 @@ - - - - - - - - - - - - Robot Setup — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Robot Setup#

-

This guide will walk through the steps to install Ubuntu Server 22.04 LTS, ROS2 Humble, and all dependencies on a Raspberry Pi 4B. This Pi is then embedded within the Robotis TurtleBot3 Burger along with a USB camera. The robotics system, TurtleBot3, is utilized in the United States Air Force Academy’s Electrical and Computer Engineering department to teach undergraduate students robotics.

-

This guide is adapted from the TurtleBot3 e-Manual.

-
-
-

Hardware#

-

Below is a list of recommended hardware and links. Other off-the-shelf components can replace the ones below.

-
    -
  • TurtleBot3

  • -
  • USB Camera (Any USB Cam will work, this is the one we use)

  • -
  • 128 GB High Speed MicroSD card

  • -
  • Monitor, mouse, and keyboard

  • -
  • If using an older version of the TurtleBot3 with a Jetson Nano or Raspberry Pi 3B+ you will need ot purchase a Raspberry Pi 4 Model B (preferably with 8 GB of RAM))

  • -
-
-

Hardware Assembly#

-

Follow the Robotis e-Manual for hardware assembly stopping after installing the Raspberry Pi.

-
-
-

Raspberry Pi#

-

A Raspberry Pi 4 B with 8 GB of RAM is used throughout this curriculum. Ensure heat sinks are propertly installed on the Pi such as these from CanaKit.

-

Also, a small fan can be installed to help with cooling. We used this 3D printed bracket to mount the fan.

-../_images/fan.jpg -
-
-

Camera#

-

After installing the Raspberry pi level of the TurtleBot3 you need to install the USB Camera Mount prior to finishing the robot build. The mount used in this course can be found in the curriculum material and is installed on two of the front standoffs on the TurtleBot3.

-../_images/camera_mount.jpg -
-
-
-

Software#

-
-

Download Ubuntu and flash MicroSD card#

-

There are multiple ways to download and install Ubuntu 22.04 to a MicroSD card, but the Raspberry Pi Imager is one of the easiest. Instructions for installing the imager on your operating system can be found on the Raspberry Pi OS software page.

-

Once installed, start the imager and select the “CHOOSE OS” button.

-../_images/installer1.png -
-Scroll down the menu and select "Other general purpose OS". -
-../_images/installer2.png -
-

Next, select “Ubuntu”.

-../_images/installer3.png -
-Lastly, scroll and select the latest 64-bit version of "Ubuntu Server 22.04 LTS". -
-../_images/installer4.png -
-

Now that you have the correct image selected, you need to choose the correct storage device that corresponds to the MicroSD card. Select “CHOOSE STORAGE”.

-
-

Warning

-

This process will overwrite the drive, so ensure you select the correct device! You can select “CHOOSE STORAGE” before inserting the MicroSD card, then insert it, and the card will be the new drive that pops up.

-
-

Once you are sure the correct drive is selected, click “WRITE”.

-

Once complete you should have an Ubuntu SD card! Ensure your Raspberry Pi is powered off, connected to a monitor, keyboard, and mouse, and insert the SD card.

-
-
-

Ubuntu Setup#

-
-

Login and changing password#

-

Once Ubuntu boots up you will be prompted to enter the login and password. It may take a few minutes on first boot to configure the default username and password, so if login fails, try again after a few minutes. The default username is ubuntu and password is ubuntu.

-

On first login, you will be prompted to change the password. Enter the current password, ubuntu, and then enter a new password twice.

-
-
-

Changing username (optional)#

-

I like to change the username to “pi” so I remember that this machine is a Raspberry Pi. This is optional and you can change the username to anything you like.

-

First, add a temp user:

-
sudo adduser temp
-
-
-

Enter an easy to remember password, and then hit enter until you are back at the terminal prompt.

-

Add the temp user to the sudo group:

-
sudo adduser temp sudo
-
-
-

Log out of ubuntu user:

-
exit
-
-
-

Login to temp user account.

-

Change the ubuntu username to the new username:

-
sudo usermod -l newUsername ubuntu
-sudo usermod -d /home/newHomeDir -m newUsername
-
-
-

For example:

-
sudo usermod -l pi ubuntu
-sudo usermod -d /home/pi -m pi
-
-
-

Log out of temp user and log in with new username and password (the password is still the same as the password you set for the ubuntu user).

-

Delete the temp user:

-
sudo deluser temp
-sudo rm -r /home/temp
-
-
-

Now at the terminal prompt you should see pi@ubuntu: and if you type pwd you should see /home/pi (with pi replaced with the username you chose).

-
-
-

Change hostname#

-

If you have multiple robots on your network it is good to give each a unique hostname. We number each robot from 0-n and each robot has a corresponding hostname (e.g., robot0).

-

Change the hostname with the command line editor of your choice.

-
sudo hostnamectl set-hostname robot0
-
-
-

Replace ubuntu with the hostname of choice, such as robot0. Save and exit.

-

The new hostname will not take effect until reboot. Don’t reboot yet, though! We have a couple more things to accomplish before reboot.

-
-
-

Set up Wi-Fi#

-

Until a desktop GUI is installed we have to work with the command line to set up the Wi-Fi. This is the most reliable method I have found and we will delete these changes once a GUI is installed.

-

First, determine the name of your Wi-Fi network adapter by typing ip link (for the Raspberry Pi version of Ubuntu Server 20.04 LTS it is typically wlan0).

-
pi@ubuntu:~$ ip link
-1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
-    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
-2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000
-    link/ether e4:5f:01:15:5b:30 brd ff:ff:ff:ff:ff:ff
-3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000
-    link/ether e4:5f:01:15:5b:31 brd ff:ff:ff:ff:ff:ff
-
-
-

Open the /etc/netplan/50-cloud-init.yaml file in your favorite browser:

-
sudo nano /etc/netplan/50-cloud-init.yaml
-
-
-

Edit the file so it looks like the below (use spaces and not tabs) replacing wlan0 with your wireless network interface and using your SSID and password:

-

Save and exit.

-
# This file is generated from information provided by the datasource.  Changes
-# to it will not persist across an instance reboot.  To disable cloud-init's  
-# network configuration capabilities, write a file
-# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
-# network: {config: disabled}
-network:
-    ethernets:
-        eth0:
-            dhcp4: true
-            optional: true
-    version: 2
-    wifis:
-        wlan0:
-             optional: true
-             access-points:
-                 "YOUR-SSID":
-                     password: "YOUR-PASSWORD"
-             dhcp4: true
-
-
-

Apply your changes using the following command:

-
sudo netplan apply
-
-
-

Alternatively, you can reboot your system and the changes will be automatically applied once the system boots.

-

Optional: It may be beneficial to setup a static IP address. To do this you need to determine your subnet and gateway.

-

Determine subnet and gateway addresses:

-
ubuntu@ubuntu:~$ ip route
-default via 192.168.0.1 dev wlan0 proto static 
-192.168.0.0/24 dev wlan0 proto kernel scope link src 192.168.0.201
-
-
-

Set static IP within subnet range:

-
# This file is generated from information provided by the datasource.  Changes
-# to it will not persist across an instance reboot.  To disable cloud-init's  
-# network configuration capabilities, write a file
-# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
-# network: {config: disabled}
-network:
-    ethernets:
-        eth0:
-            dhcp4: true
-            optional: true
-    version: 2
-    wifis:
-        wlan0:
-             dhcp4: no
-             access-points:
-                 "YOUR-SSID":
-                     password: "YOUR-PASSWORD"
-             addresses:
-                 - 192.168.0.201/24
-             routes:
-                 - to: default
-                   via: 192.168.0.1
-             nameservers:
-                 addresses: [192.168.0.1, 8.8.8.8, 1.1.1.1]
-             optional: true
-
-
-
-
-

Disable Automatic Updates#

-

Ubuntu will attempt to apply system updates in the background. This has caused issues in the past with ROS dependencies and keys. Disabling automatic updates allows you to control when Ubuntu installs updates. While this is not a good habit for general computer security, it is fine for this application of an embedded robotics system. Ensure you periodically update and upgrade your system.

-

Open the auto updater configuration file using sudoedit:

-
sudoedit /etc/apt/apt.conf.d/20auto-upgrades
-
-
-

Change the content from:

-
APT::Periodic::Update-Package-Lists "1";
-APT::Periodic::Unattended-Upgrade "1";
-
-
-

to:

-
APT::Periodic::Update-Package-Lists "0";
-APT::Periodic::Unattended-Upgrade "0";
-APT::Periodic::AutocleanInterval "0";
-APT::Periodic::Download-Upgradeable-Packages "0";
-
-
-
-
-

Enable SSH and generate new keys#

-
sudo ssh-keygen -A
-sudo systemctl start ssh
-
-
-
-
-

Add Swap Space (optional)#

-

The Raspberry Pi 4 B used in our course has 8 GB of RAM. Swap Space might not be necessary, but with a larger SD card it is beneficial.

-

You can check that there is no active swap using the free utility:

-
pi@ubuntu:~$ free -h
-               total        used        free      shared  buff/cache   available
-Mem:           7.6Gi       201Mi       7.1Gi       3.0Mi       328Mi       7.3Gi
-Swap:             0B          0B          0B
-
-
-

The fallocate program can be used to create a swap:

-
sudo fallocate -l 2G /swapfile
-
-
-

If it was created correctly, you should see the below:

-
pi@ubuntu:~$ ls -lh /swapfile
--rw------- 1 root root 2.0G Aug 19 17:30 /swapfile
-
-
-

Make the file only accessible to root by typing:

-
sudo chmod 600 /swapfile
-
-
-

Verify the permissions by typing the following:

-
pi@ubuntu:~$ ls -lh /swapfile 
--rw------- 1 root root 2.0G Aug 19 17:28 /swapfile
-Now only root user has read and write flags enabled.
-
-
-

You can set the file as swap space by typing the following:

-
pi@ubuntu:~$ sudo mkswap /swapfile
-Setting up swapspace version 1, size = 2 GiB (2147479552 bytes)
-no label, UUID=b5bc4abf-2bce-419e-870d-4d44a7a05778
-
-
-

Then turn on the swap file:

-
sudo swapon /swapfile
-
-
-

To verify that this worked you can type the following:

-
pi@ubuntu:~$ sudo swapon --show
-NAME      TYPE SIZE USED PRIO
-/swapfile file   2G   0B   -2
-
-
-

This swap will only last until reboot, so to make it permanent at it to the fstab file:

-
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
-
-
-

Now it is time to reboot by typing

-
sudo reboot
-
-
-
-
-

Verify changes#

-

After reboot and you log in your new hostname should be listed at the terminal (e.g., pi@robot0). Additionally, you should be connected to Wi-Fi and have an IP Address. You can confirm by typing the following and observing the IP address in the output:

-

logo

-

You can now use this IP address to create a remote secure shell into the TurtleBot3 using either the IP address or hostname if your network provides Dynamic DNS. From another machine connected to your network type one of the following:

-
ssh username@IP_ADDRESS
-
-
-

or

-
ssh username@HOSTNAME
-
-
-

Lastly, ensure your swap space is still active by typing the following and observing the output:

-
pi@robot99:~$ free -h
-               total        used        free      shared  buff/cache   available
-Mem:           7.6Gi       224Mi       6.5Gi       3.0Mi       903Mi       7.3Gi
-Swap:          2.0Gi          0B       2.0Gi
-
-
-
-
-

Update and Upgrade#

-

Since we turned off automatic updates, you should periodically update and upgrade. You can use this single command to accomplish both while accepting all upgrades:

-
sudo apt update && sudo apt -y upgrade
-
-
-
-
-

Install Ubuntu Desktop (optional)#

-

A desktop GUI is not necessary for a remote machine like TurtleBot3 and will take up about 1.4 GB of RAM to run. If GUI is neccessary, the following will install the environment while confirming the installation:

-
sudo apt -y install ubuntu-desktop
-
-
-

If you do install the Ubuntu Desktop and want to use the GUI to setup the Wi-Fi network then you need to remove the settings included in the /etc/netplan/50-cloud-init.yaml file. It should look like the original file when complete:

-
# This file is generated from information provided by the datasource.  Changes
-# to it will not persist across an instance reboot.  To disable cloud-init's  
-# network configuration capabilities, write a file
-# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:    
-# network: {config: disabled}
-network:
-    ethernets:
-        eth0:
-            dhcp4: true
-            optional: true
-    version: 2
-    wifis:
-        wlan0:
-             dhcp4: true
-             optional: true
-
-
-

You can now use the GUI interface in the top right of the screen to set up a Wi-Fi connection.

-
-
-
-

Setup GitHub SSH Keys#

-

The following assumes you already have a GitHub account.

-

Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login:

-
cd
-ssh-keygen -t ed25519 -C "github@email.com"
-
-
-

When prompted to “Enter a file in which to save the key”, hit enter.

-

At the prompt, type a secure password.

-

Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

-
eval "$(ssh-agent -s)"
-ssh-add ~/.ssh/id_ed25519
-
-
-

Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account).

-
nano ~/.ssh/id_ed25519.pub
-
-
-

Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email).

-

Open a web browser and sign in to your GitHub account.

-

In the upper-right corner of any page, click your profile photo, then click Settings:

-
-../_images/ssh1.png -
-

In the user settings sidebar, click SSH and GPG keys:

-
-../_images/ssh2.png -
-

Click New SSH key: -

-../_images/ssh3.png -
-

In the Title field, add a descriptive label for the new key, such as “robot0”.

-

Paste your key into the Key field (contents of the .pub file).

-

Click Add SSH key.

-
-

Update Alternatives#

-

Python3 is installed in Ubuntu 22.04 by default. Some ROS packages utilize the “python” command instead of “python3” so we need to create a new executable, “/usr/bin/python” that will call the Python3 (basically use the command “python” to call Python3):

-
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10
-
-
-
-
-
-
-

ROS2 Humble#

-

At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the TurtleBot3. All of these instructions are adapted from the ROS2 Documentation and The Robotics Back-End -. ROS 2 Humble is the latest version of ROS 2 that supports Ubuntu 22.04.

-
-

Installation#

-

Make sure you have a locale which supports UTF-8.

-
locale  # check for UTF-8
-
-sudo apt update && sudo apt install locales
-sudo locale-gen en_US en_US.UTF-8
-sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
-export LANG=en_US.UTF-8
-
-locale  # verify settings
-
-
-

First ensure that the Ubuntu Universe repository is enabled.

-
sudo apt install software-properties-common
-sudo add-apt-repository universe
-
-
-

Set up keys:

-
sudo apt update && sudo apt install curl -y
-sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
-
-
-

Then add the repository to your sources list.

-
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ro
-
-
-

Install ROS 2 Humble:

-
sudo apt update
-sudo apt upgrade
-sudo apt install ros-humble-ros-base
-
-
-

The base version provides the Bare Bones of ROS 2 to include minimum packaging, build, and communications libraries. No GUI tools are installed. As the Raspberry Pi is embedded into TurtleBot it is ideal to keep overhead as low as possible. Many of the GUI tools will be ran on the main machine.

-

Install colcon (build tool)

-

ROS2 uses colcon as a build tool (and ament as the build system). When you only install the ROS2 core packages, colcon is not here, so install it manually.

-
sudo apt install python3-colcon-common-extensions
-
-
-

Setup ROS environment variables and setup scripts within the ~/.bashrc file. If you don’t want to have to source the setup file every time you open a new shell, then add the command to your shell startup script:

-
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
-
-
-

Any time you make changes to your ~/.bashrc file you must source it:

-
source ~/.bashrc
-
-
-

Sourcing ROS 2 setup files will set several environment variables necessary for operating ROS 2. If you ever have problems finding or using your ROS 2 packages, make sure that your environment is properly set up using the following command:

-
printenv | grep -i ROS
-
-
-

Check that variables like ROS_DISTRO and ROS_VERSION are set.

-
ROS_VERSION=2
-ROS_PYTHON_VERSION=3
-ROS_DISTRO=humble
-
-
-
-
-
-

TurtleBot3#

-

ToDo: need to update this section for ROS2

-
sudo apt install libudev-dev ros-noetic-turtlebot3-msgs
-cd ~/robot_ws/src
-git clone -b develop https://github.com/ROBOTIS-GIT/ld08_driver.git
-git clone https://github.com/ROBOTIS-GIT/turtlebot3.git
-git clone https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git
-
-
-
-

ECE387 Curriculum#

-
git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git
-
-
-

The ece387_curriculum package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool:

-
cd ~/robot_ws
-rosdep install --from-paths src --ignore-src -r -y
-
-
-

This will take a while.

-

Now we can make and source our workspace:

-
cd ~/robot_ws
-catkin_make
-source ~/.bashrc
-
-
-

The last set of dependencies we need to install are Python dependencies. These are listed within our ece387_curriculum package and can be installed using the pip3 tool:

-
roscd ece387_curriculum
-pip3 install -r requirements.txt
-
-
-
-

📝️ Note: the “dlib” package will take quite a while to install.

-
- - -
-
-

Updating OpenCR firmware#

-

The last step is updating the firmware for the OpenCR controller board.

-

Install required packages on the Raspberry Pi

-
sudo dpkg --add-architecture armhf
-sudo apt-get update
-sudo apt-get install libc6:armhf
-
-
-

Setup the OpenCR model name:

-
export OPENCR_PORT=/dev/ttyACM0
-export OPENCR_MODEL=burger_noetic
-rm -rf ./opencr_update.tar.bz2
-
-
-

Download the firmware and loader, then extract the file:

-
wget https://github.com/ROBOTIS-GIT/OpenCR-Binaries/raw/master/turtlebot3/ROS1/latest/opencr_update.tar.bz2 
-tar -xvf opencr_update.tar.bz2 
-
-
-

Upload firmware to the OpenCR:

-
cd ./opencr_update
-./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr
-
-
-

A successful firmware upload for TurtleBot3 Burger will look like: -lobo

-

If not successful, attempt the debug methods in the OpenCR Setup guide.

- -

Created by Steve Beyer, 2022 -Updated by Stan Baek, 2023

-
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - - \ No newline at end of file diff --git a/Module00.2_MasterSetup/MasterSetup.html b/Module00.2_MasterSetup/MasterSetup.html deleted file mode 100755 index 0f6badb..0000000 --- a/Module00.2_MasterSetup/MasterSetup.html +++ /dev/null @@ -1,759 +0,0 @@ - - - - - - - - - - - - Master Setup — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Master Setup#

-

This guide will walk through the steps to install Ubuntu Desktop 20.04 LTS, ROS Noetic, and all dependencies on a desktop computer. This computer system is utilized in the United States Air Force Academy’s Electrical and Computer Engineering department in an embedded network with the ground robot, a TurtleBot3 Burger. The master system is used to host roscore, utilize ROS GUI tools, and create secure connections with the TurtleBot3. This guide is adapted from the TurtleBot3 e-Manual.

-
-
-

Hardware#

-

For our application, we are using Intel NUC Kits but these instructions will work on any AMD64 architecture.

-
-
-

Software#

-
-

Download Ubuntu and flash USB#

-

For the desktop machine you will first need to download Ubuntu Desktop 20.04 LTS.

-

Once downloaded, follow the instructions to create a bootable Ubuntu USB stick within Ubuntu. The guide provides links to create USB sticks from Windows and macOS as well.

-

Once the bootable USB stick is created, follow the guide to Install Ubuntu desktop selecting a useful computer name such as master0. The NUC requires you to press and hold F10 on startup to boot from a USB stick.

- -
-

Setup GitHub SSH Keys#

-

The following assumes you already have a GitHub account.

-

Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login:

-
cd
-ssh-keygen -t ed25519 -C "github@email.com"
-
-
-

When prompted to “Enter a file in which to save the key”, hit enter.

-

At the prompt, type a secure password.

-

Start the ssh-agent in the background and add your SSH private key to the ssh-agent:

-
eval "$(ssh-agent -s)"
-ssh-add ~/.ssh/id_ed25519
-
-
-

Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account).

-
nano ~/.ssh/id_ed25519.pub
-
-
-

Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email).

-

Open a web browser and sign in to your GitHub account.

-

In the upper-right corner of any page, click your profile photo, then click Settings:

-

logo

-

In the user settings sidebar, click SSH and GPG keys:

-

logo

-

Click New SSH key:

-

logo

-

In the “Title” field, add a descriptive label for the new key, such as “master0”.

-

Paste your key into the “Key” field (contents of the .pub file).

-

Click Add SSH key.

- - -
-
-

Update Alternatives#

-

Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the “python” command instead of “python3” so we need to create a new executable, “/usr/bin/python” that will call Python3 (basically use the command “python” to call Python3):

-
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10
-
-
-
-
-

Additional Software#

-
sudo apt install jupyter-notebook
-jupyter contrib nbextension install --user
-
-
- - -
-
-
-

ROS Noetic#

-

At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the master. All of these instructions are adapted from the ROS wiki. ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal.

-
-

Installation#

-

Accept software from packages.ros.org:

-
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
-
-
-

Set up keys:

-
sudo apt install curl # if you haven't already installed curl
-curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
-
-
-

Install ROS Noetic:

-
sudo apt update
-sudo apt -y install ros-noetic-desktop-full
-
-
-

The full version provides theminimum packaging, build, communications libraries, GUI tools, and 2D/3D simulation and perception packages.

-

Install ROS dependencies for building packages:

-
sudo apt -y install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip xterm build-essential
-
-
-

Initialize rosdep

-
sudo rosdep init
-rosdep update
-
-
-

Source the ROS setup file:

-
source /opt/ros/noetic/setup.bash
-
-
-

Create your ROS workspace:

-
mkdir -p ~/master_ws/src
-cd ~/master_ws/
-catkin_make
-
-
-

Setup ROS environment variables and setup scripts within the ~/.bashrc file. Open the ~/.bashrc file with your favorite command line editor and add the following to the bottom:

-
source /opt/ros/noetic/setup.bash
-source ~/master_ws/devel/setup.bash
-export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share
-export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes
-export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master
-export EDITOR='nano -w' # replace with editor of choice used with rosed command
-export TURTLEBOT3_MODEL=burger
-export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR
-
-
-

Any time you make changes to your ~/.bashrc file you must source it:

-
source ~/.bashrc
-
-
-
-
-

Dependencies#

-

There are a number of ROS packages required to operate the TurtleBot3.

-
-
ROS Dependencies#
-
sudo apt-get install ros-noetic-joy ros-noetic-teleop-twist-joy \
-  ros-noetic-teleop-twist-keyboard ros-noetic-laser-proc \
-  ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \
-  ros-noetic-rosserial-python ros-noetic-rosserial-client \
-  ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \
-  ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \
-  ros-noetic-compressed-image-transport ros-noetic-rqt* ros-noetic-rviz \
-  ros-noetic-gmapping ros-noetic-navigation ros-noetic-interactive-markers
-
-
-
-
-
TurtleBot3 Dependencies#
-
sudo apt install ros-noetic-dynamixel-sdk ros-noetic-turtlebot3-msgs ros-noetic-turtlebot3
-
-
-
-
-
Simulation:#
-
cd ~/master_ws/src
-git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git
-
-
-
-
-
ECE387 Curriculum#
-
git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git
-
-
-

The ece387_curriculum package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool:

-
cd ~/master_ws
-rosdep install --from-paths src --ignore-src -r -y
-
-
-

This may take a while.

-

Now we can make and source our workspace:

-
catkin_make
-source ~/.bashrc
-
-
-

The last set of dependencies we need to install are Python dependencies. These are listed within our ece387_curriculum package and can be installed using the pip3 tool:

-
roscd ece387_curriculum
-pip3 install -r requirements.txt
-
-
-
-

📝️ Note: the “dlib” package will take quite a while to install.

-
- -
-
-
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - - \ No newline at end of file diff --git a/Module10_FinalProject/FinalProject.html b/Module10_FinalProject/FinalProject.html old mode 100755 new mode 100644 index 739c652..f769df5 --- a/Module10_FinalProject/FinalProject.html +++ b/Module10_FinalProject/FinalProject.html @@ -1,620 +1,620 @@ - - - - - - - - - - - - Module 10: Final Project — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

Module 10: Final Project

- -
- -
-
- - - - -
- -
-

Module 10: Final Project#

-
-

Purpose#

-

Students will utilize all previous modules to drive their robot in the DFEC halls. The LIDAR will be used to drive the robot centered between two walls, the IMU will be used to make 90/180/360 deg turns, OpenCV will be used to identify randomly placed stopped signs and act accordingly, and apriltag_ros will be used to navigate the robot throughout the halls (when the robot is a certain distance from the apriltag then turn left, right, around, or the robot has reached the goal depending on the tag ID).

-
-
-

Design#

-

The first step to a complex task such as this is to plan. You will not be successful if you just start trying to code a solution. Therefore, you should not write ANY code until you have completed the design phase of the project. There are a number of tools to help with design, but we will focus on two: graphs and finite state machines.

-

Graphs: -Build a notional ROS graph for your application using PowerPoint or another digital tool. You have a lot of practice up to this point using the rqt_graph tool, but this time you will design the graph before the project. Ensure your graph includes all nodes and topics you foresee using and which machine these will operate on.

-

Finite State Machine: -A finite state machine is a great way to plan out the logic flow of a complex problem. It allows you to visualize how your robot will transition states (current condition of the robot: e.g., driving forward) based on the inputs received from all of the sensors. Your “base state” will be driving forward. The rest is up to your design. For simplicity, we will use a Moore Machine, so the output only relies on the current state.

-

A quick intro on finite state machines from ECE382 is here: ECE382: Module 7 Part 1

-
-

📝️ Note: Make sure you include both the graph and FSM in your final report in the design section under System level design (3.3)

-
-
-
-

Design Presentation#

-

Your team will present both the ROS graph and Finite State Machine in class in a presentation taking no more than 10 minutes.

-
-
-

Implementation#

-

The robot will start at an undisclosed location within the large halls of DFEC. When traveling between two walls (the halls are approximately 4 meters wide), the robot should stay in the middle of the hall. There will be an AprilTag at the end of each hall providing turn commands:

-
    -
  • ID 0: turn left

  • -
  • ID 1: turn right

  • -
  • ID 2: turn around

  • -
-

You should stop halfway in the intersection (approximately 2 meters from the tag) and turn accordingly. Stop signs will be randomly placed throughout the halls. The robot should stop approximately 1 meter from the stop sign, wait five seconds, and then continue driving forward.

-

If at any time there is an open door in the hall, the robot should ignore the door and continue driving in the middle of the hall.

-

When the goal is discovered (AprilTag ID 3), the robot should stop within 0.5 meters from the goal and complete a 360 deg turn.

-
-
-

Demonstration#

-

Demonstrations will be accomplished on lesson 40 using a randomized course. Points will be deducted for failed checkpoints (e.g., does not stop and turn within approximately 2 meters of AprilTag 0). The final rubric is below and each item is worth 6 points for a total of 60 points assigned to the demonstration:

-
    -
  • Wall following

  • -
  • Ignore doors

  • -
  • AprilTag0: Turn left

  • -
  • AprilTag1: Turn right

  • -
  • AprilTag2: Turn around

  • -
  • AprilTag3: Turn 360

  • -
  • Stop halfway in intersectiom

  • -
  • Stop 1.0 meters from stop sign

  • -
  • Wait 5 seconds after stop sign detection

  • -
  • Stop within 0.5 meters from AprilTag3

  • -
-
-
-

Turn-in Requirements#

-

[10 points] Design Presentation

-

[60 points] Demonstration

-

[30 points] Report and code

-
-

⚠️ Warning: The final project is 25% of your final grade!

-
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Module 10: Final Project — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Module 10: Final Project

+ +
+ +
+
+ + + + +
+ +
+

Module 10: Final Project#

+
+

Purpose#

+

Students will utilize all previous modules to drive their robot in the DFEC halls. The LIDAR will be used to drive the robot centered between two walls, the IMU will be used to make 90/180/360 deg turns, OpenCV will be used to identify randomly placed stopped signs and act accordingly, and apriltag_ros will be used to navigate the robot throughout the halls (when the robot is a certain distance from the apriltag then turn left, right, around, or the robot has reached the goal depending on the tag ID).

+
+
+

Design#

+

The first step to a complex task such as this is to plan. You will not be successful if you just start trying to code a solution. Therefore, you should not write ANY code until you have completed the design phase of the project. There are a number of tools to help with design, but we will focus on two: graphs and finite state machines.

+

Graphs: +Build a notional ROS graph for your application using PowerPoint or another digital tool. You have a lot of practice up to this point using the rqt_graph tool, but this time you will design the graph before the project. Ensure your graph includes all nodes and topics you foresee using and which machine these will operate on.

+

Finite State Machine: +A finite state machine is a great way to plan out the logic flow of a complex problem. It allows you to visualize how your robot will transition states (current condition of the robot: e.g., driving forward) based on the inputs received from all of the sensors. Your “base state” will be driving forward. The rest is up to your design. For simplicity, we will use a Moore Machine, so the output only relies on the current state.

+

A quick intro on finite state machines from ECE382 is here: ECE382: Module 7 Part 1

+
+

📝️ Note: Make sure you include both the graph and FSM in your final report in the design section under System level design (3.3)

+
+
+
+

Design Presentation#

+

Your team will present both the ROS graph and Finite State Machine in class in a presentation taking no more than 10 minutes.

+
+
+

Implementation#

+

The robot will start at an undisclosed location within the large halls of DFEC. When traveling between two walls (the halls are approximately 4 meters wide), the robot should stay in the middle of the hall. There will be an AprilTag at the end of each hall providing turn commands:

+
    +
  • ID 0: turn left

  • +
  • ID 1: turn right

  • +
  • ID 2: turn around

  • +
+

You should stop halfway in the intersection (approximately 2 meters from the tag) and turn accordingly. Stop signs will be randomly placed throughout the halls. The robot should stop approximately 1 meter from the stop sign, wait five seconds, and then continue driving forward.

+

If at any time there is an open door in the hall, the robot should ignore the door and continue driving in the middle of the hall.

+

When the goal is discovered (AprilTag ID 3), the robot should stop within 0.5 meters from the goal and complete a 360 deg turn.

+
+
+

Demonstration#

+

Demonstrations will be accomplished on lesson 40 using a randomized course. Points will be deducted for failed checkpoints (e.g., does not stop and turn within approximately 2 meters of AprilTag 0). The final rubric is below and each item is worth 6 points for a total of 60 points assigned to the demonstration:

+
    +
  • Wall following

  • +
  • Ignore doors

  • +
  • AprilTag0: Turn left

  • +
  • AprilTag1: Turn right

  • +
  • AprilTag2: Turn around

  • +
  • AprilTag3: Turn 360

  • +
  • Stop halfway in intersectiom

  • +
  • Stop 1.0 meters from stop sign

  • +
  • Wait 5 seconds after stop sign detection

  • +
  • Stop within 0.5 meters from AprilTag3

  • +
+
+
+

Turn-in Requirements#

+

[10 points] Design Presentation

+

[60 points] Demonstration

+

[30 points] Report and code

+
+

⚠️ Warning: The final project is 25% of your final grade!

+
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module1_ROS/ICE1_ListenerTalker.html b/Module1_ROS/ICE1_ListenerTalker.html old mode 100755 new mode 100644 index 98aceb8..9e7d906 --- a/Module1_ROS/ICE1_ListenerTalker.html +++ b/Module1_ROS/ICE1_ListenerTalker.html @@ -1,803 +1,803 @@ - - - - - - - - - - - - ICE1: Talker and Listener — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

ICE1: Talker and Listener#

-
-

Implementing the chat subscriber#

-
-

Import modules#

-
# import required modules
-import rospy
-from std_msgs.msg import String
-
-
-
-
-
-

Listener#

-

This function will create the subscriber (“listener”) used to receive chat messages from the publisher (“talker”).

-
def listener():
-    rospy.Subscriber('chat', String, callback_chat)
-
-
-

The above function creates the subscriber to the /chat topic. Every time a String message is sent over the topic the callback_chat() function is called. This is an interrupt that spins a new thread to call that function.

-
-

Callback function#

-

The callback function will log and display what the chat listener sent.

-
def callback_chat(message):
-    rospy.loginfo(rospy.get_caller_id() + " I heard %s", message.data)
-
-
-

The callback function receives the String message as an input (you can name this parameter anything, but it is helpful if it is a meaningful variable name). To access the actual message, we need to utilize the data attribute of the String message. If you browse to the documentation for the String Mesage, you will note that the message attribute is called data and it is of type string. This is why we use the command message.data.

-
-
-

Main#

-
def main():
-    rospy.init_node('listener')
-    try:
-        listener()
-        rospy.spin()
-    except rospy.ROSInterruptException:
-        pass
-
-
-

The above is similar to the talker, but adds the rospy.spin() function call to create an infinite loop to allow the subscriber to operate in the background.

-

In an actual Python script we will replace

-
def main()
-
-
-

with

-
if __name__ == "__main__":
-
-
-

This allows our python files to be imported into other python files that might also have a main() function.

-
-
-

Run the listener#

-
main()
-
-
-
[INFO] [1666756858.595079, 308.564000]: /listener I heard hello
-
-
-

At this point, the subscriber is waiting for the publisher to send a message. Browse back to your talker and type a message! You should see that message show up above after hitting enter in the talker Notebook.

-
-
-
-

Talker#

-
-

A note on this document#

-

This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

-
-
-

Purpose#

-

This Jupyter Notebook will delve a little deeper into ROS implementation. You will create a basic one way chat server that allows a user to send messages to another (both users will be on the same computer at this point in time).

-
-
-

Initialize ROS:#

-

The first step when utilizing ROS is to initialize roscore. There are two methods to accomplish this: first, by explicitly running roscore and second, by running a launch file (which will initialize roscore if it is not already running). During the first portion of this course we will explicitly run roscore and then take advantage of launch files later in the course.

-

Copy the following code and run it in a new terminal (use the shortcut ctrl+alt+t to open a new terminal window or select an open terminal and hit ctrl+shift+t to open a new terminal tab):

-

roscore

-
-
-

Implementing the chat publisher#

-
-

📝️ Note: The following is Python code that will be implemented within the Jupyter Notebook. Again, the Notebook is just a way to allow for code and text to coexist to help guide you through the ICE. You could take all of the code in this Notebook and put it within a Python file and it would work the same as it does here. The focus of this ICE is the ROS implementation. It is assumed you have a working knowledge of Python at this time so this Notebook will not go into a lot of background regarding the Python code. Module 3 will provide a Python refresher.

-
-
-
-

Import modules#

-
-

⚠️ Important: Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a * character. Do not move to the next step until the * is gone.

-
-
# import required modules
-import rospy
-from std_msgs.msg import String
-
-
-

Here, we have two modules, rospy and a message. The rospy module provides all ROS implementation to create nodes and publish messages on topics. The next line imports the String message from the std_msgs package which we will use to send our chat messages. The Standard ROS Messages include a number of common message types. You can find more information about these messages on the ROS wiki.

-
-
-

Talker Function#

-

One method to communicate in ROS is using a publisher/subscriber model. One node publishes messages over a topic and any other nodes can subscribe to this topic to receive the messages.

-

This function will create the publisher (“talker”) used to send chat messages to the subscriber (“listener”). It will read user input and publish the message.

-
def talker():
-    chat_pub = rospy.Publisher('chat', String, queue_size = 1)
-    rate = rospy.Rate(10) # 10 Hz
-    while not rospy.is_shutdown():
-        chat_str = input("Message: ")
-        rospy.loginfo("I sent %s", chat_str)
-        chat_pub.publish(chat_str)
-        rate.sleep()
-
-
-

Line 2 creates the publisher. The publisher will publish String messages over the /chat topic. The queue_size parameter determines how many messages the ROS network will hold before dropping old messages. If the publisher publishes faster than the network can handle, then messages will start getting dropped.

-

Line 3 determines the rate at which the following loop should run. With an input of 10, the loop should run 10 times per second (10 Hz). Line 4 creates a loop that runs until ctrl+c is pressed in the terminal. Line 5 gets user input while line 6 logs the message into a log file (and prints it to the screen). Line 7 publishes the chat message using the previously created publisher. Lastly, line 8 sleeps so the loop runs at the desired rate.

-
-

📝️ Note: Waiting for user input will cause the loop to not run at 10 Hz as line 5 will block until the user hits enter.

-
-
-
-

Main#

-

The main function calls our talker function.

-
def main():
-    rospy.init_node('talker')
-    try:
-        talker()
-    except rospy.ROSInterruptException:
-        pass
-
-
-

Line 2 of the above code initializes our talker node. The rest creates a try-except statement that calls our talker function. All the try/except really does is ensures we exit cleanly when ctrl+c is pressed in a terminal.

-

In an actual Python script we will replace

-
def main()
-
-
-

with

-
if __name__ == "__main__":
-
-
-

This allows our python files to be imported into other python files that might also have a main() function.

-
-
-

Run the talker#

-
main()
-
-
-
-
-

Create the listener#

-

At this point the talker is waiting for user input. Don’t start typing yet, though! We need to implement and run our listener.

-
-
-

ROS commands#

-

Note that the Jupyter code block for the main() function call on both the talker and listener has an * on the left side. That is due to the infinite loops in the talker and main functions. This means that those functions are blocking and no other Jupyter code blocks will run in these two notebooks. We have to open a new notebook to run the ROS commands we would use to investigate the state of our ROS system. This would be equivalent to opening a new terminal on the Linux computer. Open the ROS notebook and follow the instructions.

-
-
-
-

ROS#

-

Below you will see the ROS commands you will use throughout this course to investigate your ROS system and write your lab reports. The “!” character in the front allows us to run bash commands from Jupyter and would NOT be used in the command line.

-

With the talker and listener nodes running execute the below commands.

-

List all running nodes:

-
$ rosnode list
-
-
-

You should see the listener and talker nodes. The /rosout node is created when running roscore and facilitates communication in the network. You can ignore this node in your lab reports.

-

Get more information about the /listener node:

-
$ rosnode info /listener
-
-
-

You can see what topics the node is publishing and subscribing to. It publishes to the ROS log file (for debugging) and subscribes to the /chat topic.

-

List the active topics:

-
$ rostopic list
-
-
-

The first topic is the one we created. The last two are created by roscore and can be ignored.

-

Show information about the /chat topic such as what type of messages are sent over the topic and publishing and subscribing nodes.

-
$ rostopic info /chat
-
-
-

As expected, the talker node is publishing to the /chat topic while the listener node subscribes.

-

Display running nodes and communication between them:

-
$ rqt_graph
-
-
-

Close the rqt_graph.

-

Display information about the message that is sent over the /chat topic.

-
$ rostopic type /chat | rosmsg show
-
-
-

The output of the command is the same as the information we saw from the ROS documentation. Again, to access the message we have to use the data attribute.

-

Display messages sent over the /chat topic:

-
$ rostopic echo /chat
-
-
-

In the ICE1_Talker notebook send a message to the listener. You should see that message show up both here and at the listener. This echo tool is useful to ensure your nodes are sending the messages as expected.

-
-
-

Checkpoint#

-

Once complete, get checked off by an instructor showing the output of each of the above.

-
-
-

Cleanup#

-

In each of the notebooks reset the Jupyter kernel and clear output (at the top menu bar select “Kernel” and “Restart & Clear Output”). Close each notebook. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + ICE1: Talker and Listener — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

ICE1: Talker and Listener#

+
+

Implementing the chat subscriber#

+
+

Import modules#

+
# import required modules
+import rospy
+from std_msgs.msg import String
+
+
+
+
+
+

Listener#

+

This function will create the subscriber (“listener”) used to receive chat messages from the publisher (“talker”).

+
def listener():
+    rospy.Subscriber('chat', String, callback_chat)
+
+
+

The above function creates the subscriber to the /chat topic. Every time a String message is sent over the topic the callback_chat() function is called. This is an interrupt that spins a new thread to call that function.

+
+

Callback function#

+

The callback function will log and display what the chat listener sent.

+
def callback_chat(message):
+    rospy.loginfo(rospy.get_caller_id() + " I heard %s", message.data)
+
+
+

The callback function receives the String message as an input (you can name this parameter anything, but it is helpful if it is a meaningful variable name). To access the actual message, we need to utilize the data attribute of the String message. If you browse to the documentation for the String Mesage, you will note that the message attribute is called data and it is of type string. This is why we use the command message.data.

+
+
+

Main#

+
def main():
+    rospy.init_node('listener')
+    try:
+        listener()
+        rospy.spin()
+    except rospy.ROSInterruptException:
+        pass
+
+
+

The above is similar to the talker, but adds the rospy.spin() function call to create an infinite loop to allow the subscriber to operate in the background.

+

In an actual Python script we will replace

+
def main()
+
+
+

with

+
if __name__ == "__main__":
+
+
+

This allows our python files to be imported into other python files that might also have a main() function.

+
+
+

Run the listener#

+
main()
+
+
+
[INFO] [1666756858.595079, 308.564000]: /listener I heard hello
+
+
+

At this point, the subscriber is waiting for the publisher to send a message. Browse back to your talker and type a message! You should see that message show up above after hitting enter in the talker Notebook.

+
+
+
+

Talker#

+
+

A note on this document#

+

This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

+
+
+

Purpose#

+

This Jupyter Notebook will delve a little deeper into ROS implementation. You will create a basic one way chat server that allows a user to send messages to another (both users will be on the same computer at this point in time).

+
+
+

Initialize ROS:#

+

The first step when utilizing ROS is to initialize roscore. There are two methods to accomplish this: first, by explicitly running roscore and second, by running a launch file (which will initialize roscore if it is not already running). During the first portion of this course we will explicitly run roscore and then take advantage of launch files later in the course.

+

Copy the following code and run it in a new terminal (use the shortcut ctrl+alt+t to open a new terminal window or select an open terminal and hit ctrl+shift+t to open a new terminal tab):

+

roscore

+
+
+

Implementing the chat publisher#

+
+

📝️ Note: The following is Python code that will be implemented within the Jupyter Notebook. Again, the Notebook is just a way to allow for code and text to coexist to help guide you through the ICE. You could take all of the code in this Notebook and put it within a Python file and it would work the same as it does here. The focus of this ICE is the ROS implementation. It is assumed you have a working knowledge of Python at this time so this Notebook will not go into a lot of background regarding the Python code. Module 3 will provide a Python refresher.

+
+
+
+

Import modules#

+
+

⚠️ Important: Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a * character. Do not move to the next step until the * is gone.

+
+
# import required modules
+import rospy
+from std_msgs.msg import String
+
+
+

Here, we have two modules, rospy and a message. The rospy module provides all ROS implementation to create nodes and publish messages on topics. The next line imports the String message from the std_msgs package which we will use to send our chat messages. The Standard ROS Messages include a number of common message types. You can find more information about these messages on the ROS wiki.

+
+
+

Talker Function#

+

One method to communicate in ROS is using a publisher/subscriber model. One node publishes messages over a topic and any other nodes can subscribe to this topic to receive the messages.

+

This function will create the publisher (“talker”) used to send chat messages to the subscriber (“listener”). It will read user input and publish the message.

+
def talker():
+    chat_pub = rospy.Publisher('chat', String, queue_size = 1)
+    rate = rospy.Rate(10) # 10 Hz
+    while not rospy.is_shutdown():
+        chat_str = input("Message: ")
+        rospy.loginfo("I sent %s", chat_str)
+        chat_pub.publish(chat_str)
+        rate.sleep()
+
+
+

Line 2 creates the publisher. The publisher will publish String messages over the /chat topic. The queue_size parameter determines how many messages the ROS network will hold before dropping old messages. If the publisher publishes faster than the network can handle, then messages will start getting dropped.

+

Line 3 determines the rate at which the following loop should run. With an input of 10, the loop should run 10 times per second (10 Hz). Line 4 creates a loop that runs until ctrl+c is pressed in the terminal. Line 5 gets user input while line 6 logs the message into a log file (and prints it to the screen). Line 7 publishes the chat message using the previously created publisher. Lastly, line 8 sleeps so the loop runs at the desired rate.

+
+

📝️ Note: Waiting for user input will cause the loop to not run at 10 Hz as line 5 will block until the user hits enter.

+
+
+
+

Main#

+

The main function calls our talker function.

+
def main():
+    rospy.init_node('talker')
+    try:
+        talker()
+    except rospy.ROSInterruptException:
+        pass
+
+
+

Line 2 of the above code initializes our talker node. The rest creates a try-except statement that calls our talker function. All the try/except really does is ensures we exit cleanly when ctrl+c is pressed in a terminal.

+

In an actual Python script we will replace

+
def main()
+
+
+

with

+
if __name__ == "__main__":
+
+
+

This allows our python files to be imported into other python files that might also have a main() function.

+
+
+

Run the talker#

+
main()
+
+
+
+
+

Create the listener#

+

At this point the talker is waiting for user input. Don’t start typing yet, though! We need to implement and run our listener.

+
+
+

ROS commands#

+

Note that the Jupyter code block for the main() function call on both the talker and listener has an * on the left side. That is due to the infinite loops in the talker and main functions. This means that those functions are blocking and no other Jupyter code blocks will run in these two notebooks. We have to open a new notebook to run the ROS commands we would use to investigate the state of our ROS system. This would be equivalent to opening a new terminal on the Linux computer. Open the ROS notebook and follow the instructions.

+
+
+
+

ROS#

+

Below you will see the ROS commands you will use throughout this course to investigate your ROS system and write your lab reports. The “!” character in the front allows us to run bash commands from Jupyter and would NOT be used in the command line.

+

With the talker and listener nodes running execute the below commands.

+

List all running nodes:

+
$ rosnode list
+
+
+

You should see the listener and talker nodes. The /rosout node is created when running roscore and facilitates communication in the network. You can ignore this node in your lab reports.

+

Get more information about the /listener node:

+
$ rosnode info /listener
+
+
+

You can see what topics the node is publishing and subscribing to. It publishes to the ROS log file (for debugging) and subscribes to the /chat topic.

+

List the active topics:

+
$ rostopic list
+
+
+

The first topic is the one we created. The last two are created by roscore and can be ignored.

+

Show information about the /chat topic such as what type of messages are sent over the topic and publishing and subscribing nodes.

+
$ rostopic info /chat
+
+
+

As expected, the talker node is publishing to the /chat topic while the listener node subscribes.

+

Display running nodes and communication between them:

+
$ rqt_graph
+
+
+

Close the rqt_graph.

+

Display information about the message that is sent over the /chat topic.

+
$ rostopic type /chat | rosmsg show
+
+
+

The output of the command is the same as the information we saw from the ROS documentation. Again, to access the message we have to use the data attribute.

+

Display messages sent over the /chat topic:

+
$ rostopic echo /chat
+
+
+

In the ICE1_Talker notebook send a message to the listener. You should see that message show up both here and at the listener. This echo tool is useful to ensure your nodes are sending the messages as expected.

+
+
+

Checkpoint#

+

Once complete, get checked off by an instructor showing the output of each of the above.

+
+
+

Cleanup#

+

In each of the notebooks reset the Jupyter kernel and clear output (at the top menu bar select “Kernel” and “Restart & Clear Output”). Close each notebook. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module1_ROS/ROS.html b/Module1_ROS/ROS.html old mode 100755 new mode 100644 index ce80f5a..72027c3 --- a/Module1_ROS/ROS.html +++ b/Module1_ROS/ROS.html @@ -1,739 +1,739 @@ - - - - - - - - - - - - Robotics Operating System (ROS) — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

Robotics Operating System (ROS)

- -
- -
-
- - - - -
- -
-

Robotics Operating System (ROS)#

-
-

Purpose#

-

This lecture accompanies the introduction to ROS notetaker used in class. We will apply the knowledge you learned by interacting with a simulated TurlteBot3 Burger.

-
-
-

ROS Introduction.#

-

Robotics Operating System (https://www.ros.org/about-ros/):

-

The Robot Operating System (ROS) is a flexible framework for writing robot software. It is a collection of tools, libraries, and conventions that aim to simplify the task of creating complex and robust robot behavior across a wide variety of robotic platforms. ROS is sometimes called a meta operating system because it performs many functions of an operating system, but it requires a computer’s operating system such as Linux.

-

Why? Because creating truly robust, general-purpose robot software is hard. From the robot’s perspective, problems that seem trivial to humans often vary wildly between instances of tasks and environments. Dealing with these variations is so hard that no single individual, laboratory, or institution can hope to do it on their own.

-

As a result, ROS was built from the ground up to encourage collaborative robotics software development. For example, one laboratory might have experts in mapping indoor environments, and could contribute a world-class system for producing maps. Another group might have experts at using maps to navigate, and yet another group might have discovered a computer vision approach that works well for recognizing small objects in clutter. ROS was designed specifically for groups like these to collaborate and build upon each other’s work, as is described throughout this site.

-

Noetic Ninjemys (http://wiki.ros.org/noetic):

-
-
-

Setting up the terminal for ROS#

-
    -
  1. Open a new Linux terminal by pressing:

    -

    ctrl+alt+t -

    -
  2. -
  3. Change to your root directory:

    -

    cd

    -

    When the cd command has no arguments, it changes to the user’s home directory. The command cd .. will move up one level. -

    -
  4. -
  5. Print the working directory:

    -

    pwd -

    -
  6. -
  7. List the contents of the current directory:

    -

    ls -

    -
  8. -
  9. Change directory to your master_ws folder:

    -

    cd master_ws -

    -
  10. -
  11. List the contents of the current directory:

    -

    ls -

    -
  12. -
  13. Change directory to your devel folder:

    -

    cd devel -

    -
  14. -
  15. You should notice a setup.bash file in the devel folder. Source this file allowing you to call your ROS packages:

    -

    source setup.bash -

    -
  16. -
  17. Open your .bashrc file (a script that is ran every time a new terminal instance is opened):

    -

    nano ~/.bashrc

    -

    This command is ran with sudo priveleges as the .bashrc is a system level file. The ‘~’ character indicates the .bashrc file is in the user’s home directory and allows us to access it from anywhere in the file system (would be the same as using the absolute path nano /dfec/home/.bashrc ).

    -
  18. -
  19. Scroll to the bottom of the file. You should see a few lines of code such as the following:

    -
        source /opt/ros/noetic/setup.bash
    -    source ~/master_ws/devel/setup.bash
    -    export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share
    -    export ROS_MASTER_URI=http://master:11311
    -    export EDITOR='nano -w'
    -
    -
    -
    - The first three lines set up the ROS environment allowing access to built-in packages and packages within your master workspace. Line 4 establishes which system hosts the ROS network. You can replace master with your robot, and the entire ROS network would run on your robot. - Hit ctrl+s to save, then ctrl+x to exit. -
    - You need to source (execute) the .bashrc file any time the file is changed (the .bashrc file is ran every time a new terminal is opened, but since we haven't opened a new terminal yet, we have to run it manually). -
    -
    -
  20. -
  21. Execute the .bashrc file

    -

    source ~/.bashrc

    -
  22. -
-
-
-

Running the TurtleBot3 simulation#

-

The best way to learn about ROS is through implementation, however, let’s start off by playing around with a virtual TurtleBot3! The TurtleBot3 is a tool we will utilize throughout this course to apply and integrate robotics systems. Ultimately you will create a complex embedded robotics system to perform a specific dedicated task, such as navigating the halls of DFEC. But let’s see if we can get the robot to drive around first.

-
    -
  1. In a terminal (Pro tip: ctrl+alt+t opens a new terminal window, while ctrl+shift+t opens a new terminal tab in an existing window) and initialize the ROS network:

    -

    roscore

    -
  2. -
  3. That terminal is now occupied. Open a new terminal tab/window and launch the TurtleBot3 gazebo launch file (A launch file is a way to run one or more nodes at once, we will learn about launch files later):

    -

    roslaunch turtlebot3_gazebo turtlebot3_world.launch

    -
  4. -
-
-

⌨️ Syntax: roslaunch <package> <launchfile>

-
-
    -
  1. Open another terminal tab/window and launch the following:

    -

    roslaunch turtlebot3_gazebo turtlebot3_gazebo_rviz.launch

    -
  2. -
-

The launch files will run the nodes necessary to simulate our robot and opens two windows: Gazebo and rviz. Gazebo is a simulation tool that provides physics for our robot and enables the testing of algorithms prior to real-world implementation. You should see a TurtleBot3 sitting at (-2.0, -0.5) facing the positive x direction surrounded by a maze. Using the mouse, left-click and holding will pan, holding the scroll wheel will change the orientation of the camera, and scrolling will zoom in and out.

-

The second window is rviz, a tool that visualizes topics interacting in our system. You should see a number of red dots outlining the location of the obstacles in our simulation. These are objects detected by our LIDAR which is communicating over a scan topic. Using the mouse, left-click will change the orientation of the camera, holding the scroll wheel will pan, and scrolling will zoom in and out (would be nice if they were the same).

-

Don’t worry! We will learn more about both of these tools at a later time.

-

Let’s go ahead and take a look at what nodes and topics currently running in our system.

-

The “!” character in front of the following commands allows us to run bash commands from the Jupyter NB and would NOT be used in the command line.

-
rosnode list
-
-
-

There are five nodes running, the first two enable the Gazebo simulation, the third allows for the visualization of the simulated robot, the fourth is created every time roscore is ran and helps with communication in our network, and the last enables rviz.

-

Not too exciting yet, so lets see what topics are active.

-
rostopic list
-
-
-

There are a lot of topics that are utilized to simulate our robot. Our real-world robot will eventually have most of these, such as /cmd_vel, /imu, and /scan. These are topics that allow us to communicate with some of the simulated hardware, such as the orientation sensor (/imu), LIDAR (/scan), and driving the robot (/cmd_vel). The rest of the topics enable visualization and movement within the simulation and can be ignored for now.

-

Another useful tool for visualizing nodes and topics is called rqt_graph:

-
rqt_graph
-
-
-

All that is going on right now is Gazebo is publishing the position and scan data which rviz uses to visualize the robot. Close your rqt_graph and let’s add another node to make things a bit more interesting.

-

Earlier we saw the topic /cmd_vel. This is a topic used to send Twist messages to a robot. A Twist message includes linear and angular x, y, z values to move a robot in a 3-dimensional space (google ROS twist message for more information). Our robot only drives in 2-dimensions and the wheels can only move forward and backward, so we will only use the linear x (forward and backward) and angular z (turning) attributes of the Twist message. To drive our simulated robot, we need a node that can publish Twist messages to the /cmd_vel topic. Luckily, there is a pre-built node called Teleop_Twist_Keyboard that sends Twist messages using a keyboard!

-

Open a new terminal tab (select the terminal and press ctrl+shift+t) and run the following (Pro tip: in a Linux terminal if you type the first couple letters of a command and then hit tab it will autocomplete the command for you):

-

rosrun teleop_twist_keyboard teleop_twist_keyboard.py

-
-

⌨️ Syntax: rosrun <package> <executable>

-
-

To drive the robot use the ‘x’ key to decrease linear x speed to about .25 m/s and use the ‘c’ key to decrease angular z speed to about .5 rad/s. Now follow the directions to utilize the keyboard to drive the robot! You should see the robot move in the simulation.

-

Let’s take a look at our rqt_graph to see if anything has changed.

-
rqt_graph
-
-
-

You should now see the teleop_twist_keyboard node which sends messages over /cmd_vel topic to Gazebo. Close the rqt_graph window. Let’s run through a number of commands that will provide you more information about your ROS network. You will use these throughout the course to determine what is going on in your ROS network.

-
-
-

Common ROS commands#

-

The rosnode command allows us to interact with nodes. Typing any ROS command followd by --help will provide information about that command:

-
rosnode --help
-
-
-

Let’s get some information about our new node, teleop_twist_keyboard:

-
rosnode info /teleop_twist_keyboard
-
-
-

The output of the command lists the topics the node is publishing and subscribing to (here is where we can see it publishes on /cmd_vel).

-

The rostopic command interacts with topics.

-
rostopic --help
-
-
-

Some of the common rostopic commands we will use in this course are echo, hz, info, type, and list

-
rostopic list
-
-
-

Let’s get some information about the /cmd_vel topic.

-
rostopic info /cmd_vel
-
-
-

From the output we can see what nodes are publishing and subscribing to the /cmd_vel topic.

-

Echoing the topic will allow us to see what messages are sent over the topic. After running the below command, browse back to your teleop_twist_keyboard node and drive the robot. You should see the twist messages sent to Gazebo.

-
rostopic echo /cmd_vel
-
-
-
-

Note

-

When moving forward and backward (‘i’ and ‘,’ keys) only a linear x value is sent, when turning left or right (‘j’ and ‘l’ keys) only an angular z value is sent, and when arcing (‘u’, ‘o’, ‘m’, and ‘.’ keys) both a linear x and angular z value are sent.

-
-
-

Note

-

The previous command still has an * character to the left. This means this command is waiting for inputs and will block all future commands. To kill the command and restart the kernel in the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. This will allow future commands to run.

-
-

To learn more about the messages sent over the /cmd_vel topic we can use the type command and the rosmsg tool.

-
rostopic type /cmd_vel
-
-
-
rosmsg show geometry_msgs/Twist
-
-
-

Or in one combined command:

-
rostopic type /cmd_vel | rosmsg show
-
-
-
-
-

Cleanup#

-

Before moving on to the in-class exercise, close all running nodes. In the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. In each terminal window, close the node by typing ctrl+c. Ensure roscore is terminated before moving on to the ICE.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Robotics Operating System (ROS) — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Robotics Operating System (ROS)

+ +
+ +
+
+ + + + +
+ +
+

Robotics Operating System (ROS)#

+
+

Purpose#

+

This lecture accompanies the introduction to ROS notetaker used in class. We will apply the knowledge you learned by interacting with a simulated TurlteBot3 Burger.

+
+
+

ROS Introduction.#

+

Robotics Operating System (https://www.ros.org/about-ros/):

+

The Robot Operating System (ROS) is a flexible framework for writing robot software. It is a collection of tools, libraries, and conventions that aim to simplify the task of creating complex and robust robot behavior across a wide variety of robotic platforms. ROS is sometimes called a meta operating system because it performs many functions of an operating system, but it requires a computer’s operating system such as Linux.

+

Why? Because creating truly robust, general-purpose robot software is hard. From the robot’s perspective, problems that seem trivial to humans often vary wildly between instances of tasks and environments. Dealing with these variations is so hard that no single individual, laboratory, or institution can hope to do it on their own.

+

As a result, ROS was built from the ground up to encourage collaborative robotics software development. For example, one laboratory might have experts in mapping indoor environments, and could contribute a world-class system for producing maps. Another group might have experts at using maps to navigate, and yet another group might have discovered a computer vision approach that works well for recognizing small objects in clutter. ROS was designed specifically for groups like these to collaborate and build upon each other’s work, as is described throughout this site.

+

Noetic Ninjemys (http://wiki.ros.org/noetic):

+
+
+

Setting up the terminal for ROS#

+
    +
  1. Open a new Linux terminal by pressing:

    +

    ctrl+alt+t +

    +
  2. +
  3. Change to your root directory:

    +

    cd

    +

    When the cd command has no arguments, it changes to the user’s home directory. The command cd .. will move up one level. +

    +
  4. +
  5. Print the working directory:

    +

    pwd +

    +
  6. +
  7. List the contents of the current directory:

    +

    ls +

    +
  8. +
  9. Change directory to your master_ws folder:

    +

    cd master_ws +

    +
  10. +
  11. List the contents of the current directory:

    +

    ls +

    +
  12. +
  13. Change directory to your devel folder:

    +

    cd devel +

    +
  14. +
  15. You should notice a setup.bash file in the devel folder. Source this file allowing you to call your ROS packages:

    +

    source setup.bash +

    +
  16. +
  17. Open your .bashrc file (a script that is ran every time a new terminal instance is opened):

    +

    nano ~/.bashrc

    +

    This command is ran with sudo priveleges as the .bashrc is a system level file. The ‘~’ character indicates the .bashrc file is in the user’s home directory and allows us to access it from anywhere in the file system (would be the same as using the absolute path nano /dfec/home/.bashrc ).

    +
  18. +
  19. Scroll to the bottom of the file. You should see a few lines of code such as the following:

    +
        source /opt/ros/noetic/setup.bash
    +    source ~/master_ws/devel/setup.bash
    +    export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share
    +    export ROS_MASTER_URI=http://master:11311
    +    export EDITOR='nano -w'
    +
    +
    +
    + The first three lines set up the ROS environment allowing access to built-in packages and packages within your master workspace. Line 4 establishes which system hosts the ROS network. You can replace master with your robot, and the entire ROS network would run on your robot. + Hit ctrl+s to save, then ctrl+x to exit. +
    + You need to source (execute) the .bashrc file any time the file is changed (the .bashrc file is ran every time a new terminal is opened, but since we haven't opened a new terminal yet, we have to run it manually). +
    +
    +
  20. +
  21. Execute the .bashrc file

    +

    source ~/.bashrc

    +
  22. +
+
+
+

Running the TurtleBot3 simulation#

+

The best way to learn about ROS is through implementation, however, let’s start off by playing around with a virtual TurtleBot3! The TurtleBot3 is a tool we will utilize throughout this course to apply and integrate robotics systems. Ultimately you will create a complex embedded robotics system to perform a specific dedicated task, such as navigating the halls of DFEC. But let’s see if we can get the robot to drive around first.

+
    +
  1. In a terminal (Pro tip: ctrl+alt+t opens a new terminal window, while ctrl+shift+t opens a new terminal tab in an existing window) and initialize the ROS network:

    +

    roscore

    +
  2. +
  3. That terminal is now occupied. Open a new terminal tab/window and launch the TurtleBot3 gazebo launch file (A launch file is a way to run one or more nodes at once, we will learn about launch files later):

    +

    roslaunch turtlebot3_gazebo turtlebot3_world.launch

    +
  4. +
+
+

⌨️ Syntax: roslaunch <package> <launchfile>

+
+
    +
  1. Open another terminal tab/window and launch the following:

    +

    roslaunch turtlebot3_gazebo turtlebot3_gazebo_rviz.launch

    +
  2. +
+

The launch files will run the nodes necessary to simulate our robot and opens two windows: Gazebo and rviz. Gazebo is a simulation tool that provides physics for our robot and enables the testing of algorithms prior to real-world implementation. You should see a TurtleBot3 sitting at (-2.0, -0.5) facing the positive x direction surrounded by a maze. Using the mouse, left-click and holding will pan, holding the scroll wheel will change the orientation of the camera, and scrolling will zoom in and out.

+

The second window is rviz, a tool that visualizes topics interacting in our system. You should see a number of red dots outlining the location of the obstacles in our simulation. These are objects detected by our LIDAR which is communicating over a scan topic. Using the mouse, left-click will change the orientation of the camera, holding the scroll wheel will pan, and scrolling will zoom in and out (would be nice if they were the same).

+

Don’t worry! We will learn more about both of these tools at a later time.

+

Let’s go ahead and take a look at what nodes and topics currently running in our system.

+

The “!” character in front of the following commands allows us to run bash commands from the Jupyter NB and would NOT be used in the command line.

+
rosnode list
+
+
+

There are five nodes running, the first two enable the Gazebo simulation, the third allows for the visualization of the simulated robot, the fourth is created every time roscore is ran and helps with communication in our network, and the last enables rviz.

+

Not too exciting yet, so lets see what topics are active.

+
rostopic list
+
+
+

There are a lot of topics that are utilized to simulate our robot. Our real-world robot will eventually have most of these, such as /cmd_vel, /imu, and /scan. These are topics that allow us to communicate with some of the simulated hardware, such as the orientation sensor (/imu), LIDAR (/scan), and driving the robot (/cmd_vel). The rest of the topics enable visualization and movement within the simulation and can be ignored for now.

+

Another useful tool for visualizing nodes and topics is called rqt_graph:

+
rqt_graph
+
+
+

All that is going on right now is Gazebo is publishing the position and scan data which rviz uses to visualize the robot. Close your rqt_graph and let’s add another node to make things a bit more interesting.

+

Earlier we saw the topic /cmd_vel. This is a topic used to send Twist messages to a robot. A Twist message includes linear and angular x, y, z values to move a robot in a 3-dimensional space (google ROS twist message for more information). Our robot only drives in 2-dimensions and the wheels can only move forward and backward, so we will only use the linear x (forward and backward) and angular z (turning) attributes of the Twist message. To drive our simulated robot, we need a node that can publish Twist messages to the /cmd_vel topic. Luckily, there is a pre-built node called Teleop_Twist_Keyboard that sends Twist messages using a keyboard!

+

Open a new terminal tab (select the terminal and press ctrl+shift+t) and run the following (Pro tip: in a Linux terminal if you type the first couple letters of a command and then hit tab it will autocomplete the command for you):

+

rosrun teleop_twist_keyboard teleop_twist_keyboard.py

+
+

⌨️ Syntax: rosrun <package> <executable>

+
+

To drive the robot use the ‘x’ key to decrease linear x speed to about .25 m/s and use the ‘c’ key to decrease angular z speed to about .5 rad/s. Now follow the directions to utilize the keyboard to drive the robot! You should see the robot move in the simulation.

+

Let’s take a look at our rqt_graph to see if anything has changed.

+
rqt_graph
+
+
+

You should now see the teleop_twist_keyboard node which sends messages over /cmd_vel topic to Gazebo. Close the rqt_graph window. Let’s run through a number of commands that will provide you more information about your ROS network. You will use these throughout the course to determine what is going on in your ROS network.

+
+
+

Common ROS commands#

+

The rosnode command allows us to interact with nodes. Typing any ROS command followd by --help will provide information about that command:

+
rosnode --help
+
+
+

Let’s get some information about our new node, teleop_twist_keyboard:

+
rosnode info /teleop_twist_keyboard
+
+
+

The output of the command lists the topics the node is publishing and subscribing to (here is where we can see it publishes on /cmd_vel).

+

The rostopic command interacts with topics.

+
rostopic --help
+
+
+

Some of the common rostopic commands we will use in this course are echo, hz, info, type, and list

+
rostopic list
+
+
+

Let’s get some information about the /cmd_vel topic.

+
rostopic info /cmd_vel
+
+
+

From the output we can see what nodes are publishing and subscribing to the /cmd_vel topic.

+

Echoing the topic will allow us to see what messages are sent over the topic. After running the below command, browse back to your teleop_twist_keyboard node and drive the robot. You should see the twist messages sent to Gazebo.

+
rostopic echo /cmd_vel
+
+
+
+

Note

+

When moving forward and backward (‘i’ and ‘,’ keys) only a linear x value is sent, when turning left or right (‘j’ and ‘l’ keys) only an angular z value is sent, and when arcing (‘u’, ‘o’, ‘m’, and ‘.’ keys) both a linear x and angular z value are sent.

+
+
+

Note

+

The previous command still has an * character to the left. This means this command is waiting for inputs and will block all future commands. To kill the command and restart the kernel in the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. This will allow future commands to run.

+
+

To learn more about the messages sent over the /cmd_vel topic we can use the type command and the rosmsg tool.

+
rostopic type /cmd_vel
+
+
+
rosmsg show geometry_msgs/Twist
+
+
+

Or in one combined command:

+
rostopic type /cmd_vel | rosmsg show
+
+
+
+
+

Cleanup#

+

Before moving on to the in-class exercise, close all running nodes. In the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. In each terminal window, close the node by typing ctrl+c. Ensure roscore is terminated before moving on to the ICE.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module2_Linux/Linux.html b/Module2_Linux/Linux.html old mode 100755 new mode 100644 index 04c2864..e65ef37 --- a/Module2_Linux/Linux.html +++ b/Module2_Linux/Linux.html @@ -1,883 +1,883 @@ - - - - - - - - - - - - Module 2: Linux for Robotics — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

Module 2: Linux for Robotics

- -
- -
-
- - - - -
- -
-

Module 2: Linux for Robotics#

-
-

Note

-

This document is known as a Jupyter notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

-
-
-

Purpose#

-

This Jupyter Notebook accompanies the introduction to Linux notetaker used in class. We will apply the knowledge you learned by interacting with the Ubuntu Operating System (OS) on the Master.

-
-
-

Linux Commands#

-

During class we went over a number of basic Linux commands. Open a terminal on the Master and let’s practice using those commands (use the shortcut ctrl+alt+t to open a new terminal window or select an open terminal and hit ctrl+shift+t to open a new terminal tab).

-

When observing the terminal (or Shell) you will note the syntax username@hostname:(i.e., on the master: dfec@masterX:, on the robot: pi@robotX), the current working directory (i.e., ~, which represents the home folder of the user), and lastly the ‘$’ character and a blinking cursor. This line is the prompt and the blinking cursor indicates the terminal is active and ready for commands.

-

Run the commands to move to the home folder and make a new directory:

-

cd

-

mkdir my_folder

-
-

💡️ Tip: In Linux, if you highlight a command you can paste it into the terminal by clicking the scroll wheel.

-
-

Change directories into your new folder:

-

cd my_folder

-

Create a new bash script we can use to drive the TurtleBot3:

-

touch move_turtlebot.sh

-

A bash script is a regular text file that allows you to run any command you would run within the terminal. We will use it to run a few ROS command line tools to move our TurtleBot.

-

We can use the Nano text editor to edit a file within the terminal. There are a number of text editors within the terminal and WWIII might be fought over which is best; some other options include Vim and Emacs. Nano is one of the more simple editors for quick edits. Feel free to use whichever works best for you, but all guidance within this course will be based on Nano.

-

Edit the new bash script:

-

nano move_turtlebot.sh

-

Copy the following into the script:

-
#!/bin/bash
-
-ARG1=$1
-
-if [ $ARG1 == 'forward' ]; then
-    rostopic pub /cmd_vel geometry_msgs/Twist "linear:
-    x: 0.15
-    y: 0.0
-    z: 0.0
-angular:
-    x: 0.0
-    y: 0.0
-    z: 0.0"
-        
-elif [ $ARG1 == 'rotate' ]; then
-    rostopic pub /cmd_vel geometry_msgs/Twist "linear:
-    x: 0.0
-    y: 0.0
-    z: 0.0
-angular:
-    x: 0.0
-    y: 0.0
-    z: 0.5"
-        
-elif [ $ARG1 == 'stop' ]; then
-    rostopic pub /cmd_vel geometry_msgs/Twist "linear:
-    x: 0.0
-    y: 0.0
-    z: 0.0
-angular:
-    x: 0.0
-    y: 0.0
-    z: 0.0"
-else
-echo "Please enter one of the following:
-    forward
-    rotate
-    stop"
-fi
-
-
-

Typing ctrl+s saves the file and then typing ctrl+x exits Nano.

-

Again, a bash script just runs commands exactly as you would within a terminal. The above code takes an argument and publishes a Twist message over the /cmd_vel topic to drive the robot accordingly.

-

Before running this script, let’s get our ROS environment setup:

-
    -
  1. Open a new terminal and type roscore.

  2. -
  3. Open a new terminal tab and run our TurtleBot3 simulation:

    -

    roslaunch turtlebot3_gazebo turtlebot3_empty_world.launch

    -
  4. -
-

Now, in a new terminal, run the script you created:

-

./move_turtlebot.sh

-

Did you get an error? That is because the permissions have not been properly set and you do not have execute permissions. You can observe the permissions of a file by typing ls -la.

-

For the move_turtlebot.sh file you should see -rw-rw-r--. The first position indicates file type (‘d’ for directory, ‘-’ for regular file), the next three positions indicate read (r), write (w), and execute (x) permissions for the file owner (u), the next three indicate permisions for the group owner of the file (g), and the last three characters indicate permissions for all other users (o).

-

You can change the permissions of a file or directory using the command chmod:

-
-

⌨️ Syntax: chmod <groups to assign the permissions><permissions to assign/remove> <file/folder>

-
-

For example, if we wanted to give the Owner execute permissions, you can enter the command:

-

chmod u+x move_turtlebot.sh

-

Typically we will give all users executable permissions (chmod +x move_turtlebot.sh). This isn’t the most secure thing to do, but in our controlled environment, it isn’t an issue. If you type ls -la now you should see the ‘x’ permission added for each permission group (-rwxrwxr-x).

-

Try running your script again:

-

./move_turtlebot.sh rotate

-
-

📝️ Note: You can remove permissions by utilizing the ‘-’ character instead of ‘+’.

-
-

Move to your github repo that you previously created:

-

cd ~/master_ws/src/ece387_master_sp23-USERNAME/master/

-

Now we are going to create a new ROS package. You learned this in the previous homework assignment, but we will continue practicing:

-

catkin_create_pkg module02 std_msgs rospy roscpp

-

Before we go any further, it is always a good idea to compile your workspace with the new package. To do that, we will use a command catkin_make from within the top-level of the workspace:

-

cd ~/master_ws

-

catkin_make

-

Now go back to the my_folder that you created at the beginning of the lesson and create a new bash script, bash_script.sh, to accomplish the following:

-
    -
  1. Moves into the package you just created (you will need to figure out the complete path to this folder)

  2. -
  3. Creates a directory called my_scripts

  4. -
  5. Moves into that directory

  6. -
  7. Creates a file called move_turtlebot_square.py

  8. -
  9. Lists all files showing the permissions

  10. -
  11. Modifies the permissions of the move_turtlebot_square.py file so all groups have executable permissions

  12. -
  13. Lists all files again showing the updated permissions

  14. -
-

Now run the script. If successful you should see the file listed without and then with execute permissions.

-

We are done with this script so let’s remove it (you will need to either use the complete path to the file you want to remove, or be in the directory of the file you want to remove). The rm command can remove folders or files. If you want to learn more about a command there are two helpful tools: Help: rm --help; Manual: man rm.

-

Type the following to remove our bash script:

-

rm bash_script.sh.

-
-

Note

-

To delete a whole folder add the -r tag to remove directories and thier contents recursively (e.g., rm -r my_folder, but don’t remove your folder just yet).

-
-

We can copy (cp, just like ctrl+c in a GUI) and move (mv, just like ctrl+x in a GUI) files and folders as well. Let’s copy the move_turtlebot.sh to the my_scripts folder you created earlier:

-

cp move_turtlebot.sh ~/master_ws/src/ece387_master_sp23-USERNAME/master/module02/my_scripts

-
-

⌨️ Syntax: cp <source> <destination>

-
-
-

Note

-

For the above to work, you must already be in the same folder as the move_turtlebot.sh file. Otherwise you have to use the absolute file path, such as ~/my_folder/move_turtlebot.sh.

-
-

You can now delete your my_folder folder.

-

cd ..

-

rm -r my_folder

-

Change directories to your my_scripts folder. We can use a ROS tool, roscd, to change directories to ROS packages without using the absolute file path:

-

roscd module02/my_scripts

-
-

⌨️ Syntax: roscd <package/folder>

-
-

Edit the move_turtlebot_square.py file and paste the following contents:

-
#!/usr/bin/env python3
-import rospy, time, math
-from geometry_msgs.msg import Twist
-
-class MoveTurtleBot():
-    def __init__(self):
-        self.pub = rospy.Publisher('cmd_vel', Twist, queue_size=1)
-        self.cmd = Twist()
-        self.ctrl_c = False
-        rospy.on_shutdown(self.shutdownhook)
-        self.rate = rospy.Rate(10)    # 10 Hz
-        
-    def publish_cmd_vel_once(self):
-        """
-        In case publishing fails on first attempt
-        """
-        while not self.ctrl_c:
-            connections = self.pub.get_num_connections()
-            if connections > 0:
-                self.pub.publish(self.cmd)
-                rospy.loginfo("Cmd Published")
-                break
-            else:
-                self.rate.sleep()
-                
-    def shutdownhook(self):
-        rospy.loginfo("Shutting down. Stopping TurtleBot!")
-        self.stop_turtlebot()
-        self.ctrl_c = True
-        
-    def stop_turtlebot(self):
-        self.cmd.linear.x = 0.0
-        self.cmd.angular.z = 0.0
-        self.publish_cmd_vel_once()
-        
-    def move_time(self, moving_time = 10.0, lin_spd = 0.2, ang_spd = 0.2):
-        self.cmd.linear.x = lin_spd
-        self.cmd.angular.z = ang_spd
-        
-        self.publish_cmd_vel_once()
-        time.sleep(moving_time)
-        
-    def move_square(self):
-        i = 0
-        
-        while not self.ctrl_c:
-            # Move Forward
-            self.move_time(moving_time = 2.0, lin_spd = 0.2, ang_spd = 0)
-            # Turn
-            ang_spd = 0.5    # rad/sec
-            moving_time = math.radians(90)/ang_spd
-            self.move_time(moving_time = moving_time, lin_spd = 0.0, ang_spd = ang_spd)
-            
-        
-if __name__ == '__main__':
-    rospy.init_node('move_turtlebot')
-    move_object = MoveTurtleBot()
-    try:
-        move_object.move_square()
-    except rospy.ROSInterruptException:
-        pass
-
-
-

There is a lot going on in this script, but hopefully after the previous lesson some of it makes sense. To summarize, the script creates a node, move_turtlebot, that publishes Twist messages to the /cmd_vel topic to drive the robot in a square: drive forward, turn 90 degrees, drive forward, repeat. This script will run until killed using ctrl+c.

-

The script is already executable, so you can run it using ROS!

-

rosrun module02 move_turtlebot_square.py

-
-

Note

-

Note:** It won’t be a perfect square as the robot doesn’t turn perfectly, but it will be close!

-
-

The robot is now driving in a square until the script is killed. If you select the terminal and hit ctrl+c, it will kill the script.

-

Run the script again and this time hit ctrl+z. You can see that the robot is still running, but the commands are not updating. This is because ctrl+z suspends the current process, but does not kill it. We can observe all running processes on Linux by typing ps -faux. As you can see, there are a lot! The grep command allows us to filter these processes. Try the following:

-

ps -faux | grep move_turtlebot_square.py

-

The first entry should be our process and the leftmost number is the process ID (PID). We can kill any process using this number and the kill command:

-

kill PID replacing PID with the number listed.

-

If you hit enter again, you should see that the process was killed. Unfortunately, the TurtleBot will just continue to execute the last command sent, so you need to kill the simulation as well. Just select that terminal and hit ctrl+c.

-

The grep tool is very powerful and can be used with any Linux command. For example, if you wanted to see all turtlebot packages available to us, we could type the following:

-

rospack list

-

There are a lot, so this isn’t very helpful, but we can filter this command!

-

rospack list | grep turtlebot

-

The vertical line, ‘|’, pipes the results of the first command into the second command, so we can filter all packages looking only for turtlebot packages.

-
-
-

Working with a remote machine#

-

Later in this course you will drive your TurtleBot around unplugged from a monitor and keyboard, but you will still need access to the Raspberry Pi on the robot to run ROS nodes. One of the easiest ways to remotely access a Linux machine is through a secure shell. To create a secure shell, you need the uesrname and hostname of the computer you want to remote into. For the Raspberry Pis, all usernames are pi and the hostname is your robot number (e.g., robot0).

-
    -
  1. Open a terminal on your master

  2. -
  3. Check connectivity to the robot:

    -

    ping robotX -

    -
  4. -
  5. Create a secure shell to the robot:

    -

    ssh pi@robotX

    -
  6. -
-
-

⌨️ Syntax: ssh <username>@<hostname/IP address>

-
-
    -
  1. Enter the robot’s password

    -

    You should see that the terminal lists the pi username and your robot’s hostname, robotX. Any command you run in this shell will execute on the robot. -

    -
  2. -
  3. Create a new package on the robot we can use to store scripts for this module. This package should be called ‘module02’ and should be in your student repo. From the terminal ssh’d into the robot, type:

  4. -
-

cd ece387_robot_sp23-USERNAME/robot/

-

catkin_create_pkg module02

-
    -
  1. Change directories to your new folder.

  2. -
  3. Print the working directory. You should get “/home/pi/robot_ws/src/ece387_robot_sp23-USERNAME/robot/”

  4. -
  5. You can type exit to close an SSH connection (but don’t close the connection yet!).

  6. -
-
-

📝️ Note: At times, there may be network issues and name resolution will fail. What this means is if you try to ping the robot from the master or vice versa using the hostname (e.g., ping master0) it will not work. However, it will work if you use the IP address (e.g., ping 192.168.2.120). To do this, you need the IP address of the machine you want to access. This IP address will change periodically. The easiest way to determine the current IP Address is, with the machine plugged into a monitor and keyboard, type ip addr in a terminal. This will list all of the network interfaces on the machine (such as eth0 for ethernet and wlan0 for wireless). You are looking for the inet field under wlan0 (may be called wlo1). Now you can unplug the roobt from the monitor, ping the IP address to check connectivity, and then SSH into the robot.

-
-

The other remote tool you may want to take advantage of is SCP, which securely copies a file to a remote machine.

-
-

⌨️ Syntax: scp <src location> <username>@<hostname>:<dest location>

-
-

For example, copy the move_turtlebot_square.py file to your robot by typing the following in a new terminal on the master:

-

roscd module02/my_scripts

-

scp move_turtlebot_square.py pi@robot0:/home/pi/robot_ws/src/ece387_robot_sp23-USERNAME/robot/module02/src/

-
-

📝️ Note: The destination uses the absolute path you printed earlier.

-
-

In your secure shell list the contents of your ‘my_scripts’ folder. You should see the move_turtlebot_square.py file. Check that it is executable. If it is not, make it executable.

-

Now let’s move our TurtleBot using the script on the robot:

-
    -
  1. Run roscore on the master.

  2. -
  3. Run the simulated robot on the master.

  4. -
  5. SSH into your robot Raspberry Pi.

  6. -
  7. Run the move_turtlebot_square.py using the rosrun command on the robot.

  8. -
-

You are now controlling your simulation (which is running on the master) remotely from the robot. In future lessons you will have nodes running on your robot to drive the robot and will control the robot remotely from the master.

-
-
-

ROS#

-

Below you will run ROS commands. The “!” character in the front allows us to run bash commands from Jupter and would NOT be used in the command line.

-

Accomplish the following on the master by adding the commands necessary below:

-

List all running nodes:

-

-
-
-

List the active topics:

-

-
-
-

Display running nodes and communication between them:

-

-
-
-

Exit the rqt_graph.

-

Show information about a the /cmd_vel topic such as what type of messages are sent over the topic and publishing and subscribing nodes.

-

-
-
-

Display information about the message that is sent over the /cmd_vel topic.

-

-
-
-

Display messages sent over the /cmd_vel topic:

-

-
-
-
-
-

Checkpoint#

-

Once complete, get checked off by an instructor showing the output of each of the above.

-
-

Note

-

You will use all of the above ROS commands for each lab to write your lab reports. You could create a bash script to run these commands automatically ;)

-
-

Additionally, place screen captures showing each of the commands running or the windows they bring up into a folder within ../module02/Pictures on your master repo. Then push the repo to get credit for this work.

-
-
-

Summary#

-

You have seen a lot of different Linux/ROS commands during this lesson but it only scratches the surface. There are tons of online resources available if you want to learn more. I recommend working through a few of these tutorials: http://www.ee.surrey.ac.uk/Teaching/Unix/. They are fairly quick and willl give you more insight into the tools we discussed.

-
-
-

Cleanup#

-

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections. In each of the notebooks reset the Jupter kernel and clear output. Now it is safe to exit out of this window. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Module 2: Linux for Robotics — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Module 2: Linux for Robotics

+ +
+ +
+
+ + + + +
+ +
+

Module 2: Linux for Robotics#

+
+

Note

+

This document is known as a Jupyter notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

+
+
+

Purpose#

+

This Jupyter Notebook accompanies the introduction to Linux notetaker used in class. We will apply the knowledge you learned by interacting with the Ubuntu Operating System (OS) on the Master.

+
+
+

Linux Commands#

+

During class we went over a number of basic Linux commands. Open a terminal on the Master and let’s practice using those commands (use the shortcut ctrl+alt+t to open a new terminal window or select an open terminal and hit ctrl+shift+t to open a new terminal tab).

+

When observing the terminal (or Shell) you will note the syntax username@hostname:(i.e., on the master: dfec@masterX:, on the robot: pi@robotX), the current working directory (i.e., ~, which represents the home folder of the user), and lastly the ‘$’ character and a blinking cursor. This line is the prompt and the blinking cursor indicates the terminal is active and ready for commands.

+

Run the commands to move to the home folder and make a new directory:

+

cd

+

mkdir my_folder

+
+

💡️ Tip: In Linux, if you highlight a command you can paste it into the terminal by clicking the scroll wheel.

+
+

Change directories into your new folder:

+

cd my_folder

+

Create a new bash script we can use to drive the TurtleBot3:

+

touch move_turtlebot.sh

+

A bash script is a regular text file that allows you to run any command you would run within the terminal. We will use it to run a few ROS command line tools to move our TurtleBot.

+

We can use the Nano text editor to edit a file within the terminal. There are a number of text editors within the terminal and WWIII might be fought over which is best; some other options include Vim and Emacs. Nano is one of the more simple editors for quick edits. Feel free to use whichever works best for you, but all guidance within this course will be based on Nano.

+

Edit the new bash script:

+

nano move_turtlebot.sh

+

Copy the following into the script:

+
#!/bin/bash
+
+ARG1=$1
+
+if [ $ARG1 == 'forward' ]; then
+    rostopic pub /cmd_vel geometry_msgs/Twist "linear:
+    x: 0.15
+    y: 0.0
+    z: 0.0
+angular:
+    x: 0.0
+    y: 0.0
+    z: 0.0"
+        
+elif [ $ARG1 == 'rotate' ]; then
+    rostopic pub /cmd_vel geometry_msgs/Twist "linear:
+    x: 0.0
+    y: 0.0
+    z: 0.0
+angular:
+    x: 0.0
+    y: 0.0
+    z: 0.5"
+        
+elif [ $ARG1 == 'stop' ]; then
+    rostopic pub /cmd_vel geometry_msgs/Twist "linear:
+    x: 0.0
+    y: 0.0
+    z: 0.0
+angular:
+    x: 0.0
+    y: 0.0
+    z: 0.0"
+else
+echo "Please enter one of the following:
+    forward
+    rotate
+    stop"
+fi
+
+
+

Typing ctrl+s saves the file and then typing ctrl+x exits Nano.

+

Again, a bash script just runs commands exactly as you would within a terminal. The above code takes an argument and publishes a Twist message over the /cmd_vel topic to drive the robot accordingly.

+

Before running this script, let’s get our ROS environment setup:

+
    +
  1. Open a new terminal and type roscore.

  2. +
  3. Open a new terminal tab and run our TurtleBot3 simulation:

    +

    roslaunch turtlebot3_gazebo turtlebot3_empty_world.launch

    +
  4. +
+

Now, in a new terminal, run the script you created:

+

./move_turtlebot.sh

+

Did you get an error? That is because the permissions have not been properly set and you do not have execute permissions. You can observe the permissions of a file by typing ls -la.

+

For the move_turtlebot.sh file you should see -rw-rw-r--. The first position indicates file type (‘d’ for directory, ‘-’ for regular file), the next three positions indicate read (r), write (w), and execute (x) permissions for the file owner (u), the next three indicate permisions for the group owner of the file (g), and the last three characters indicate permissions for all other users (o).

+

You can change the permissions of a file or directory using the command chmod:

+
+

⌨️ Syntax: chmod <groups to assign the permissions><permissions to assign/remove> <file/folder>

+
+

For example, if we wanted to give the Owner execute permissions, you can enter the command:

+

chmod u+x move_turtlebot.sh

+

Typically we will give all users executable permissions (chmod +x move_turtlebot.sh). This isn’t the most secure thing to do, but in our controlled environment, it isn’t an issue. If you type ls -la now you should see the ‘x’ permission added for each permission group (-rwxrwxr-x).

+

Try running your script again:

+

./move_turtlebot.sh rotate

+
+

📝️ Note: You can remove permissions by utilizing the ‘-’ character instead of ‘+’.

+
+

Move to your github repo that you previously created:

+

cd ~/master_ws/src/ece387_master_sp23-USERNAME/master/

+

Now we are going to create a new ROS package. You learned this in the previous homework assignment, but we will continue practicing:

+

catkin_create_pkg module02 std_msgs rospy roscpp

+

Before we go any further, it is always a good idea to compile your workspace with the new package. To do that, we will use a command catkin_make from within the top-level of the workspace:

+

cd ~/master_ws

+

catkin_make

+

Now go back to the my_folder that you created at the beginning of the lesson and create a new bash script, bash_script.sh, to accomplish the following:

+
    +
  1. Moves into the package you just created (you will need to figure out the complete path to this folder)

  2. +
  3. Creates a directory called my_scripts

  4. +
  5. Moves into that directory

  6. +
  7. Creates a file called move_turtlebot_square.py

  8. +
  9. Lists all files showing the permissions

  10. +
  11. Modifies the permissions of the move_turtlebot_square.py file so all groups have executable permissions

  12. +
  13. Lists all files again showing the updated permissions

  14. +
+

Now run the script. If successful you should see the file listed without and then with execute permissions.

+

We are done with this script so let’s remove it (you will need to either use the complete path to the file you want to remove, or be in the directory of the file you want to remove). The rm command can remove folders or files. If you want to learn more about a command there are two helpful tools: Help: rm --help; Manual: man rm.

+

Type the following to remove our bash script:

+

rm bash_script.sh.

+
+

Note

+

To delete a whole folder add the -r tag to remove directories and thier contents recursively (e.g., rm -r my_folder, but don’t remove your folder just yet).

+
+

We can copy (cp, just like ctrl+c in a GUI) and move (mv, just like ctrl+x in a GUI) files and folders as well. Let’s copy the move_turtlebot.sh to the my_scripts folder you created earlier:

+

cp move_turtlebot.sh ~/master_ws/src/ece387_master_sp23-USERNAME/master/module02/my_scripts

+
+

⌨️ Syntax: cp <source> <destination>

+
+
+

Note

+

For the above to work, you must already be in the same folder as the move_turtlebot.sh file. Otherwise you have to use the absolute file path, such as ~/my_folder/move_turtlebot.sh.

+
+

You can now delete your my_folder folder.

+

cd ..

+

rm -r my_folder

+

Change directories to your my_scripts folder. We can use a ROS tool, roscd, to change directories to ROS packages without using the absolute file path:

+

roscd module02/my_scripts

+
+

⌨️ Syntax: roscd <package/folder>

+
+

Edit the move_turtlebot_square.py file and paste the following contents:

+
#!/usr/bin/env python3
+import rospy, time, math
+from geometry_msgs.msg import Twist
+
+class MoveTurtleBot():
+    def __init__(self):
+        self.pub = rospy.Publisher('cmd_vel', Twist, queue_size=1)
+        self.cmd = Twist()
+        self.ctrl_c = False
+        rospy.on_shutdown(self.shutdownhook)
+        self.rate = rospy.Rate(10)    # 10 Hz
+        
+    def publish_cmd_vel_once(self):
+        """
+        In case publishing fails on first attempt
+        """
+        while not self.ctrl_c:
+            connections = self.pub.get_num_connections()
+            if connections > 0:
+                self.pub.publish(self.cmd)
+                rospy.loginfo("Cmd Published")
+                break
+            else:
+                self.rate.sleep()
+                
+    def shutdownhook(self):
+        rospy.loginfo("Shutting down. Stopping TurtleBot!")
+        self.stop_turtlebot()
+        self.ctrl_c = True
+        
+    def stop_turtlebot(self):
+        self.cmd.linear.x = 0.0
+        self.cmd.angular.z = 0.0
+        self.publish_cmd_vel_once()
+        
+    def move_time(self, moving_time = 10.0, lin_spd = 0.2, ang_spd = 0.2):
+        self.cmd.linear.x = lin_spd
+        self.cmd.angular.z = ang_spd
+        
+        self.publish_cmd_vel_once()
+        time.sleep(moving_time)
+        
+    def move_square(self):
+        i = 0
+        
+        while not self.ctrl_c:
+            # Move Forward
+            self.move_time(moving_time = 2.0, lin_spd = 0.2, ang_spd = 0)
+            # Turn
+            ang_spd = 0.5    # rad/sec
+            moving_time = math.radians(90)/ang_spd
+            self.move_time(moving_time = moving_time, lin_spd = 0.0, ang_spd = ang_spd)
+            
+        
+if __name__ == '__main__':
+    rospy.init_node('move_turtlebot')
+    move_object = MoveTurtleBot()
+    try:
+        move_object.move_square()
+    except rospy.ROSInterruptException:
+        pass
+
+
+

There is a lot going on in this script, but hopefully after the previous lesson some of it makes sense. To summarize, the script creates a node, move_turtlebot, that publishes Twist messages to the /cmd_vel topic to drive the robot in a square: drive forward, turn 90 degrees, drive forward, repeat. This script will run until killed using ctrl+c.

+

The script is already executable, so you can run it using ROS!

+

rosrun module02 move_turtlebot_square.py

+
+

Note

+

Note:** It won’t be a perfect square as the robot doesn’t turn perfectly, but it will be close!

+
+

The robot is now driving in a square until the script is killed. If you select the terminal and hit ctrl+c, it will kill the script.

+

Run the script again and this time hit ctrl+z. You can see that the robot is still running, but the commands are not updating. This is because ctrl+z suspends the current process, but does not kill it. We can observe all running processes on Linux by typing ps -faux. As you can see, there are a lot! The grep command allows us to filter these processes. Try the following:

+

ps -faux | grep move_turtlebot_square.py

+

The first entry should be our process and the leftmost number is the process ID (PID). We can kill any process using this number and the kill command:

+

kill PID replacing PID with the number listed.

+

If you hit enter again, you should see that the process was killed. Unfortunately, the TurtleBot will just continue to execute the last command sent, so you need to kill the simulation as well. Just select that terminal and hit ctrl+c.

+

The grep tool is very powerful and can be used with any Linux command. For example, if you wanted to see all turtlebot packages available to us, we could type the following:

+

rospack list

+

There are a lot, so this isn’t very helpful, but we can filter this command!

+

rospack list | grep turtlebot

+

The vertical line, ‘|’, pipes the results of the first command into the second command, so we can filter all packages looking only for turtlebot packages.

+
+
+

Working with a remote machine#

+

Later in this course you will drive your TurtleBot around unplugged from a monitor and keyboard, but you will still need access to the Raspberry Pi on the robot to run ROS nodes. One of the easiest ways to remotely access a Linux machine is through a secure shell. To create a secure shell, you need the uesrname and hostname of the computer you want to remote into. For the Raspberry Pis, all usernames are pi and the hostname is your robot number (e.g., robot0).

+
    +
  1. Open a terminal on your master

  2. +
  3. Check connectivity to the robot:

    +

    ping robotX +

    +
  4. +
  5. Create a secure shell to the robot:

    +

    ssh pi@robotX

    +
  6. +
+
+

⌨️ Syntax: ssh <username>@<hostname/IP address>

+
+
    +
  1. Enter the robot’s password

    +

    You should see that the terminal lists the pi username and your robot’s hostname, robotX. Any command you run in this shell will execute on the robot. +

    +
  2. +
  3. Create a new package on the robot we can use to store scripts for this module. This package should be called ‘module02’ and should be in your student repo. From the terminal ssh’d into the robot, type:

  4. +
+

cd ece387_robot_sp23-USERNAME/robot/

+

catkin_create_pkg module02

+
    +
  1. Change directories to your new folder.

  2. +
  3. Print the working directory. You should get “/home/pi/robot_ws/src/ece387_robot_sp23-USERNAME/robot/”

  4. +
  5. You can type exit to close an SSH connection (but don’t close the connection yet!).

  6. +
+
+

📝️ Note: At times, there may be network issues and name resolution will fail. What this means is if you try to ping the robot from the master or vice versa using the hostname (e.g., ping master0) it will not work. However, it will work if you use the IP address (e.g., ping 192.168.2.120). To do this, you need the IP address of the machine you want to access. This IP address will change periodically. The easiest way to determine the current IP Address is, with the machine plugged into a monitor and keyboard, type ip addr in a terminal. This will list all of the network interfaces on the machine (such as eth0 for ethernet and wlan0 for wireless). You are looking for the inet field under wlan0 (may be called wlo1). Now you can unplug the roobt from the monitor, ping the IP address to check connectivity, and then SSH into the robot.

+
+

The other remote tool you may want to take advantage of is SCP, which securely copies a file to a remote machine.

+
+

⌨️ Syntax: scp <src location> <username>@<hostname>:<dest location>

+
+

For example, copy the move_turtlebot_square.py file to your robot by typing the following in a new terminal on the master:

+

roscd module02/my_scripts

+

scp move_turtlebot_square.py pi@robot0:/home/pi/robot_ws/src/ece387_robot_sp23-USERNAME/robot/module02/src/

+
+

📝️ Note: The destination uses the absolute path you printed earlier.

+
+

In your secure shell list the contents of your ‘my_scripts’ folder. You should see the move_turtlebot_square.py file. Check that it is executable. If it is not, make it executable.

+

Now let’s move our TurtleBot using the script on the robot:

+
    +
  1. Run roscore on the master.

  2. +
  3. Run the simulated robot on the master.

  4. +
  5. SSH into your robot Raspberry Pi.

  6. +
  7. Run the move_turtlebot_square.py using the rosrun command on the robot.

  8. +
+

You are now controlling your simulation (which is running on the master) remotely from the robot. In future lessons you will have nodes running on your robot to drive the robot and will control the robot remotely from the master.

+
+
+

ROS#

+

Below you will run ROS commands. The “!” character in the front allows us to run bash commands from Jupter and would NOT be used in the command line.

+

Accomplish the following on the master by adding the commands necessary below:

+

List all running nodes:

+

+
+
+

List the active topics:

+

+
+
+

Display running nodes and communication between them:

+

+
+
+

Exit the rqt_graph.

+

Show information about a the /cmd_vel topic such as what type of messages are sent over the topic and publishing and subscribing nodes.

+

+
+
+

Display information about the message that is sent over the /cmd_vel topic.

+

+
+
+

Display messages sent over the /cmd_vel topic:

+

+
+
+
+
+

Checkpoint#

+

Once complete, get checked off by an instructor showing the output of each of the above.

+
+

Note

+

You will use all of the above ROS commands for each lab to write your lab reports. You could create a bash script to run these commands automatically ;)

+
+

Additionally, place screen captures showing each of the commands running or the windows they bring up into a folder within ../module02/Pictures on your master repo. Then push the repo to get credit for this work.

+
+
+

Summary#

+

You have seen a lot of different Linux/ROS commands during this lesson but it only scratches the surface. There are tons of online resources available if you want to learn more. I recommend working through a few of these tutorials: http://www.ee.surrey.ac.uk/Teaching/Unix/. They are fairly quick and willl give you more insight into the tools we discussed.

+
+
+

Cleanup#

+

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections. In each of the notebooks reset the Jupter kernel and clear output. Now it is safe to exit out of this window. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module3_Python3/ICE3_ClientServer.html b/Module3_Python3/ICE3_ClientServer.html old mode 100755 new mode 100644 index 7bce21a..eb9c7db --- a/Module3_Python3/ICE3_ClientServer.html +++ b/Module3_Python3/ICE3_ClientServer.html @@ -1,783 +1,783 @@ - - - - - - - - - - - - ICE3 - Client and Server — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

ICE3 - Client and Server#

-
-

A note on this document#

-

This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

-
-
-

Purpose#

-

This Jupyter Notebook will allow you to practice some of the techniques you have learned over the last few modules. You will develop an advanced chat client and server (similar to ICE1) enabling a client to send a message and a server to respond accordingly.

-
-
-

Initialize ROS:#

-

The first step when utilizing ROS is to initialize roscore. There are two methods to accomplish this: first, by explicitly running roscore and second, by running a launch file (which will initialize roscore if it is not already running). During the first portion of this course we will explicitly run roscore and then take advantage of launch files later in the course.

-

Copy the following code and run it in a new terminal (use the shortcut ctrl+alt+t to open a new terminal window or select an open terminal and hit ctrl+shift+t to open a new terminal tab):

-

roscore

-
-
-

Implementing the chat client#

-
-

📝️ Note: This Jupyter Notebook will require you to enter Python3 code within code sections. You can type any Python3 code and expand the block if necessary. After typing the code, execute the code block before moving forward.

-
-
-

Import modules#

-
-

⚠️ Important: Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a * character. Do not move to the next step until the * is gone.

-
-
# import required modules for ROS and the String message from std_msgs
-
-
-
-
-
-

Client Class#

-
    -
  1. Create a Client class with a dictionary used to map numbers to messages.

  2. -
  3. Initialize the class with the following:

    -
      -
    1. an instance variable to store the String message

    2. -
    3. a publisher that publishes String messages on the client topic

    4. -
    5. a subscriber to the server topic which receives String messages and calls a callback when messages are sent.

    6. -
    7. a timer that runs every second and calls a class method

    8. -
    9. nicely handle shutdown

    10. -
    -
  4. -
  5. Create the callback input class method that is ran every second and has the user pick a message to send.

  6. -
  7. Create the callback received class method that is called every time a message is received from the server.

  8. -
  9. Handle shutdown.

  10. -
-
class Client:
-    MESSAGE = {1: "Hello!", 2: "How are you?", 3: "Where are you from?",
-               4: "What are you doing today?"}
-    
-    def __init__(self):
-        # 2.A
-
-        # 2.B
-        
-        # 2.C
-        
-        # 2.D
-        
-        # 2.E nicely handle shutdown
-        self.ctrl_c = False
-        rospy.on_shutdown(self.shutdownhook)
-
-    def callback_input(self, event):
-        valid = False
-        while not valid and not self.ctrl_c:
-            # get input from user (you must inform them their options)
-            
-            try:
-                # convert to int, if not number throw ValueError
-                val = int(chat_str)
-                # check if valid number, if valid then access
-                # that entry in the dictionary, publish the message
-                # and set valid to True; if not valid, print error
-                # message to user (make this error message useful)
-                
-                
-                
-                
-                
-            except ValueError:
-                # print error message to user (make 
-                # this error message useful)
-                
-                
-    def callback_received(self, msg):
-        # print message sent to the server
-        
-        # print the response from the server
-    
-    # handle shutdown
-    def shutdownhook(self):
-        print("Shutting down the client")
-        self.ctrl_c = True
-
-
-
-
-

Main#

-

The main function calls initializes our node, creates an instance of the client class, then runs forever.

-
def main():
-    # initialize node
-    
-    # create an instance of the client class
-    
-    # run forever
-    rospy.spin()
-
-
-
-
-

Run the program#

-
main()
-
-
-
-
-
-

Implementing the chat server#

-
# import required modules for ROS and the String message from std_msgs
-
-
-
-
-
-

Server Class#

-
    -
  1. Create a server class with a dictionary used to map messages to responses (one for each message from the client).

  2. -
  3. Initialize the class with the following:

    -
      -
    1. an instance variable to store the String message

    2. -
    3. a subscriber to the client topic which receives String messages and calls a callback called received.

    4. -
    5. a publisher to the server topic which sends String messages

    6. -
    7. nicely handle shutdown

    8. -
    -
  4. -
  5. Create the callback received class method that is called every time a message is received from the client.

  6. -
  7. Handle shutdown.

  8. -
-
class Server:
-    # class dictionary storing server responses
-    MESSAGE = 
-    
-    def __init__(self):
-        # 2.A
-
-        # 2.B
-        
-        # 2.C
-        
-        # 2.D nicely handle shutdown
-        self.ctrl_c = False
-        rospy.on_shutdown(self.shutdownhook)
-
-    def callback_received(self, msg):
-        # print the message from the client
-        
-        # print the response that will be sent to the client
-        
-        # publish the response
-    
-    # handle shutdown
-    def shutdownhook(self):
-        print("Shutting down the server")
-        self.ctrl_c = True
-
-
-
-
-

Main#

-

The main function calls initializes our node, creates an instance of the server class, then runs forever.

-
def main():
-    # initialize node
-    
-    # create an instance of the server class
-    
-    # run forever
-    rospy.spin()
-
-
-
-
-

Run the program#

-
main()
-
-
-

At this point, the server is waiting for the client to send a message. Browse back to your client and type a message! You should see that message show up above.

-
-
-
-

ROS commands#

-

Open the ROS notebook and follow the instructions.

-
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + ICE3 - Client and Server — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

ICE3 - Client and Server#

+
+

A note on this document#

+

This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

+
+
+

Purpose#

+

This Jupyter Notebook will allow you to practice some of the techniques you have learned over the last few modules. You will develop an advanced chat client and server (similar to ICE1) enabling a client to send a message and a server to respond accordingly.

+
+
+

Initialize ROS:#

+

The first step when utilizing ROS is to initialize roscore. There are two methods to accomplish this: first, by explicitly running roscore and second, by running a launch file (which will initialize roscore if it is not already running). During the first portion of this course we will explicitly run roscore and then take advantage of launch files later in the course.

+

Copy the following code and run it in a new terminal (use the shortcut ctrl+alt+t to open a new terminal window or select an open terminal and hit ctrl+shift+t to open a new terminal tab):

+

roscore

+
+
+

Implementing the chat client#

+
+

📝️ Note: This Jupyter Notebook will require you to enter Python3 code within code sections. You can type any Python3 code and expand the block if necessary. After typing the code, execute the code block before moving forward.

+
+
+

Import modules#

+
+

⚠️ Important: Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a * character. Do not move to the next step until the * is gone.

+
+
# import required modules for ROS and the String message from std_msgs
+
+
+
+
+
+

Client Class#

+
    +
  1. Create a Client class with a dictionary used to map numbers to messages.

  2. +
  3. Initialize the class with the following:

    +
      +
    1. an instance variable to store the String message

    2. +
    3. a publisher that publishes String messages on the client topic

    4. +
    5. a subscriber to the server topic which receives String messages and calls a callback when messages are sent.

    6. +
    7. a timer that runs every second and calls a class method

    8. +
    9. nicely handle shutdown

    10. +
    +
  4. +
  5. Create the callback input class method that is ran every second and has the user pick a message to send.

  6. +
  7. Create the callback received class method that is called every time a message is received from the server.

  8. +
  9. Handle shutdown.

  10. +
+
class Client:
+    MESSAGE = {1: "Hello!", 2: "How are you?", 3: "Where are you from?",
+               4: "What are you doing today?"}
+    
+    def __init__(self):
+        # 2.A
+
+        # 2.B
+        
+        # 2.C
+        
+        # 2.D
+        
+        # 2.E nicely handle shutdown
+        self.ctrl_c = False
+        rospy.on_shutdown(self.shutdownhook)
+
+    def callback_input(self, event):
+        valid = False
+        while not valid and not self.ctrl_c:
+            # get input from user (you must inform them their options)
+            
+            try:
+                # convert to int, if not number throw ValueError
+                val = int(chat_str)
+                # check if valid number, if valid then access
+                # that entry in the dictionary, publish the message
+                # and set valid to True; if not valid, print error
+                # message to user (make this error message useful)
+                
+                
+                
+                
+                
+            except ValueError:
+                # print error message to user (make 
+                # this error message useful)
+                
+                
+    def callback_received(self, msg):
+        # print message sent to the server
+        
+        # print the response from the server
+    
+    # handle shutdown
+    def shutdownhook(self):
+        print("Shutting down the client")
+        self.ctrl_c = True
+
+
+
+
+

Main#

+

The main function calls initializes our node, creates an instance of the client class, then runs forever.

+
def main():
+    # initialize node
+    
+    # create an instance of the client class
+    
+    # run forever
+    rospy.spin()
+
+
+
+
+

Run the program#

+
main()
+
+
+
+
+
+

Implementing the chat server#

+
# import required modules for ROS and the String message from std_msgs
+
+
+
+
+
+

Server Class#

+
    +
  1. Create a server class with a dictionary used to map messages to responses (one for each message from the client).

  2. +
  3. Initialize the class with the following:

    +
      +
    1. an instance variable to store the String message

    2. +
    3. a subscriber to the client topic which receives String messages and calls a callback called received.

    4. +
    5. a publisher to the server topic which sends String messages

    6. +
    7. nicely handle shutdown

    8. +
    +
  4. +
  5. Create the callback received class method that is called every time a message is received from the client.

  6. +
  7. Handle shutdown.

  8. +
+
class Server:
+    # class dictionary storing server responses
+    MESSAGE = 
+    
+    def __init__(self):
+        # 2.A
+
+        # 2.B
+        
+        # 2.C
+        
+        # 2.D nicely handle shutdown
+        self.ctrl_c = False
+        rospy.on_shutdown(self.shutdownhook)
+
+    def callback_received(self, msg):
+        # print the message from the client
+        
+        # print the response that will be sent to the client
+        
+        # publish the response
+    
+    # handle shutdown
+    def shutdownhook(self):
+        print("Shutting down the server")
+        self.ctrl_c = True
+
+
+
+
+

Main#

+

The main function calls initializes our node, creates an instance of the server class, then runs forever.

+
def main():
+    # initialize node
+    
+    # create an instance of the server class
+    
+    # run forever
+    rospy.spin()
+
+
+
+
+

Run the program#

+
main()
+
+
+

At this point, the server is waiting for the client to send a message. Browse back to your client and type a message! You should see that message show up above.

+
+
+
+

ROS commands#

+

Open the ROS notebook and follow the instructions.

+
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module3_Python3/Python3.html b/Module3_Python3/Python3.html old mode 100755 new mode 100644 index 48e90b6..76bf651 --- a/Module3_Python3/Python3.html +++ b/Module3_Python3/Python3.html @@ -1,1000 +1,1000 @@ - - - - - - - - - - - - Python3 for Robotics — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Python3 for Robotics#

-
-

A note on this document#

-

This document is known as a Jupyter notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

-
-
-

Purpose#

-

This assignment will help refresh Python3 fundamentals through application. You will implement a rock paper scissors player and computer that will compete over ROS.

-
-
-

Import modules at top of Python code#

-

With Python code, the convention is to import all required modules at the top of the python script. Modules are packages of additional functionality built by others. Typically, a module is comprised of compiled code that serves a specific function.

-

By importing all modules at the beginning of the script, we ensure that all modules have been loaded by the time we need them later in the script. Also, it ensures anyone reading our code can very quickly see all the modules (dependencies) needed to run the code.

-

Our imports are in the following code block.

-
-

⚠️ Important: Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a * character. Do not move to the next step until the * is gone.

-
-
# import required modules
-import random
-
-
-

Here, the module is called “random”, and by importing the module this way, we can use the module by referencing “random” in our code. There are a few other ways to do imports, but we will wait get to those in another script.

-

Another thing to note is we can make comments in python by starting a line with “#”. Comments are lines in your code that do not perform any function and are intended only to inform others (and your future self) about the purpose of your code. It is a good idea to document the purpose of your code at the beginning of the code with comments and to leave short and concise comments throughout your code. Many people get lazy and fail to comment their code, but don’t fall prey to that trap!

-
-
-

Variables#

-

In programming languages, variables are containers that store simple information. In Python, variables can store strings or numbers. A string is a block of text, such as “my TurtleBot is faster than yours”. A number can be an integer or a decimal number.

-
# create a variable to hold a string asking the user if they want to play rock, paper, scissors
-welcome_text = 'Would you like to play rock, paper, scissors?'
-
-# create a variable holding a number representing the number of possible choices in rock, paper, scissors
-num_choices = 3
-
-# verify the variables are holding the correct information by using the "print" function to display the content of both variables
-print('The variable welcome_text contains: %s' % welcome_text)
-print('The variable num_choices contains: %02d' % num_choices)
-
-
-

A few things are going on here while creating variables. First, Python lets us define variables using the convention variable_name = value. As long as the variable name is unique (it is not the name of a function, module, or other variable), we can define our variable this way. For strings, single quotes or double quotes must be used around the text that needs to be in the variable; it doesn’t matter whether you choose single or double quotes, just stay consistent.

-

Second, we show the print function. The print function takes a string as its input and prints the string to the output, which is right below the block of text. Given format % values, format includes a string and % conversion items which are replaced with elements of values (such as a 2 digit value %02d). The values must be a tuple with the exact number of items specified in the format string.

-
-
-

Lists#

-

In Python, we can store information in more than just variables. Python has a data type called “lists”, and they store just that - lists. An example of a Python list is below.

-
# create a list containing all of the possible choices in rock, paper, scissors
-choices = ['Rock','Paper','Scissors']
-
-# print the contents of "choices" to see what it contains
-print(choices)
-
-
-

We chose to use strings for each item in the list above, but we could have just as easily chosen numbers or other objects instead. Lists always begin with an open hard bracket, [, and end with the closed hard bracket, ]. Additionally, list items are separated by commas. For this case, choices is the name of our list. Python has other data structures, such as dictionaries, tuples, and classes, but we will stick to variables and lists for now.

-

You’re probably thinking, “Lists look great and all, but how do I use them?” Great question! All of the items in the list are given a position, and in Python lingo, this is called an index. Each item (string in this case) in the list has an index, and the indices start at 0 on the furthest left and increase as you move to the right through the list. You can use an item’s index to retrieve it from the list. Using the name of the list, choices in this case, followed by hard brackets [] with the index of the item in the middle will retrieve the item.

-
# retrieve each item from the list with its index and print it to the output
-print('The first item is: %s' % choices[0])
-print('The second item is: %s' % choices[1])
-print('The third item is: %s' % choices[2])
-print('All items of the list include: %s' % choices)
-
-# find the number of items in the list (length of the list)
-len(choices)
-
-
-
-
-

Functions#

-

In Python, we can create functions to perform a specific task. Functions help us organize our code and simplify our code by allowing us to call a function multiple times rather than writing the same code multiple times. Python functions start with the keyword def followed by the function name, an open parenthesis, any inputs to pass to the function, a close parenthesis, and a colon. When we start writing what the function will do, we need to start on a new line and “tab-in” so that Python knows we are writing within the scope of the function. The function finishes when it gets to the last line of code or it reaches a return keyword. Let’s write a function that represents the computer randomly selecting either ‘Rock’, ‘Paper’, or ‘Scissors’.

-
# create a function that represents the computer selecting either 'Rock',
-# 'Paper', or 'Scissors' at random
-# inputs: choices, which is a list representing any item the 
-#         computer can choose
-# outputs: rand_choice, an item chosen at random from the input, 
-#          choices
-def get_comp_choice(choices_list):
-    # use random module imported earlier to select a random index from the list
-    rand_index = random.randint(0,len(choices_list)-1)
-    # use the index to select an item from the list
-    rand_choice = choices_list[rand_index]
-    # end the function by returning the computer's choice
-    return rand_choice
-
-
-
# call the function a few times to test that it works properly
-# we are using the list 'choices' from earlier 
-choice1 = get_comp_choice(choices)
-print('Test Choice 1: ',choice1)
-choice2 = get_comp_choice(choices)
-print('Test Choice 2: ',choice2)
-choice3 = get_comp_choice(choices)
-print('Test Choice 3: ',choice3)
-
-
-

In the function get_comp_choice, we use the randint function from the random module we imported earlier. The randint function accepts as its input a range of integers from which it will select an integer at random. Then, it returns this integer and stores it in the variable rand_index. The first input to this function is the smallest possible integer to select, and the second input is the largest possible integer to select. The random integer is used to retrieve an item from the input list choices_list. Finally, we use the return keyword to allow the user to store the choice in a variable when the function is called.

-

The second code block calls the function 3 times to test that it works as we expect. Press Shift + Enter in the code block multiple times to see how the “Test Choices” change. As you can see above, we call a function by using its name followed by an open parenthesis, any inputs, and a close parenthesis. You can pass a different list to the function to further test that it works as you expect!

-
-
-

User input#

-

In order to play rock, paper, scissors, we need to get input from the user. The python input function is perfect for this.

-
# collect an input string from the user 
-choice = input("Please choose 'Rock', 'Paper', or 'Scissors' by typing either one of the three:")
-
-
-
# print the contents of the variable choice 
-print(choice)
-
-# print the type of data contained in the variable choice
-print(type(choice))
-
-
-

The input function takes a string of instructions as input and requests that the user input a string. After the user inputs a string, the function assigns that input string to a variable. In this case, the variable choice will contain whatever string the user types in the box (numbers typed in the input box will be interpreted as strings).

-

In the string of instructions passed to the input function, the string is surrounded by double quotes, and the individual items are surrounded by single quotes. When placing quotation marks inside of a string, the quotation marks inside the string must be single if the outside quotes are double and vice versa. Lastly, the string includes \n, the \n tells python to create a newline in the output, and this ensures that the box for input is on a separate line than the instructions.

-

We chose to print out the contents of choice to the output to show what is actually stored in the variable choice. Also, we use the type function inside of the print function to print the data type being stored in choice, and the output of <class 'str'> confirms that strings are always stored in variables created by the input function. Try putting different words and numbers in the input box to verify this yourself!

-
-
-

Validating user input with if statements#

-

Now that we have the user input, let’s verify that the user actually did input either Rock, Paper, or Scissors (humans make mistakes after all). We can do this with if statements in Python. If the user inputted a valid string, we can continue with the game. If not, the user has to start over and put in a new string. The key words if, elif, and else followed by a logical expression and a colon denote valid if, else if, and else statements, respectively. Below is an example of an if statement to check if the input is valid.

-
# use if statement to check if user's input is valid
-# start by checking if choice stores the string Rock
-if choice == 'Rock':
-    print('Good input')
-    valid_input = True
-#check if choice stores the string Paper
-elif choice == 'Paper':
-    print('Good input')
-    valid_input = True
-#check if choice stores the string Scissors
-elif choice == 'Scissors':
-    print('Good input')
-    valid_input = True
-# any other strings in choice are invalid
-else:
-    print('Bad input. Try again.')
-    valid_input = False
-
-
-

Logical expressions in Python compare one value or string to another. Python uses == to test equality and != to test if something is not equal to something else. Python can check if a value is greater or less than another number as well. Googling ‘Logical operators in Python’ will give a complete list.

-

Variables can also store boolean values, meaning they can store True or False. These are shortcuts for saying something is on or off, and in this case, we are using True if the input is valid and False if the input is not valid. We will use them later to make our code more concise.

-

Can you think of a way to rewrite the if statement above without the elif statements? Hint: Python uses or and and to combine multiple logical expressions into one. Hint for more advanced way: you can use the list choices from above.

-

One final note about our input validation: if the user inputs rock, what happens? Can you think of a way to accept this as a valid input without adding another elif statement?

-
-
-

Classes#

-

We will use ROS to communicate and classes enable us to bundle data and functionality together while taking advantage of ROS tools. A class creates new types of objects, allowing instances of that class to be made. Each class can have attributes and methods that, combined, maintain and modify a class’s state. For example, we could create a Robot class that has attributes such as number of wheels, speed, and physics. There might be methods that provide a controller information about the robot’s state, such as get_speed or set_speed. If we were using multiple robots we could have multiple instances of the class and each instance’s attributes and methods would only control that particular robot. We will use classes throughout this course.

-

A class name should use the CapWords convention. When we are operating within the scope of a class, we have to make sure to use the correct namespaces. Within a class you will see the keyword self quite a bit. This refers to an instance variable or method and is scoped within the class. For example, if a class has an instance variable num_wheels, when within that class, we must access it using the explicit scope: self.num_wheels.

-

Let’s discuss the components of the player class

-
-

Create the player class and init method#

-
class Player:
-    # class constant to store player choices
-    CHOICES = ['Rock', 'Paper', 'Scissors']
-    
-    # initialize class
-    def __init__(self, num_rounds = 3):
-        # instance variables
-        self.num_rounds = num_rounds # total numer of games to be played
-        self.round = 0 # number of rounds played so far
-        self.players_choice = String()
-        self.computers_choice = String()
-        self.round_complete = True
-        self.user_wins = 0
-        self.computer_wins = 0
-        
-        # publisher to send player's choice over the player topic
-        self.pub = rospy.Publisher('player', String, queue_size=1)
-        
-        # timer to request user's input, should be called once/second, but will stall
-        # on user input
-        rospy.Timer(rospy.Duration(1), self.callback_players_choice)
-        
-        # subscriber to receive the computer's choice over the computer topic
-        rospy.Subscriber('computer', String, self.callback_computers_choice)
-
-
-

The above creates and initializes a class called Player. CHOICES is a class level variable which is the same for every instance of the class. When an instance of the class is created the number of rounds to be played can be passed to the init function and becomes part of the instance of the class. If no input is provided, the default is set to 3. It is always good to set a default. You can see that comments are provided to describe a few of the instance variables that might need some clarification, but not for the self explanatory variables. That is one benefit of using descriptive variable names, it is easier for some to understand what is going on.

-

To access an instance variable within the class you must use the self keyword.

-

The last three lines create the ROS publisher, timer, and subscriber. The publisher and subscriber should be familiar to you by now, and the timer periodically calls a callback which we will use to take input from the user. It runs every duration, which is every one second in this example.

-
-
-

ROS callback method part of the Player class called by timer#

-
# method to request user input and publish to computer
-# inputs: event, ROS information on timing of interrupt.
-#         Can be ignored.
-# outputs: publishes player's choice
-def callback_players_choice(self, event):
-    # check to ensure we wait for the round to be complete to get user input
-    if self.round_complete and not self.is_game_complete():
-        self.players_choice = input('Please choose between %s by typing either one of the three.\n' % self.CHOICES)
-
-        # check to ensure it is a valid input
-        if self.players_choice in self.CHOICES:
-            self.round_complete = False
-            self.pub.publish(self.players_choice)
-        else:
-            print('Invalid input. Please choose between %s by typing either one of the three.\n' % self.CHOICES)
-
-
-

This is an example of a class method. In ROS we call it a callback, because some other process triggers the calling of the method instead of the main function. This particular method is called by the timer. The check is a more concise version of the one we saw earlier. If the check is valid the choice String is published over the player topic.

-
-
-

ROS callback method part of Player class called by topic#

-
# method to receive computer's choice and determine winner
-# inputs: choice, computer's choice sent over the computer
-#         topic
-# outputs: none
-def callback_computers_choice(self, choice):
-    self.computers_choice = choice.data
-    self.round += 1
-    print('Game %d: %s vs. %s' % (self.round, self.players_choice, self.computers_choice))
-
-    # Determine winner and increase number of wins for player by 1
-    if self.players_choice == self.computers_choice: 
-        print('Draw')
-    elif self.players_choice == 'Rock' and self.computers_choice == 'Scissors':
-        print('You win!')
-        self.user_wins += 1
-    elif self.players_choice == 'Paper' and self.computers_choice == 'Rock':
-        print('You win!')
-        self.user_wins += 1
-    elif self.players_choice == 'Scissors' and self.computers_choice == 'Paper':
-        print('You win!')
-        self.user_wins += 1
-    else:
-        print('You lose.')
-        self.computer_wins += 1
-
-    self.round_complete = True
-
-
-

This class method is another callback that is called whenever a String message is sent over the computer topic. When called, the message is passed to the module. In this example we named the variable choice and store it in the computers_choice instance variable.

-
-
-

Class methods part of Player class#

-
# returns the number of wins by the user and computer
-def get_results(self):
-    return self.user_wins, self.computer_wins
-
-# checks if the game is complete
-def is_game_complete(self):
-    if self.num_rounds == self.round and self.round_complete:
-        return True
-    else:
-        return False
-
-
-

These are two class methods that are used either within the class or in main. Class variables are technically accessible in main, but the first method above is a better convention for accessing instance variables.

-
-
-
-

Let’s start the game!#

-

Alright, now that we remember the basics of a Python, let’s build our game. We will start with the user player.

-
-

Initialize ROS:#

-

The first step when utilizing ROS is to initialize roscore. There are two methods to accomplish this: first, by explicitly running roscore and second, by running a launch file (which will initialize roscore if it is not already running). During the first portion of this course we will explicitly run roscore and then take advantage of launch files later in the course.

-

Copy the following code and run it in a new terminal (use the shortcut ctrl+alt+t to open a new terminal window):

-

roscore

-
-
-

Import modules:#

-
import rospy
-from std_msgs.msg import String
-
-
-

Here, we are importing two modules: rospy, which allows us to run ROS code in Python, and the String message from the std_msgs ROS package.

-
-
-

Build the class:#

-

The below is a copy of what we saw above.

-
class User:
-    # class constant to store user choices
-    CHOICES = ['Rock', 'Paper', 'Scissors']
-    
-    # initialize class
-    def __init__(self, num_rounds = 3):
-        # instance variables
-        self.num_rounds = num_rounds # total numer of games to be played
-        self.round = 0 # number of rounds played so far
-        self.users_choice = String()
-        self.computers_choice = String()
-        self.round_complete = True
-        self.user_wins = 0
-        self.computer_wins = 0
-        
-        # publisher to send user's choice over the user topic
-        self.pub = rospy.Publisher('user_choice', String, queue_size=1)
-        
-        # timer to request user's input, should be called once per second, but will stall
-        # on user input
-        rospy.Timer(rospy.Duration(1), self.callback_users_choice)
-        
-        # subscriber to receive the computer's choice over the computer topic
-        rospy.Subscriber('computer_choice', String, self.callback_computers_choice)
-            
-    # method to request user input and publish to computer
-    # inputs: event, ROS information on timing of interrupt.
-    #         Can be ignored.
-    # outputs: publishes user's choice
-    def callback_users_choice(self, event):
-        # check to ensure we wait for the round to be complete to get user input
-        if self.round_complete and not self.is_game_complete():
-            self.users_choice = input('Please choose between %s by typing either one of the three.\n' 
-                                      % self.CHOICES)
-
-            # check to ensure it is a valid input
-            if self.users_choice in self.CHOICES:
-                self.round_complete = False
-                self.pub.publish(self.users_choice)
-            else:
-                print('Invalid input. Please choose between %s by typing either one of the three.\n' 
-                      % self.CHOICES)
-                
-    # method to receive computer's choice and determine winner
-    # inputs: choice, computer's choice sent over the computer
-    #         topic
-    # outputs: none
-    def callback_computers_choice(self, choice):
-        self.computers_choice = choice.data
-        self.round += 1
-        print('Game %d: %s vs. %s' % (self.round, self.users_choice, self.computers_choice))
-        
-        # Determine winner and increase number of wins for user by 1
-        if self.users_choice == self.computers_choice: 
-            print('Draw')
-        elif self.users_choice == 'Rock' and self.computers_choice == 'Scissors':
-            print('You win!')
-            self.user_wins += 1
-        elif self.users_choice == 'Paper' and self.computers_choice == 'Rock':
-            print('You win!')
-            self.user_wins += 1
-        elif self.users_choice == 'Scissors' and self.computers_choice == 'Paper':
-            print('You win!')
-            self.user_wins += 1
-        else:
-            print('You lose.')
-            self.computer_wins += 1
-        
-        self.round_complete = True
-
-    # returns the number of wins by the user and computer
-    def get_results(self):
-        return self.user_wins, self.computer_wins
-    
-    # checks if the game is complete
-    def is_game_complete(self):
-        if self.num_rounds == self.round and self.round_complete:
-            return True
-        else:
-            return False
-
-
-
-
-

Create the main#

-
def main():
-    # initialize a node called player
-    rospy.init_node('user')
-
-    print("***Make sure your computer player is running.***")
-
-    num_rounds = int(input('How many rounds do you want to play? '))
-
-    # create an instance of the class that will play the specified number of games
-    u = User(num_rounds)
-
-    # run continuously until the game is complete
-    while not u.is_game_complete():
-        pass
-
-    # print results
-    user_wins, computer_wins = u.get_results()
-    print('Total number of user wins: %s' % (user_wins))
-    print('Total number of computer wins: %s' % (computer_wins))
-    print('Total number of draws: %s' % (num_rounds - (user_wins + computer_wins)))
-    print('User win percentage: %s' % (user_wins/num_rounds))
-
-
-

In an actual Python script we will replace

-
def main()
-
-
-

with

-
if __name__ == "__main__":
-
-
-

This allows our python files to be imported into other python files that might also have a main() function.

-
-
-

Create the computer player#

-

We must build and run the computer player before we can continue. Open the RPSComputer notebook and follow the instructions.

-
-
-

Run the program#

-
main()
-
-
-
-
-

ROS refresher#

-

Open the ROS notebook and follow the instructions.

-
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Python3 for Robotics — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Python3 for Robotics#

+
+

A note on this document#

+

This document is known as a Jupyter notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

+
+
+

Purpose#

+

This assignment will help refresh Python3 fundamentals through application. You will implement a rock paper scissors player and computer that will compete over ROS.

+
+
+

Import modules at top of Python code#

+

With Python code, the convention is to import all required modules at the top of the python script. Modules are packages of additional functionality built by others. Typically, a module is comprised of compiled code that serves a specific function.

+

By importing all modules at the beginning of the script, we ensure that all modules have been loaded by the time we need them later in the script. Also, it ensures anyone reading our code can very quickly see all the modules (dependencies) needed to run the code.

+

Our imports are in the following code block.

+
+

⚠️ Important: Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a * character. Do not move to the next step until the * is gone.

+
+
# import required modules
+import random
+
+
+

Here, the module is called “random”, and by importing the module this way, we can use the module by referencing “random” in our code. There are a few other ways to do imports, but we will wait get to those in another script.

+

Another thing to note is we can make comments in python by starting a line with “#”. Comments are lines in your code that do not perform any function and are intended only to inform others (and your future self) about the purpose of your code. It is a good idea to document the purpose of your code at the beginning of the code with comments and to leave short and concise comments throughout your code. Many people get lazy and fail to comment their code, but don’t fall prey to that trap!

+
+
+

Variables#

+

In programming languages, variables are containers that store simple information. In Python, variables can store strings or numbers. A string is a block of text, such as “my TurtleBot is faster than yours”. A number can be an integer or a decimal number.

+
# create a variable to hold a string asking the user if they want to play rock, paper, scissors
+welcome_text = 'Would you like to play rock, paper, scissors?'
+
+# create a variable holding a number representing the number of possible choices in rock, paper, scissors
+num_choices = 3
+
+# verify the variables are holding the correct information by using the "print" function to display the content of both variables
+print('The variable welcome_text contains: %s' % welcome_text)
+print('The variable num_choices contains: %02d' % num_choices)
+
+
+

A few things are going on here while creating variables. First, Python lets us define variables using the convention variable_name = value. As long as the variable name is unique (it is not the name of a function, module, or other variable), we can define our variable this way. For strings, single quotes or double quotes must be used around the text that needs to be in the variable; it doesn’t matter whether you choose single or double quotes, just stay consistent.

+

Second, we show the print function. The print function takes a string as its input and prints the string to the output, which is right below the block of text. Given format % values, format includes a string and % conversion items which are replaced with elements of values (such as a 2 digit value %02d). The values must be a tuple with the exact number of items specified in the format string.

+
+
+

Lists#

+

In Python, we can store information in more than just variables. Python has a data type called “lists”, and they store just that - lists. An example of a Python list is below.

+
# create a list containing all of the possible choices in rock, paper, scissors
+choices = ['Rock','Paper','Scissors']
+
+# print the contents of "choices" to see what it contains
+print(choices)
+
+
+

We chose to use strings for each item in the list above, but we could have just as easily chosen numbers or other objects instead. Lists always begin with an open hard bracket, [, and end with the closed hard bracket, ]. Additionally, list items are separated by commas. For this case, choices is the name of our list. Python has other data structures, such as dictionaries, tuples, and classes, but we will stick to variables and lists for now.

+

You’re probably thinking, “Lists look great and all, but how do I use them?” Great question! All of the items in the list are given a position, and in Python lingo, this is called an index. Each item (string in this case) in the list has an index, and the indices start at 0 on the furthest left and increase as you move to the right through the list. You can use an item’s index to retrieve it from the list. Using the name of the list, choices in this case, followed by hard brackets [] with the index of the item in the middle will retrieve the item.

+
# retrieve each item from the list with its index and print it to the output
+print('The first item is: %s' % choices[0])
+print('The second item is: %s' % choices[1])
+print('The third item is: %s' % choices[2])
+print('All items of the list include: %s' % choices)
+
+# find the number of items in the list (length of the list)
+len(choices)
+
+
+
+
+

Functions#

+

In Python, we can create functions to perform a specific task. Functions help us organize our code and simplify our code by allowing us to call a function multiple times rather than writing the same code multiple times. Python functions start with the keyword def followed by the function name, an open parenthesis, any inputs to pass to the function, a close parenthesis, and a colon. When we start writing what the function will do, we need to start on a new line and “tab-in” so that Python knows we are writing within the scope of the function. The function finishes when it gets to the last line of code or it reaches a return keyword. Let’s write a function that represents the computer randomly selecting either ‘Rock’, ‘Paper’, or ‘Scissors’.

+
# create a function that represents the computer selecting either 'Rock',
+# 'Paper', or 'Scissors' at random
+# inputs: choices, which is a list representing any item the 
+#         computer can choose
+# outputs: rand_choice, an item chosen at random from the input, 
+#          choices
+def get_comp_choice(choices_list):
+    # use random module imported earlier to select a random index from the list
+    rand_index = random.randint(0,len(choices_list)-1)
+    # use the index to select an item from the list
+    rand_choice = choices_list[rand_index]
+    # end the function by returning the computer's choice
+    return rand_choice
+
+
+
# call the function a few times to test that it works properly
+# we are using the list 'choices' from earlier 
+choice1 = get_comp_choice(choices)
+print('Test Choice 1: ',choice1)
+choice2 = get_comp_choice(choices)
+print('Test Choice 2: ',choice2)
+choice3 = get_comp_choice(choices)
+print('Test Choice 3: ',choice3)
+
+
+

In the function get_comp_choice, we use the randint function from the random module we imported earlier. The randint function accepts as its input a range of integers from which it will select an integer at random. Then, it returns this integer and stores it in the variable rand_index. The first input to this function is the smallest possible integer to select, and the second input is the largest possible integer to select. The random integer is used to retrieve an item from the input list choices_list. Finally, we use the return keyword to allow the user to store the choice in a variable when the function is called.

+

The second code block calls the function 3 times to test that it works as we expect. Press Shift + Enter in the code block multiple times to see how the “Test Choices” change. As you can see above, we call a function by using its name followed by an open parenthesis, any inputs, and a close parenthesis. You can pass a different list to the function to further test that it works as you expect!

+
+
+

User input#

+

In order to play rock, paper, scissors, we need to get input from the user. The python input function is perfect for this.

+
# collect an input string from the user 
+choice = input("Please choose 'Rock', 'Paper', or 'Scissors' by typing either one of the three:")
+
+
+
# print the contents of the variable choice 
+print(choice)
+
+# print the type of data contained in the variable choice
+print(type(choice))
+
+
+

The input function takes a string of instructions as input and requests that the user input a string. After the user inputs a string, the function assigns that input string to a variable. In this case, the variable choice will contain whatever string the user types in the box (numbers typed in the input box will be interpreted as strings).

+

In the string of instructions passed to the input function, the string is surrounded by double quotes, and the individual items are surrounded by single quotes. When placing quotation marks inside of a string, the quotation marks inside the string must be single if the outside quotes are double and vice versa. Lastly, the string includes \n, the \n tells python to create a newline in the output, and this ensures that the box for input is on a separate line than the instructions.

+

We chose to print out the contents of choice to the output to show what is actually stored in the variable choice. Also, we use the type function inside of the print function to print the data type being stored in choice, and the output of <class 'str'> confirms that strings are always stored in variables created by the input function. Try putting different words and numbers in the input box to verify this yourself!

+
+
+

Validating user input with if statements#

+

Now that we have the user input, let’s verify that the user actually did input either Rock, Paper, or Scissors (humans make mistakes after all). We can do this with if statements in Python. If the user inputted a valid string, we can continue with the game. If not, the user has to start over and put in a new string. The key words if, elif, and else followed by a logical expression and a colon denote valid if, else if, and else statements, respectively. Below is an example of an if statement to check if the input is valid.

+
# use if statement to check if user's input is valid
+# start by checking if choice stores the string Rock
+if choice == 'Rock':
+    print('Good input')
+    valid_input = True
+#check if choice stores the string Paper
+elif choice == 'Paper':
+    print('Good input')
+    valid_input = True
+#check if choice stores the string Scissors
+elif choice == 'Scissors':
+    print('Good input')
+    valid_input = True
+# any other strings in choice are invalid
+else:
+    print('Bad input. Try again.')
+    valid_input = False
+
+
+

Logical expressions in Python compare one value or string to another. Python uses == to test equality and != to test if something is not equal to something else. Python can check if a value is greater or less than another number as well. Googling ‘Logical operators in Python’ will give a complete list.

+

Variables can also store boolean values, meaning they can store True or False. These are shortcuts for saying something is on or off, and in this case, we are using True if the input is valid and False if the input is not valid. We will use them later to make our code more concise.

+

Can you think of a way to rewrite the if statement above without the elif statements? Hint: Python uses or and and to combine multiple logical expressions into one. Hint for more advanced way: you can use the list choices from above.

+

One final note about our input validation: if the user inputs rock, what happens? Can you think of a way to accept this as a valid input without adding another elif statement?

+
+
+

Classes#

+

We will use ROS to communicate and classes enable us to bundle data and functionality together while taking advantage of ROS tools. A class creates new types of objects, allowing instances of that class to be made. Each class can have attributes and methods that, combined, maintain and modify a class’s state. For example, we could create a Robot class that has attributes such as number of wheels, speed, and physics. There might be methods that provide a controller information about the robot’s state, such as get_speed or set_speed. If we were using multiple robots we could have multiple instances of the class and each instance’s attributes and methods would only control that particular robot. We will use classes throughout this course.

+

A class name should use the CapWords convention. When we are operating within the scope of a class, we have to make sure to use the correct namespaces. Within a class you will see the keyword self quite a bit. This refers to an instance variable or method and is scoped within the class. For example, if a class has an instance variable num_wheels, when within that class, we must access it using the explicit scope: self.num_wheels.

+

Let’s discuss the components of the player class

+
+

Create the player class and init method#

+
class Player:
+    # class constant to store player choices
+    CHOICES = ['Rock', 'Paper', 'Scissors']
+    
+    # initialize class
+    def __init__(self, num_rounds = 3):
+        # instance variables
+        self.num_rounds = num_rounds # total numer of games to be played
+        self.round = 0 # number of rounds played so far
+        self.players_choice = String()
+        self.computers_choice = String()
+        self.round_complete = True
+        self.user_wins = 0
+        self.computer_wins = 0
+        
+        # publisher to send player's choice over the player topic
+        self.pub = rospy.Publisher('player', String, queue_size=1)
+        
+        # timer to request user's input, should be called once/second, but will stall
+        # on user input
+        rospy.Timer(rospy.Duration(1), self.callback_players_choice)
+        
+        # subscriber to receive the computer's choice over the computer topic
+        rospy.Subscriber('computer', String, self.callback_computers_choice)
+
+
+

The above creates and initializes a class called Player. CHOICES is a class level variable which is the same for every instance of the class. When an instance of the class is created the number of rounds to be played can be passed to the init function and becomes part of the instance of the class. If no input is provided, the default is set to 3. It is always good to set a default. You can see that comments are provided to describe a few of the instance variables that might need some clarification, but not for the self explanatory variables. That is one benefit of using descriptive variable names, it is easier for some to understand what is going on.

+

To access an instance variable within the class you must use the self keyword.

+

The last three lines create the ROS publisher, timer, and subscriber. The publisher and subscriber should be familiar to you by now, and the timer periodically calls a callback which we will use to take input from the user. It runs every duration, which is every one second in this example.

+
+
+

ROS callback method part of the Player class called by timer#

+
# method to request user input and publish to computer
+# inputs: event, ROS information on timing of interrupt.
+#         Can be ignored.
+# outputs: publishes player's choice
+def callback_players_choice(self, event):
+    # check to ensure we wait for the round to be complete to get user input
+    if self.round_complete and not self.is_game_complete():
+        self.players_choice = input('Please choose between %s by typing either one of the three.\n' % self.CHOICES)
+
+        # check to ensure it is a valid input
+        if self.players_choice in self.CHOICES:
+            self.round_complete = False
+            self.pub.publish(self.players_choice)
+        else:
+            print('Invalid input. Please choose between %s by typing either one of the three.\n' % self.CHOICES)
+
+
+

This is an example of a class method. In ROS we call it a callback, because some other process triggers the calling of the method instead of the main function. This particular method is called by the timer. The check is a more concise version of the one we saw earlier. If the check is valid the choice String is published over the player topic.

+
+
+

ROS callback method part of Player class called by topic#

+
# method to receive computer's choice and determine winner
+# inputs: choice, computer's choice sent over the computer
+#         topic
+# outputs: none
+def callback_computers_choice(self, choice):
+    self.computers_choice = choice.data
+    self.round += 1
+    print('Game %d: %s vs. %s' % (self.round, self.players_choice, self.computers_choice))
+
+    # Determine winner and increase number of wins for player by 1
+    if self.players_choice == self.computers_choice: 
+        print('Draw')
+    elif self.players_choice == 'Rock' and self.computers_choice == 'Scissors':
+        print('You win!')
+        self.user_wins += 1
+    elif self.players_choice == 'Paper' and self.computers_choice == 'Rock':
+        print('You win!')
+        self.user_wins += 1
+    elif self.players_choice == 'Scissors' and self.computers_choice == 'Paper':
+        print('You win!')
+        self.user_wins += 1
+    else:
+        print('You lose.')
+        self.computer_wins += 1
+
+    self.round_complete = True
+
+
+

This class method is another callback that is called whenever a String message is sent over the computer topic. When called, the message is passed to the module. In this example we named the variable choice and store it in the computers_choice instance variable.

+
+
+

Class methods part of Player class#

+
# returns the number of wins by the user and computer
+def get_results(self):
+    return self.user_wins, self.computer_wins
+
+# checks if the game is complete
+def is_game_complete(self):
+    if self.num_rounds == self.round and self.round_complete:
+        return True
+    else:
+        return False
+
+
+

These are two class methods that are used either within the class or in main. Class variables are technically accessible in main, but the first method above is a better convention for accessing instance variables.

+
+
+
+

Let’s start the game!#

+

Alright, now that we remember the basics of a Python, let’s build our game. We will start with the user player.

+
+

Initialize ROS:#

+

The first step when utilizing ROS is to initialize roscore. There are two methods to accomplish this: first, by explicitly running roscore and second, by running a launch file (which will initialize roscore if it is not already running). During the first portion of this course we will explicitly run roscore and then take advantage of launch files later in the course.

+

Copy the following code and run it in a new terminal (use the shortcut ctrl+alt+t to open a new terminal window):

+

roscore

+
+
+

Import modules:#

+
import rospy
+from std_msgs.msg import String
+
+
+

Here, we are importing two modules: rospy, which allows us to run ROS code in Python, and the String message from the std_msgs ROS package.

+
+
+

Build the class:#

+

The below is a copy of what we saw above.

+
class User:
+    # class constant to store user choices
+    CHOICES = ['Rock', 'Paper', 'Scissors']
+    
+    # initialize class
+    def __init__(self, num_rounds = 3):
+        # instance variables
+        self.num_rounds = num_rounds # total numer of games to be played
+        self.round = 0 # number of rounds played so far
+        self.users_choice = String()
+        self.computers_choice = String()
+        self.round_complete = True
+        self.user_wins = 0
+        self.computer_wins = 0
+        
+        # publisher to send user's choice over the user topic
+        self.pub = rospy.Publisher('user_choice', String, queue_size=1)
+        
+        # timer to request user's input, should be called once per second, but will stall
+        # on user input
+        rospy.Timer(rospy.Duration(1), self.callback_users_choice)
+        
+        # subscriber to receive the computer's choice over the computer topic
+        rospy.Subscriber('computer_choice', String, self.callback_computers_choice)
+            
+    # method to request user input and publish to computer
+    # inputs: event, ROS information on timing of interrupt.
+    #         Can be ignored.
+    # outputs: publishes user's choice
+    def callback_users_choice(self, event):
+        # check to ensure we wait for the round to be complete to get user input
+        if self.round_complete and not self.is_game_complete():
+            self.users_choice = input('Please choose between %s by typing either one of the three.\n' 
+                                      % self.CHOICES)
+
+            # check to ensure it is a valid input
+            if self.users_choice in self.CHOICES:
+                self.round_complete = False
+                self.pub.publish(self.users_choice)
+            else:
+                print('Invalid input. Please choose between %s by typing either one of the three.\n' 
+                      % self.CHOICES)
+                
+    # method to receive computer's choice and determine winner
+    # inputs: choice, computer's choice sent over the computer
+    #         topic
+    # outputs: none
+    def callback_computers_choice(self, choice):
+        self.computers_choice = choice.data
+        self.round += 1
+        print('Game %d: %s vs. %s' % (self.round, self.users_choice, self.computers_choice))
+        
+        # Determine winner and increase number of wins for user by 1
+        if self.users_choice == self.computers_choice: 
+            print('Draw')
+        elif self.users_choice == 'Rock' and self.computers_choice == 'Scissors':
+            print('You win!')
+            self.user_wins += 1
+        elif self.users_choice == 'Paper' and self.computers_choice == 'Rock':
+            print('You win!')
+            self.user_wins += 1
+        elif self.users_choice == 'Scissors' and self.computers_choice == 'Paper':
+            print('You win!')
+            self.user_wins += 1
+        else:
+            print('You lose.')
+            self.computer_wins += 1
+        
+        self.round_complete = True
+
+    # returns the number of wins by the user and computer
+    def get_results(self):
+        return self.user_wins, self.computer_wins
+    
+    # checks if the game is complete
+    def is_game_complete(self):
+        if self.num_rounds == self.round and self.round_complete:
+            return True
+        else:
+            return False
+
+
+
+
+

Create the main#

+
def main():
+    # initialize a node called player
+    rospy.init_node('user')
+
+    print("***Make sure your computer player is running.***")
+
+    num_rounds = int(input('How many rounds do you want to play? '))
+
+    # create an instance of the class that will play the specified number of games
+    u = User(num_rounds)
+
+    # run continuously until the game is complete
+    while not u.is_game_complete():
+        pass
+
+    # print results
+    user_wins, computer_wins = u.get_results()
+    print('Total number of user wins: %s' % (user_wins))
+    print('Total number of computer wins: %s' % (computer_wins))
+    print('Total number of draws: %s' % (num_rounds - (user_wins + computer_wins)))
+    print('User win percentage: %s' % (user_wins/num_rounds))
+
+
+

In an actual Python script we will replace

+
def main()
+
+
+

with

+
if __name__ == "__main__":
+
+
+

This allows our python files to be imported into other python files that might also have a main() function.

+
+
+

Create the computer player#

+

We must build and run the computer player before we can continue. Open the RPSComputer notebook and follow the instructions.

+
+
+

Run the program#

+
main()
+
+
+
+
+

ROS refresher#

+

Open the ROS notebook and follow the instructions.

+
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module3_Python3/ROS.html b/Module3_Python3/ROS.html old mode 100755 new mode 100644 index 2b8f43e..de032a8 --- a/Module3_Python3/ROS.html +++ b/Module3_Python3/ROS.html @@ -1,635 +1,635 @@ - - - - - - - - - - - - ICE3: ROS — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

ICE3: ROS

- -
-
- -
-

Contents

-
- -
-
-
- - - - -
- -
-

ICE3: ROS#

-
-

ROS#

-

Below you will see some ROS commands. The “!” character in the front allows us to run bash commands from Jupyter and would NOT be used in the command line.

-

With the user and computer nodes running execute the below commands.

-

List the active topics:

-
rostopic list
-
-
-

Display running nodes and communication between them:

-
rqt_graph
-
-
-

Exit the rqt_graph and then list all running nodes:

-
rosnode list
-
-
-

Show information about a the /computer_choice topic such as what type of messages are sent over the topic and publishing and subscribing nodes:

-
rostopic info /computer_choice
-
-
-

Display information about the message that is sent over the /computer topic:

-
rostopic type /computer_choice | rosmsg show
-
-
-

Display messages sent over the /user_choice topic:

-
rostopic echo /user_choice
-
-
-

In the Module3_Python3 notebook, reset the Jupter kernel and clear output: At the top menu bar select “Kernel” and “Restart & Clear Output” (you do not need to restart the computer player). Make sure to rerun the code blocks to import modules, build the class, and create the main. When you select ‘Rock’, ‘Paper’, or ‘Scissors’ you should see the message echoed above.

-
-
-

Cleanup#

-

In each of the notebooks reset the Jupter kernel and clear output. Close each notebook. Shutdown the notebook server by typing ctrl+c within the terminal you ran Jupter-notebook in. Select ‘y’. Kill all processes in your terminals (ctrl+c) and ensure roscore is terminated before moving on to the ICE.

-

You should now have a better understanding of how to utilize ROS and Python. To learn more about the Python style guide and standards, visit PEP 8 – Style Guid for Python Code.

-
-
-

Deliverables#

-

Below you will run ROS commands. The “!” character in the front allows us to run bash commands from Jupyter and would NOT be used in the command line.

-

With the client and server nodes running execute the below commands.

-

List all running nodes:

-

-
-
-

List the active topics:

-
 
-
-
-

Display running nodes and communication between them:

-
 
-
-
-

Exit the rqt_graph.

-

Show information about a the /client topic such as what type of messages are sent over the topic and publishing and subscribing nodes.

-
 
-
-
-

Display information about the message that is sent over the /client topic.

-

-
-
-

Display messages sent over the /client topic:

-

-
-
-

In the ICE3_Client notebook when you send a message you should see the message echoed above.

-
-
-

Checkpoint#

-

Once complete, take the necessary screen shots of all the items above functioning. Upload those screenshots to your repo in a Module03 folder within the master folder of your repo.

-
-
-

Cleanup#

-

In each of the notebooks reset the Jupter kernel and clear output. Close each notebook. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-
-
- - - - -
- - - - - - -
- - - -
- - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + ICE3: ROS — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

ICE3: ROS

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

ICE3: ROS#

+
+

ROS#

+

Below you will see some ROS commands. The “!” character in the front allows us to run bash commands from Jupyter and would NOT be used in the command line.

+

With the user and computer nodes running execute the below commands.

+

List the active topics:

+
rostopic list
+
+
+

Display running nodes and communication between them:

+
rqt_graph
+
+
+

Exit the rqt_graph and then list all running nodes:

+
rosnode list
+
+
+

Show information about a the /computer_choice topic such as what type of messages are sent over the topic and publishing and subscribing nodes:

+
rostopic info /computer_choice
+
+
+

Display information about the message that is sent over the /computer topic:

+
rostopic type /computer_choice | rosmsg show
+
+
+

Display messages sent over the /user_choice topic:

+
rostopic echo /user_choice
+
+
+

In the Module3_Python3 notebook, reset the Jupter kernel and clear output: At the top menu bar select “Kernel” and “Restart & Clear Output” (you do not need to restart the computer player). Make sure to rerun the code blocks to import modules, build the class, and create the main. When you select ‘Rock’, ‘Paper’, or ‘Scissors’ you should see the message echoed above.

+
+
+

Cleanup#

+

In each of the notebooks reset the Jupter kernel and clear output. Close each notebook. Shutdown the notebook server by typing ctrl+c within the terminal you ran Jupter-notebook in. Select ‘y’. Kill all processes in your terminals (ctrl+c) and ensure roscore is terminated before moving on to the ICE.

+

You should now have a better understanding of how to utilize ROS and Python. To learn more about the Python style guide and standards, visit PEP 8 – Style Guid for Python Code.

+
+
+

Deliverables#

+

Below you will run ROS commands. The “!” character in the front allows us to run bash commands from Jupyter and would NOT be used in the command line.

+

With the client and server nodes running execute the below commands.

+

List all running nodes:

+

+
+
+

List the active topics:

+
 
+
+
+

Display running nodes and communication between them:

+
 
+
+
+

Exit the rqt_graph.

+

Show information about a the /client topic such as what type of messages are sent over the topic and publishing and subscribing nodes.

+
 
+
+
+

Display information about the message that is sent over the /client topic.

+

+
+
+

Display messages sent over the /client topic:

+

+
+
+

In the ICE3_Client notebook when you send a message you should see the message echoed above.

+
+
+

Checkpoint#

+

Once complete, take the necessary screen shots of all the items above functioning. Upload those screenshots to your repo in a Module03 folder within the master folder of your repo.

+
+
+

Cleanup#

+

In each of the notebooks reset the Jupter kernel and clear output. Close each notebook. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+
+
+ + + + +
+ + + + + + +
+ + + +
+ + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module3_Python3/RPSComputer.html b/Module3_Python3/RPSComputer.html old mode 100755 new mode 100644 index 1983b6c..95e0b73 --- a/Module3_Python3/RPSComputer.html +++ b/Module3_Python3/RPSComputer.html @@ -1,598 +1,598 @@ - - - - - - - - - - - - Module 3: Python3 for Robotics — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

Module 3: Python3 for Robotics

- -
-
- -
-

Contents

-
- -
-
-
- - - - -
- -
-

Module 3: Python3 for Robotics#

-
-

A note on this document#

-

This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

-
-
-

Computer Player#

-

We will now implement a computer player node that will send a random choice to the player node.

-

First import your required modules:

-
import rospy, random
-from std_msgs.msg import String
-
-
-

Here, we are importing two modules: rospy, which allows us to run ROS code in Python, and the String message from the std_msgs ROS package.

-
class Computer:
-    # class constant to store computer choices
-    CHOICES = ['Rock', 'Paper', 'Scissors']
-    
-    # initialize class
-    def __init__(self,):
-        # instance variables
-        self.computers_choice = String()
-        
-        # subscriber to receive the computer's choice over the computer topic
-        rospy.Subscriber('user_choice', String, self.callback_computers_choice)
-        
-        # publisher to send player's choice over the player topic
-        self.pub = rospy.Publisher('computer_choice', String, queue_size=1)
-    
-    def callback_computers_choice(self, data):
-        # use random module imported earlier to select a random index from the list
-        rand_index = random.randint(0,len(self.CHOICES)-1)
-        # use the index to select an item from the list
-        self.computers_choice = self.CHOICES[rand_index]
-        self.pub.publish(self.computers_choice)
-
-
-
rospy.init_node('computer')
-Computer()
-rospy.spin()
-
-
-

Now return to the Module3_Python3 User Notebook and fill in the requested inputs.

-
-
- - - - -
- - - - - - -
- - - -
- - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Module 3: Python3 for Robotics — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Module 3: Python3 for Robotics

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Module 3: Python3 for Robotics#

+
+

A note on this document#

+

This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press Shift + Enter to run the code. Earlier blocks of code need to be run for the later blocks of code to work.

+
+
+

Computer Player#

+

We will now implement a computer player node that will send a random choice to the player node.

+

First import your required modules:

+
import rospy, random
+from std_msgs.msg import String
+
+
+

Here, we are importing two modules: rospy, which allows us to run ROS code in Python, and the String message from the std_msgs ROS package.

+
class Computer:
+    # class constant to store computer choices
+    CHOICES = ['Rock', 'Paper', 'Scissors']
+    
+    # initialize class
+    def __init__(self,):
+        # instance variables
+        self.computers_choice = String()
+        
+        # subscriber to receive the computer's choice over the computer topic
+        rospy.Subscriber('user_choice', String, self.callback_computers_choice)
+        
+        # publisher to send player's choice over the player topic
+        self.pub = rospy.Publisher('computer_choice', String, queue_size=1)
+    
+    def callback_computers_choice(self, data):
+        # use random module imported earlier to select a random index from the list
+        rand_index = random.randint(0,len(self.CHOICES)-1)
+        # use the index to select an item from the list
+        self.computers_choice = self.CHOICES[rand_index]
+        self.pub.publish(self.computers_choice)
+
+
+
rospy.init_node('computer')
+Computer()
+rospy.spin()
+
+
+

Now return to the Module3_Python3 User Notebook and fill in the requested inputs.

+
+
+ + + + +
+ + + + + + +
+ + + +
+ + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module4_DrivingTheRobot/DrivingTheRobot.html b/Module4_DrivingTheRobot/DrivingTheRobot.html old mode 100755 new mode 100644 index ff395fb..967446a --- a/Module4_DrivingTheRobot/DrivingTheRobot.html +++ b/Module4_DrivingTheRobot/DrivingTheRobot.html @@ -1,657 +1,657 @@ - - - - - - - - - - - - Driving the Robot — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Driving the Robot#

-
-

Lesson Objectives:#

-
    -
  1. Gain additional familiarity with simulation environment

  2. -
  3. Gain familiarity with Turtlebot 3 robotics platform

  4. -
  5. Practice with ROS diagnostic tools

  6. -
-
-
-

Agenda:#

-
    -
  1. Use Linux terminals to launch and control Turtlebot 3 in simulation environment.

  2. -
  3. Use Linux terminals to launch and control actual Turtlebot 3.

  4. -
-
-
-

Gain Additional Familiarity with Simulation Environment.#

-

At this point we are nearly 20% complete with the course, and we have the foundational knowledge required -about the ecosystem we will be working in. In short we will be using the Linux operating system to host -ROS. We will use ROS to execute python code to control and interact with the various subsystems on our -robotics platform.

-

Before we move on to controlling our actual robot, we want to stress the importance and capabilities of our simulation environment. There will be times, where it may be inconvenient to work with the actual robot. Additionally, with all hardware systems, there can occasionally be inconsistent behaviors. For both of these reasons (and many others), having an effective simulator may be very useful. -There are two tools that we will find very useful for simulation (RViz and Gazebo).

-
    -
  • RViz - (short for “ROS visualization”) is a 3D visualization software tool for robots, sensors, and -algorithms. It enables you to see the robot’s perception of its world (real or simulated).

  • -
  • Gazebo - Gazebo is a 3D robot simulator. Its objective is to simulate a robot, giving you a close -substitute to how your robot would behave in a real-world physical environment. It can compute the -impact of forces (such as gravity).

  • -
-

The difference between the two can be summed up in the following excerpt from Morgan Quigley (one of -the original developers of ROS) in his book Programming Robots with ROS:

-
RViz shows you what the robot thinks is happening, while Gazebo shows you what is really happening.
-
-
-

For future research or development efforts, you may need to build your own simulation environment. However, -for this course, the developers of the Turtlebot3, Robotis, have already done this for us.

-
    -
  • In a free terminal window, bring up roscore

  • -
  • In a second free terminal window, we are going to use the roslaunch command to bring up the Gazebo -environment within a maze. Tip: There are numerous virtual environments, but this will work for -now

  • -
-
dfec@master:∼$ roslaunch turtlebot3_gazebo turtlebot3_world.launch
-
-
-
    -
  • In a third terminal window, we are now going to bring up the RViz environment.

  • -
-
dfec@master:∼$ roslaunch turtlebot3_gazebo turtlebot3_gazebo_rviz.launch
-
-
-
    -
  • In a fourth terminal window, we are going to bring up a valuable tool that will allow us to control the -robot via the keyboard. This tool is called teleop_twist.

  • -
-
dfec@master:∼$ rosrun teleop_twist_keyboard teleop_twist_keyboard.py
-
-
-
-

Tip

-

I strongly recommend that you commit the above sequence of commands to memory, or at a minimum -have them in a place that you can quickly recall them. There is nothing until Module 9 that absolutely -requires the real robot, as everything else can be simulated.

-
-
-
-

Gain Familiarity with Turtlebot3 Robotics Platform.#

-

The Module04 Jupyter Notebook will guide you through the process of connecting to and activating your -robot for the first time.

-
    -
  1. On the master, open the Jupyter Notebook server (if it is not already open):

  2. -
-
dfec@master:∼$ roscd ece387_curriculum/Module04_DrivingTheRobot
-dfec@master:∼$ jupyter−lab
-
-
-
    -
  1. Open ICE4: Driving the Robot and follow the instructions.

  2. -
-
-
-

Assignments.#

-
    -
  • Complete ICE4 if not accomplished during class.

  • -
  • Push screen captures into your repo/master/module04 on github

  • -
-
-
-

Next time.#

-
    -
  • Lesson 10: Module 5 - Custom Messages

  • -
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Driving the Robot — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Driving the Robot#

+
+

Lesson Objectives:#

+
    +
  1. Gain additional familiarity with simulation environment

  2. +
  3. Gain familiarity with Turtlebot 3 robotics platform

  4. +
  5. Practice with ROS diagnostic tools

  6. +
+
+
+

Agenda:#

+
    +
  1. Use Linux terminals to launch and control Turtlebot 3 in simulation environment.

  2. +
  3. Use Linux terminals to launch and control actual Turtlebot 3.

  4. +
+
+
+

Gain Additional Familiarity with Simulation Environment.#

+

At this point we are nearly 20% complete with the course, and we have the foundational knowledge required +about the ecosystem we will be working in. In short we will be using the Linux operating system to host +ROS. We will use ROS to execute python code to control and interact with the various subsystems on our +robotics platform.

+

Before we move on to controlling our actual robot, we want to stress the importance and capabilities of our simulation environment. There will be times, where it may be inconvenient to work with the actual robot. Additionally, with all hardware systems, there can occasionally be inconsistent behaviors. For both of these reasons (and many others), having an effective simulator may be very useful. +There are two tools that we will find very useful for simulation (RViz and Gazebo).

+
    +
  • RViz - (short for “ROS visualization”) is a 3D visualization software tool for robots, sensors, and +algorithms. It enables you to see the robot’s perception of its world (real or simulated).

  • +
  • Gazebo - Gazebo is a 3D robot simulator. Its objective is to simulate a robot, giving you a close +substitute to how your robot would behave in a real-world physical environment. It can compute the +impact of forces (such as gravity).

  • +
+

The difference between the two can be summed up in the following excerpt from Morgan Quigley (one of +the original developers of ROS) in his book Programming Robots with ROS:

+
RViz shows you what the robot thinks is happening, while Gazebo shows you what is really happening.
+
+
+

For future research or development efforts, you may need to build your own simulation environment. However, +for this course, the developers of the Turtlebot3, Robotis, have already done this for us.

+
    +
  • In a free terminal window, bring up roscore

  • +
  • In a second free terminal window, we are going to use the roslaunch command to bring up the Gazebo +environment within a maze. Tip: There are numerous virtual environments, but this will work for +now

  • +
+
dfec@master:∼$ roslaunch turtlebot3_gazebo turtlebot3_world.launch
+
+
+
    +
  • In a third terminal window, we are now going to bring up the RViz environment.

  • +
+
dfec@master:∼$ roslaunch turtlebot3_gazebo turtlebot3_gazebo_rviz.launch
+
+
+
    +
  • In a fourth terminal window, we are going to bring up a valuable tool that will allow us to control the +robot via the keyboard. This tool is called teleop_twist.

  • +
+
dfec@master:∼$ rosrun teleop_twist_keyboard teleop_twist_keyboard.py
+
+
+
+

Tip

+

I strongly recommend that you commit the above sequence of commands to memory, or at a minimum +have them in a place that you can quickly recall them. There is nothing until Module 9 that absolutely +requires the real robot, as everything else can be simulated.

+
+
+
+

Gain Familiarity with Turtlebot3 Robotics Platform.#

+

The Module04 Jupyter Notebook will guide you through the process of connecting to and activating your +robot for the first time.

+
    +
  1. On the master, open the Jupyter Notebook server (if it is not already open):

  2. +
+
dfec@master:∼$ roscd ece387_curriculum/Module04_DrivingTheRobot
+dfec@master:∼$ jupyter−lab
+
+
+
    +
  1. Open ICE4: Driving the Robot and follow the instructions.

  2. +
+
+
+

Assignments.#

+
    +
  • Complete ICE4 if not accomplished during class.

  • +
  • Push screen captures into your repo/master/module04 on github

  • +
+
+
+

Next time.#

+
    +
  • Lesson 10: Module 5 - Custom Messages

  • +
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.html b/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.html old mode 100755 new mode 100644 index 4d62930..247c6c7 --- a/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.html +++ b/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.html @@ -1,666 +1,666 @@ - - - - - - - - - - - - ICE4: Driving the Robot — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

ICE4: Driving the Robot

- -
- -
-
- - - - -
- -
-

ICE4: Driving the Robot#

-
-

Purpose#

-

This In-Class Exercise will introduce you to utilizing pre-built ROS packages to accomplish a task. It will also provide you experience interacting with someone else’s source code (.py files) to learn how that component works. You will use ROS to run two nodes, turtlebot3_core and teleop_twist_keyboard, to drive the Turtlebot3 with a keyboard. You will continue to practice using ROS tools to observe how these components communicate.

-
-
-

Code used to drive the robot#

-
    -
  1. On the Master, open a terminal and run roscore.

  2. -
  3. Open a new terminal on the Master and create a secure shell into the Turtlebot3 using the SSH command you learned during Module 2. This will allow you to run commands as if you were on the Turtlebot3.

  4. -
  5. Using the secure shell, open the source code for the turtlebot3_core launch using the nano command line editor tool through the rosed command:

    -
    $ rosed turtlebot3_bringup turtlebot3_core.launch
    -
    -
    -
    -

    ⌨️ Syntax: rosed <package> <filename>

    -
    -
    -

    Note

    -

    You may remember when we set up our .bashrc file we set the system variable EDITOR to nano -w. This enables the rosed command to utilize the nano editor.

    -
    -

    We will learn more about launch files in a few modules, but just understand that a launch file is used to launch one or more ROS nodes. This particular launch file only launches one node, serial_node.py. This node will connect to the OpenCR controller on the Turtlebot3 using the port and baud rate parameters. This connection will enable us to send Twist messages over the /cmd_vel topic to drive the Turtlebot3 using the keyboard.

    -
  6. -
  7. Close the editor by hitting ctrl+x.

  8. -
  9. It is always a good idea to check that the Turtlebot3 is communicating with the Master. To do this, we can list the active topics the Turtlebot3 sees. Run the following within your secure shell:

    -
    $ rostopic list`
    -
    -
    -

    If all is well, then there should be two topics provided by roscore running on the Master: /rosout and /rosout_agg. We will typically ignore these topics.

    -
  10. -
  11. Run the turtlebot3_core.launch file using the roslaunch command:

    -
    $ roslaunch turtlebot3_bringup turtlebot3_core.launch`
    -
    -
    -
    -

    ⌨️ Syntax: roslaunch <package> <launchfile>

    -
    -

    Your Turtlebot3 is now ready to drive and should be listening for Twist messages to be sent over the /cmd_vel topic.

    -
  12. -
-
-
-

Driving the robot#

-
    -
  1. Open a new terminal on the Master and observe the nodes currently running:

    -

    rosrun rqt_graph rqt_graph

    -

    You should only see one node running right now, turtlebot3_core, with no connections.

    -
  2. -
  3. Open a new terminal tab and list the active topics. There should be one active topic other than the ones created by roscore: /cmd_vel.

  4. -
  5. We used the /cmd_vel topic when driving the simulated Turtlebot3, but let’s refresh our memory about the topic:

    -

    rostopic info cmd_vel

    -

    As you can see the /cmd_vel topic is currently subscribed to by the turtlebot3_core with no publishers (just as we would expect after seeing the rqt_graph). We also note that topic utilizes the Twist message type. The following will show information about the fields within the Twist message sent over the /cmd_vel topic:

    -

    rostopic type cmd_vel | rosmsg show

    -
  6. -
  7. You can find information about pre-built packages by googling the package name along with the ROS distribution. Open up your favorite browser and google “teleop twist keyboard noetic”. The first result should be from the ROS wiki page.

  8. -
  9. Ensure the ROS package teleop_twist_keyboard is installed on your Master:

    -

    rospack find teleop_twist_keyboard

    -

    If installed, the command should return the absolute path to the package, similar to /opt/ros/noetic/share/teleop_twist_keyboard

    -

    If the command instead returns an error, then you need to install the package using apt:

    -

    sudo apt install ros-noetic-teleop-twist-keyboard

    -
    -

    Tip

    -

    All packages built for Noetic can be downloaded in the above manner (ros-noetic-desired-pkg with underscores in the package name replaced by dashes). Some packages were only built for previous ROS distribution and will have to be built from source (we will demonstrate this at a future time).

    -
    -
  10. -
  11. Run the teleop_twist_keyboard node on the Master:

    -
    -

    Tip

    -

    Don’t forget your tab completion! You can start typing a package name or node and then hit tab for it to complete the command for you!

    -
    -

    rosrun teleop_twist_keyboard teleop_twist_keyboard.py

    -
  12. -
  13. Before we get too excited and drive the Turtlebot3 off a cliff, observe how the nodes communicate using the rqt_graph tool in a new terminal (if you still have the previous rqt_graph running, you can hit the refresh button in the top left corner).

  14. -
  15. Select the terminal that has the teleop_twist_keyboard node running and observe the instructions for sending Twist messages. These are the same as when driving the simulated Turtlebot3.

  16. -
  17. The Turtlebot3 operates best with a linear velocity between 0.2 m/s and 0.5 m/s. It turns best with an angular velocity between 0.5 rad/s and 1.5 rad/s. Drive the Turtlebot3 using these parameters.

  18. -
-
-
-

ROS#

-

In labs throughout this course we will request information about the topics, nodes, and messages within your system. Accomplish the following in a new terminal on your Master (you can ignore all nodes/topics that result from roscore).

-
    -
  1. List all running nodes.

  2. -
  3. Determine what topics the nodes subscribe and publish to (repeat for each node).

  4. -
  5. Display running nodes and communication between them.

  6. -
  7. List the active topics.

  8. -
  9. Determine the type of messages sent over the topics (repeat for each topic).

  10. -
  11. Determine the fields of the messages.

  12. -
  13. Observe the information sent over a topic (repeat for each topic).

  14. -
-
-
-

Checkpoint#

-

Once complete, push screenshots showing the output of each of the above to your student repo on github in a /master/module04 folder.

-
-
-

Summary#

-

In this exercise you examined and used pre-built packages and source code to drive the Turtlebot3 and understand how the system worked. You then were able to analyze the topics, nodes, and messages within the ROS system to better understand the flow of information and control. The pro-tips presented throughout this exercise will make you a better user of Linux and ROS.

-
-
-

Cleanup#

-

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + ICE4: Driving the Robot — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

ICE4: Driving the Robot

+ +
+ +
+
+ + + + +
+ +
+

ICE4: Driving the Robot#

+
+

Purpose#

+

This In-Class Exercise will introduce you to utilizing pre-built ROS packages to accomplish a task. It will also provide you experience interacting with someone else’s source code (.py files) to learn how that component works. You will use ROS to run two nodes, turtlebot3_core and teleop_twist_keyboard, to drive the Turtlebot3 with a keyboard. You will continue to practice using ROS tools to observe how these components communicate.

+
+
+

Code used to drive the robot#

+
    +
  1. On the Master, open a terminal and run roscore.

  2. +
  3. Open a new terminal on the Master and create a secure shell into the Turtlebot3 using the SSH command you learned during Module 2. This will allow you to run commands as if you were on the Turtlebot3.

  4. +
  5. Using the secure shell, open the source code for the turtlebot3_core launch using the nano command line editor tool through the rosed command:

    +
    $ rosed turtlebot3_bringup turtlebot3_core.launch
    +
    +
    +
    +

    ⌨️ Syntax: rosed <package> <filename>

    +
    +
    +

    Note

    +

    You may remember when we set up our .bashrc file we set the system variable EDITOR to nano -w. This enables the rosed command to utilize the nano editor.

    +
    +

    We will learn more about launch files in a few modules, but just understand that a launch file is used to launch one or more ROS nodes. This particular launch file only launches one node, serial_node.py. This node will connect to the OpenCR controller on the Turtlebot3 using the port and baud rate parameters. This connection will enable us to send Twist messages over the /cmd_vel topic to drive the Turtlebot3 using the keyboard.

    +
  6. +
  7. Close the editor by hitting ctrl+x.

  8. +
  9. It is always a good idea to check that the Turtlebot3 is communicating with the Master. To do this, we can list the active topics the Turtlebot3 sees. Run the following within your secure shell:

    +
    $ rostopic list`
    +
    +
    +

    If all is well, then there should be two topics provided by roscore running on the Master: /rosout and /rosout_agg. We will typically ignore these topics.

    +
  10. +
  11. Run the turtlebot3_core.launch file using the roslaunch command:

    +
    $ roslaunch turtlebot3_bringup turtlebot3_core.launch`
    +
    +
    +
    +

    ⌨️ Syntax: roslaunch <package> <launchfile>

    +
    +

    Your Turtlebot3 is now ready to drive and should be listening for Twist messages to be sent over the /cmd_vel topic.

    +
  12. +
+
+
+

Driving the robot#

+
    +
  1. Open a new terminal on the Master and observe the nodes currently running:

    +

    rosrun rqt_graph rqt_graph

    +

    You should only see one node running right now, turtlebot3_core, with no connections.

    +
  2. +
  3. Open a new terminal tab and list the active topics. There should be one active topic other than the ones created by roscore: /cmd_vel.

  4. +
  5. We used the /cmd_vel topic when driving the simulated Turtlebot3, but let’s refresh our memory about the topic:

    +

    rostopic info cmd_vel

    +

    As you can see the /cmd_vel topic is currently subscribed to by the turtlebot3_core with no publishers (just as we would expect after seeing the rqt_graph). We also note that topic utilizes the Twist message type. The following will show information about the fields within the Twist message sent over the /cmd_vel topic:

    +

    rostopic type cmd_vel | rosmsg show

    +
  6. +
  7. You can find information about pre-built packages by googling the package name along with the ROS distribution. Open up your favorite browser and google “teleop twist keyboard noetic”. The first result should be from the ROS wiki page.

  8. +
  9. Ensure the ROS package teleop_twist_keyboard is installed on your Master:

    +

    rospack find teleop_twist_keyboard

    +

    If installed, the command should return the absolute path to the package, similar to /opt/ros/noetic/share/teleop_twist_keyboard

    +

    If the command instead returns an error, then you need to install the package using apt:

    +

    sudo apt install ros-noetic-teleop-twist-keyboard

    +
    +

    Tip

    +

    All packages built for Noetic can be downloaded in the above manner (ros-noetic-desired-pkg with underscores in the package name replaced by dashes). Some packages were only built for previous ROS distribution and will have to be built from source (we will demonstrate this at a future time).

    +
    +
  10. +
  11. Run the teleop_twist_keyboard node on the Master:

    +
    +

    Tip

    +

    Don’t forget your tab completion! You can start typing a package name or node and then hit tab for it to complete the command for you!

    +
    +

    rosrun teleop_twist_keyboard teleop_twist_keyboard.py

    +
  12. +
  13. Before we get too excited and drive the Turtlebot3 off a cliff, observe how the nodes communicate using the rqt_graph tool in a new terminal (if you still have the previous rqt_graph running, you can hit the refresh button in the top left corner).

  14. +
  15. Select the terminal that has the teleop_twist_keyboard node running and observe the instructions for sending Twist messages. These are the same as when driving the simulated Turtlebot3.

  16. +
  17. The Turtlebot3 operates best with a linear velocity between 0.2 m/s and 0.5 m/s. It turns best with an angular velocity between 0.5 rad/s and 1.5 rad/s. Drive the Turtlebot3 using these parameters.

  18. +
+
+
+

ROS#

+

In labs throughout this course we will request information about the topics, nodes, and messages within your system. Accomplish the following in a new terminal on your Master (you can ignore all nodes/topics that result from roscore).

+
    +
  1. List all running nodes.

  2. +
  3. Determine what topics the nodes subscribe and publish to (repeat for each node).

  4. +
  5. Display running nodes and communication between them.

  6. +
  7. List the active topics.

  8. +
  9. Determine the type of messages sent over the topics (repeat for each topic).

  10. +
  11. Determine the fields of the messages.

  12. +
  13. Observe the information sent over a topic (repeat for each topic).

  14. +
+
+
+

Checkpoint#

+

Once complete, push screenshots showing the output of each of the above to your student repo on github in a /master/module04 folder.

+
+
+

Summary#

+

In this exercise you examined and used pre-built packages and source code to drive the Turtlebot3 and understand how the system worked. You then were able to analyze the topics, nodes, and messages within the ROS system to better understand the flow of information and control. The pro-tips presented throughout this exercise will make you a better user of Linux and ROS.

+
+
+

Cleanup#

+

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module5_CustomMessages/CustomMessages.html b/Module5_CustomMessages/CustomMessages.html old mode 100755 new mode 100644 index 1226920..978b7b0 --- a/Module5_CustomMessages/CustomMessages.html +++ b/Module5_CustomMessages/CustomMessages.html @@ -1,980 +1,980 @@ - - - - - - - - - - - - Custom Messages — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Custom Messages#

-
-

Purpose#

-

This In-Class Exercise will provide you more insight into ROS messages and how information is passed between two nodes. A node can publish specific messages over a topic and other nodes are able to subscribe to that topic to receive the message. The format of these messages must be pre-defined and each node needs to know the format of the message. ROS provides a number of pre-built messages, but also allows for developers to create custom messages. In this lesson you will learn the method for and practice creating custom messages. In the corresponding lab you will develop a custom message to drive the robot. The custom message will eventually be used to enable a controller to drive the robot based on multiple data sources (e.g., IMU, LIDAR, keyboard).

-
-
-

ROS msgs#

-
-

msg#

-

ROS utilizes a simplified message description language to describe data values that ROS nodes publish. There are a lot of built-in messages that are widely used by ROS packages (common_msgs). The geometry_msgs package is one example of a pre-built message which provides the Twist message type used to drive the robot in the previous ICE.

-
-
-

Custom messages#

-

If a pre-built message type does not meet the needs of a system, custom messages can be created. A custom message is created using a .msg file, a simple text file that descries the fields of a ROS message. The catkin_make tool uses the message file to generate source code for messages. The .msg files are stored in the msg directory of a package. There are two parts to a .msg file: fields and constants. Fields are the data that is sent inside of the message. Constants define useful values that can be used to interpret those fields. We will primarily use fields.

-
-
-

Fields#

-

Each field consists of a type and a name, separated by a space per line. The field types you can use are:

-
    -
  • int8, int16, int64 (plus uint*)

  • -
  • float32, float64

  • -
  • string

  • -
  • time, duration

  • -
  • other msg files

  • -
  • variable length array[] and fixed-length array[X].

  • -
-
- -
-

Format#

-
Header header
-fieldtype fieldname
-
-
-

For example, if we created a custom message file titled Person.msg that describes a person it might look like this:

-
Header header
-string firstname
-string lastname
-int32 age
-
-
-
-
-

Importing messages#

-

To utilize a msg in a node it must first be imported into the script.

-
    from geometry_msgs.msg import Twist
-    from ice5.msg import Person
-
-
-
-

⌨️ Syntax: from <package>.msg import msg

-
-
-
-

Using messages#

-

After importing the msg you can access the fields similar to any object. For example, if we created an instance variable to store our Person message:

-
person = Person()
-
-
-

You would then access the fields using the field names of the msg:

-
print("%s %s is %d years old!" % (person.firstname, person.lastname, person.age))
-
-
-

If you wanted to set the linear x and angular z speeds of a Twist message before publishing it to the TurtleBot3 you would again use the field names provided by the pre-built message. If you googled the Twist message (Twist Message, you would see the contents of the Twist message include two other messages, linear and angular, of type Vector3:

-
geometry_msgs/Vector3 linear
-geometry_msgs/Vector3 angular
-
-
-

Each of those two messages include the same fields and fieldnames:

-
float64 x
-float64 y
-float64 z
-
-
-

To set the linear x and angular z values, we have to access those fields using an objected oriented method. For example:

-
    from gemoetry_msgs.msg import Twist
-    
-    # create a publisher to send Twist messages over the cmd_vel topic
-    pub = rospy.Publisher('cmd_vel', Twist, queue_size = 1)
-    
-    # set the Twist message to drive the robot straight at 0.25 m/s
-    bot_cmd = Twist()
-    bot_cmd.linear.x = 0.25  # notice you have to access both the "linear" & "x" fields of the Twist message
-    bot_cmd.angular.z = 0.0
-    
-    # publish the Twist message
-    pub.publish(bot_cmd)
-
-
-
-
-
-

In-Class Exercise 5#

-

In this exercise you will create a custom message that describes a person. This message will provide two strings, first and last name, and an integer age for a person. We will then create a node that publishes information about that person and a node that subscribes to that information.

-
-

Create the custom message:#

-
    -
  1. In a new terminal on the Master, create an ice5 package which depends on the std_msgs package and rospy package, compile and source the ws:

    -
    cd ~/master_ws/src/ece387_master_spring2024-USERNAME/master
    -catkin_create_pkg ice5 std_msgs rospy
    -cd ~/master_ws
    -catkin_make
    -source ~/.bashrc
    -
    -
    -
  2. -
  3. Change directory to the package folder and create a msg directory:

    -
    roscd ice5
    -mkdir msg
    -cd msg
    -
    -
    -
  4. -
  5. Create the msg file for the Person and add the fields previously discussed (header, firstname, lastname, and age):

    -
    nano Person.msg
    -
    -
    -
  6. -
  7. Save and exit: ctrl+s, ctrl+x

  8. -
-
-
-

Write the Publisher#

-
    -
  1. Create the file for the publisher:

    -
    roscd ice5/src
    -touch ice5_publisher.py
    -
    -
    -
  2. -
  3. Copy the below code to the ice5_publisher.py file and fill in the required lines (look for the TODO tag). You can edit via the terminal using nano, but it is often easier to use a GUI editor. Browse to the publisher in the file browser and double-click. This will open the file in thonny (if it is open in any other editor, stop, raise your hand, and get help from an instructor)

  4. -
-
#!/usr/bin/env python3
-import rospy
-from ice5.msg import Person # import the message: from PKG.msg import MSG
-
-class Talker:
-    """Class that publishes basic information about person"""
-    def __init__(self, first = "Cadet", last = "Snuffy", age = 21):
-        self.msg = Person()         # creates a Person message
-        self.msg.firstname = first  # assign the firstname field
-        self.msg.lastname = last    # assign the lastname field
-        self.msg.age = age          # assign the age field
-        
-        # TODO: create the publisher that publishes Person messages over the person topic
-        # Since we don't care about losing messages we can set the queue size to 1
-        self.pub =
-        
-        # TODO: create a timer that will call the callback_publish() function every .1 seconds (10 Hz)
-        rospy.Timer()
-        
-        # nicely handle shutdown (ctrl+c)
-        self.ctrl_c = False
-        rospy.on_shutdown(self.shutdownhook)
-        
-    def callback_publish(self, event):
-        if not self.ctrl_c:
-            # TODO: publish the msg
-            
-            
-    def shutdownhook(self):
-        print("Shutting down publisher.")
-        self.ctrl_c = True
-    	
-if __name__ == '__main__':
-    rospy.init_node('talker')
-    
-    # create an instance of the Talker class changing the class variables
-    Talker("Steven", "Beyer", 33)
-    rospy.spin()	# run forever until ctrl+c    
-
-
-
    -
  1. Save and exit.

  2. -
  3. Make the node executable.

  4. -
-
-
-

Write the Subscriber#

-
    -
  1. Create the file for the subscriber:

    -
    touch ice5_subscriber.py
    -
    -
    -
  2. -
  3. Copy the below code to the ice5_subscriber.py file and fill in the required lines (look for the TODO tag).

  4. -
-
#!/usr/bin/env python3
-import rospy
-from ice5.msg import Person # import the message: from PKG.msg import MSG
-
-class Listener:
-    """Listener class that prints information about person"""
-    def __init__(self):
-    	# TODO: create the subscriber that receives Person messages over the person topic
-    	# and calls the callback_person() function.
-        
-        
-        # nicely handle shutdown (Ctrl+c)
-        rospy.on_shutdown(self.shutdownhook)
-        
-    def callback_person(self, person):
-    	# TODO: print the information about the person
-        
-        
-    def shutdownhook(self):
-    	print("Shutting down subscriber.")
-        
-if __name__ == '__main__':
-    rospy.init_node('listener')
-    # create an instance of the class
-    Listener()
-    # keeps python from exiting until this node is stopped
-    rospy.spin()
-
-
-
    -
  1. Save and exit.

  2. -
  3. Make the node executable.

  4. -
-
-
-

Requirements to use custom messages.#

-

There are a number of settings that have to be set within the package.xml and CMakeLists.txt files that tell catkin to compile the messages.

-
-

package.xml#

-
    -
  1. Edit package.xml (rosed ice5 package.xml) and uncomment these two lines (remove arrows on both sides of the line):

    -
    <build_depend>message_generation</build_depend>
    -<exec_depend>message_runtime</exec_depend>
    -
    -
    -
  2. -
  3. Save and exit.

  4. -
-
-
-

CMakeLists.txt#

-
    -
  1. Edit CMakeLists.txt (rosed ice5 CMakeLists.txt) and make the following changes:

    -
      -
    1. Add the message_generation dependency to the find_package call so that you can generate messages:

      -
      # Do not just add this to your CMakeLists.txt, modify the existing text to 
      -# add message_generation before the closing parenthesis
      -find_package(catkin REQUIRED COMPONENTS
      -    rospy
      -    std_msgs
      -    message_generation
      -)
      -
      -
      -
    2. -
    3. Find the following block of code:

      -
      ## Generate messages in the 'msg' folder
      -# add_message_files(
      -#    FILES
      -#    Message1.msg
      -#    Message2.msg
      -#)
      -
      -
      -

      and uncomment by removing the # symbols and then replace the Message*.msg files with your .msg file, such that it looks like this:

      -
      add_message_files(
      -    FILES
      -    Person.msg
      -)
      -
      -
      -
    4. -
    5. Find the following block of code:

      -
      # generate_messages(
      -#    DEPENDENCIES
      -#    std_msgs
      -#)
      -
      -
      -

      and uncomment so it looks like:

      -
      generate_messages(
      -    DEPENDENCIES
      -    std_msgs
      -)
      -
      -
      -
    6. -
    7. Uncomment and add the message_runtime dependency to the CATKIN_DEPENDS line in the catkin_package() function near the bottom without changing anything else:

      -
      catkin_package(
      -    ...
      -    CATKIN_DEPENDS rospy std_msgs message_runtime
      -    ...
      -)
      -
      -
      -
      -
    8. -
    9. Save and exit.

    10. -
    -
  2. -
-
-
-
-

Compile and run the code#

-
    -
  1. Make and source your package:

    -
    cd ~/master_ws
    -catkin_make
    -source ~/.bashrc
    -
    -
    -
  2. -
  3. Run roscore!

  4. -
  5. The rospy tool can measure certain statistics for every topic connection. We can visualize this in rqt_graph, but we have to enable it after roscore, but before any nodes. In a new terminal run the following to enable statistics:

    -
    rosparam set enable_statistics true
    -
    -
    -
  6. -
  7. Run the publisher:

    -
    rosrun ice5 ice5_publisher.py
    -
    -
    -
  8. -
  9. In a new terminal, run the subscriber:

    -
    rosrun ice5 ice5_subscriber.py
    -
    -
    -
  10. -
  11. In a new terminal observe the nodes running:

    -
    rosnode list
    -
    -
    -
  12. -
  13. Observe information about each node:

    -
    rosnode info /talker
    -
    -
    -
  14. -
  15. Observe how information is being passed:

    -
    rosrun rqt_graph rqt_graph
    -
    -
    -
    -

    📝️ Note: You may have to hit refresh a few times to get the statistics previously mentioned.

    -
    -
  16. -
  17. Observe all active topics:

    -
    rostopic list
    -
    -
    -
  18. -
  19. Observe the information about the message sent over the topics (repeat for each topic, remember we do not care about topics we did not create (e.g., rosout, rosout_agg, statistics)).

    -
    rostopic info person
    -
    -
    -
  20. -
  21. Observe the fields of the message sent over each topic:

    -
    rostopic type person | rosmsg show
    -
    -
    -
  22. -
-
-
-
-

Checkpoint#

-

Once complete, get checked off by an instructor showing the output of each of the above.

-
-
-

Summary#

-

In this lesson you learned about ROS pre-built and custom messages! You created your own message which described a Person. You then developed a publisher to send information about the Person to a subscriber.

-
-
-

Cleanup#

-

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-

Ensure roscore is terminated before moving on to the lab.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Custom Messages — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Custom Messages#

+
+

Purpose#

+

This In-Class Exercise will provide you more insight into ROS messages and how information is passed between two nodes. A node can publish specific messages over a topic and other nodes are able to subscribe to that topic to receive the message. The format of these messages must be pre-defined and each node needs to know the format of the message. ROS provides a number of pre-built messages, but also allows for developers to create custom messages. In this lesson you will learn the method for and practice creating custom messages. In the corresponding lab you will develop a custom message to drive the robot. The custom message will eventually be used to enable a controller to drive the robot based on multiple data sources (e.g., IMU, LIDAR, keyboard).

+
+
+

ROS msgs#

+
+

msg#

+

ROS utilizes a simplified message description language to describe data values that ROS nodes publish. There are a lot of built-in messages that are widely used by ROS packages (common_msgs). The geometry_msgs package is one example of a pre-built message which provides the Twist message type used to drive the robot in the previous ICE.

+
+
+

Custom messages#

+

If a pre-built message type does not meet the needs of a system, custom messages can be created. A custom message is created using a .msg file, a simple text file that descries the fields of a ROS message. The catkin_make tool uses the message file to generate source code for messages. The .msg files are stored in the msg directory of a package. There are two parts to a .msg file: fields and constants. Fields are the data that is sent inside of the message. Constants define useful values that can be used to interpret those fields. We will primarily use fields.

+
+
+

Fields#

+

Each field consists of a type and a name, separated by a space per line. The field types you can use are:

+
    +
  • int8, int16, int64 (plus uint*)

  • +
  • float32, float64

  • +
  • string

  • +
  • time, duration

  • +
  • other msg files

  • +
  • variable length array[] and fixed-length array[X].

  • +
+
+ +
+

Format#

+
Header header
+fieldtype fieldname
+
+
+

For example, if we created a custom message file titled Person.msg that describes a person it might look like this:

+
Header header
+string firstname
+string lastname
+int32 age
+
+
+
+
+

Importing messages#

+

To utilize a msg in a node it must first be imported into the script.

+
    from geometry_msgs.msg import Twist
+    from ice5.msg import Person
+
+
+
+

⌨️ Syntax: from <package>.msg import msg

+
+
+
+

Using messages#

+

After importing the msg you can access the fields similar to any object. For example, if we created an instance variable to store our Person message:

+
person = Person()
+
+
+

You would then access the fields using the field names of the msg:

+
print("%s %s is %d years old!" % (person.firstname, person.lastname, person.age))
+
+
+

If you wanted to set the linear x and angular z speeds of a Twist message before publishing it to the TurtleBot3 you would again use the field names provided by the pre-built message. If you googled the Twist message (Twist Message, you would see the contents of the Twist message include two other messages, linear and angular, of type Vector3:

+
geometry_msgs/Vector3 linear
+geometry_msgs/Vector3 angular
+
+
+

Each of those two messages include the same fields and fieldnames:

+
float64 x
+float64 y
+float64 z
+
+
+

To set the linear x and angular z values, we have to access those fields using an objected oriented method. For example:

+
    from gemoetry_msgs.msg import Twist
+    
+    # create a publisher to send Twist messages over the cmd_vel topic
+    pub = rospy.Publisher('cmd_vel', Twist, queue_size = 1)
+    
+    # set the Twist message to drive the robot straight at 0.25 m/s
+    bot_cmd = Twist()
+    bot_cmd.linear.x = 0.25  # notice you have to access both the "linear" & "x" fields of the Twist message
+    bot_cmd.angular.z = 0.0
+    
+    # publish the Twist message
+    pub.publish(bot_cmd)
+
+
+
+
+
+

In-Class Exercise 5#

+

In this exercise you will create a custom message that describes a person. This message will provide two strings, first and last name, and an integer age for a person. We will then create a node that publishes information about that person and a node that subscribes to that information.

+
+

Create the custom message:#

+
    +
  1. In a new terminal on the Master, create an ice5 package which depends on the std_msgs package and rospy package, compile and source the ws:

    +
    cd ~/master_ws/src/ece387_master_spring2024-USERNAME/master
    +catkin_create_pkg ice5 std_msgs rospy
    +cd ~/master_ws
    +catkin_make
    +source ~/.bashrc
    +
    +
    +
  2. +
  3. Change directory to the package folder and create a msg directory:

    +
    roscd ice5
    +mkdir msg
    +cd msg
    +
    +
    +
  4. +
  5. Create the msg file for the Person and add the fields previously discussed (header, firstname, lastname, and age):

    +
    nano Person.msg
    +
    +
    +
  6. +
  7. Save and exit: ctrl+s, ctrl+x

  8. +
+
+
+

Write the Publisher#

+
    +
  1. Create the file for the publisher:

    +
    roscd ice5/src
    +touch ice5_publisher.py
    +
    +
    +
  2. +
  3. Copy the below code to the ice5_publisher.py file and fill in the required lines (look for the TODO tag). You can edit via the terminal using nano, but it is often easier to use a GUI editor. Browse to the publisher in the file browser and double-click. This will open the file in thonny (if it is open in any other editor, stop, raise your hand, and get help from an instructor)

  4. +
+
#!/usr/bin/env python3
+import rospy
+from ice5.msg import Person # import the message: from PKG.msg import MSG
+
+class Talker:
+    """Class that publishes basic information about person"""
+    def __init__(self, first = "Cadet", last = "Snuffy", age = 21):
+        self.msg = Person()         # creates a Person message
+        self.msg.firstname = first  # assign the firstname field
+        self.msg.lastname = last    # assign the lastname field
+        self.msg.age = age          # assign the age field
+        
+        # TODO: create the publisher that publishes Person messages over the person topic
+        # Since we don't care about losing messages we can set the queue size to 1
+        self.pub =
+        
+        # TODO: create a timer that will call the callback_publish() function every .1 seconds (10 Hz)
+        rospy.Timer()
+        
+        # nicely handle shutdown (ctrl+c)
+        self.ctrl_c = False
+        rospy.on_shutdown(self.shutdownhook)
+        
+    def callback_publish(self, event):
+        if not self.ctrl_c:
+            # TODO: publish the msg
+            
+            
+    def shutdownhook(self):
+        print("Shutting down publisher.")
+        self.ctrl_c = True
+    	
+if __name__ == '__main__':
+    rospy.init_node('talker')
+    
+    # create an instance of the Talker class changing the class variables
+    Talker("Steven", "Beyer", 33)
+    rospy.spin()	# run forever until ctrl+c    
+
+
+
    +
  1. Save and exit.

  2. +
  3. Make the node executable.

  4. +
+
+
+

Write the Subscriber#

+
    +
  1. Create the file for the subscriber:

    +
    touch ice5_subscriber.py
    +
    +
    +
  2. +
  3. Copy the below code to the ice5_subscriber.py file and fill in the required lines (look for the TODO tag).

  4. +
+
#!/usr/bin/env python3
+import rospy
+from ice5.msg import Person # import the message: from PKG.msg import MSG
+
+class Listener:
+    """Listener class that prints information about person"""
+    def __init__(self):
+    	# TODO: create the subscriber that receives Person messages over the person topic
+    	# and calls the callback_person() function.
+        
+        
+        # nicely handle shutdown (Ctrl+c)
+        rospy.on_shutdown(self.shutdownhook)
+        
+    def callback_person(self, person):
+    	# TODO: print the information about the person
+        
+        
+    def shutdownhook(self):
+    	print("Shutting down subscriber.")
+        
+if __name__ == '__main__':
+    rospy.init_node('listener')
+    # create an instance of the class
+    Listener()
+    # keeps python from exiting until this node is stopped
+    rospy.spin()
+
+
+
    +
  1. Save and exit.

  2. +
  3. Make the node executable.

  4. +
+
+
+

Requirements to use custom messages.#

+

There are a number of settings that have to be set within the package.xml and CMakeLists.txt files that tell catkin to compile the messages.

+
+

package.xml#

+
    +
  1. Edit package.xml (rosed ice5 package.xml) and uncomment these two lines (remove arrows on both sides of the line):

    +
    <build_depend>message_generation</build_depend>
    +<exec_depend>message_runtime</exec_depend>
    +
    +
    +
  2. +
  3. Save and exit.

  4. +
+
+
+

CMakeLists.txt#

+
    +
  1. Edit CMakeLists.txt (rosed ice5 CMakeLists.txt) and make the following changes:

    +
      +
    1. Add the message_generation dependency to the find_package call so that you can generate messages:

      +
      # Do not just add this to your CMakeLists.txt, modify the existing text to 
      +# add message_generation before the closing parenthesis
      +find_package(catkin REQUIRED COMPONENTS
      +    rospy
      +    std_msgs
      +    message_generation
      +)
      +
      +
      +
    2. +
    3. Find the following block of code:

      +
      ## Generate messages in the 'msg' folder
      +# add_message_files(
      +#    FILES
      +#    Message1.msg
      +#    Message2.msg
      +#)
      +
      +
      +

      and uncomment by removing the # symbols and then replace the Message*.msg files with your .msg file, such that it looks like this:

      +
      add_message_files(
      +    FILES
      +    Person.msg
      +)
      +
      +
      +
    4. +
    5. Find the following block of code:

      +
      # generate_messages(
      +#    DEPENDENCIES
      +#    std_msgs
      +#)
      +
      +
      +

      and uncomment so it looks like:

      +
      generate_messages(
      +    DEPENDENCIES
      +    std_msgs
      +)
      +
      +
      +
    6. +
    7. Uncomment and add the message_runtime dependency to the CATKIN_DEPENDS line in the catkin_package() function near the bottom without changing anything else:

      +
      catkin_package(
      +    ...
      +    CATKIN_DEPENDS rospy std_msgs message_runtime
      +    ...
      +)
      +
      +
      +
      +
    8. +
    9. Save and exit.

    10. +
    +
  2. +
+
+
+
+

Compile and run the code#

+
    +
  1. Make and source your package:

    +
    cd ~/master_ws
    +catkin_make
    +source ~/.bashrc
    +
    +
    +
  2. +
  3. Run roscore!

  4. +
  5. The rospy tool can measure certain statistics for every topic connection. We can visualize this in rqt_graph, but we have to enable it after roscore, but before any nodes. In a new terminal run the following to enable statistics:

    +
    rosparam set enable_statistics true
    +
    +
    +
  6. +
  7. Run the publisher:

    +
    rosrun ice5 ice5_publisher.py
    +
    +
    +
  8. +
  9. In a new terminal, run the subscriber:

    +
    rosrun ice5 ice5_subscriber.py
    +
    +
    +
  10. +
  11. In a new terminal observe the nodes running:

    +
    rosnode list
    +
    +
    +
  12. +
  13. Observe information about each node:

    +
    rosnode info /talker
    +
    +
    +
  14. +
  15. Observe how information is being passed:

    +
    rosrun rqt_graph rqt_graph
    +
    +
    +
    +

    📝️ Note: You may have to hit refresh a few times to get the statistics previously mentioned.

    +
    +
  16. +
  17. Observe all active topics:

    +
    rostopic list
    +
    +
    +
  18. +
  19. Observe the information about the message sent over the topics (repeat for each topic, remember we do not care about topics we did not create (e.g., rosout, rosout_agg, statistics)).

    +
    rostopic info person
    +
    +
    +
  20. +
  21. Observe the fields of the message sent over each topic:

    +
    rostopic type person | rosmsg show
    +
    +
    +
  22. +
+
+
+
+

Checkpoint#

+

Once complete, get checked off by an instructor showing the output of each of the above.

+
+
+

Summary#

+

In this lesson you learned about ROS pre-built and custom messages! You created your own message which described a Person. You then developed a publisher to send information about the Person to a subscriber.

+
+
+

Cleanup#

+

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+

Ensure roscore is terminated before moving on to the lab.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module5_CustomMessages/Lab1_CustomMessages.html b/Module5_CustomMessages/Lab1_CustomMessages.html old mode 100755 new mode 100644 index 02d6aa9..94e3d49 --- a/Module5_CustomMessages/Lab1_CustomMessages.html +++ b/Module5_CustomMessages/Lab1_CustomMessages.html @@ -1,643 +1,643 @@ - - - - - - - - - - - - Lab 1: Custom Messages — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Lab 1: Custom Messages#

-
-

Synopsis#

-

This lab will provide practice in creating custom messages. You will use provided code that listens for events from the mouse. Specifically, the code is listening to the position of the cursor, and any buttons pressed. You will create a custom message that is going to pass the cursor position and mouse button events across the /mouse_info topic. You will develop a controller that will subscribe to the /mouse_info topic and then publish a Twist message to /cmd_vel according to the requirements in the lab handout.

-
-
-

Due by 0730 Lesson 13 (M or T depending on which day your class meets)#

-

Before starting this lab, you NEED to complete ICE5. We will work most of this together as a class, but some will be self effort to practice the concepts needed for this lab.

-
-
-

Lab Objectives:#

-

In this lab, we will be building upon everything we have learned in the first 5 modules of the course. We are -going add functionality to use basic code that I have created to:

-
    -
  1. Read the mouse position

  2. -
  3. Detect button or scroll wheel events from the mouse

  4. -
  5. Send custom message to controller

  6. -
  7. The Controller will then scale a Twist message on the \cmd_vel topic to drive the robot

  8. -
-

In short, we are going to use the mouse to drive the robot. Moving the mouse cursor up or down on the screen will drive the robot forward and backward. Moving the mouse cursor side to side on the screen will cause the robot to turn left or right. There should also be a dead zone within +/- .25 of the center of the screen where no movement will occur The code template that I have provided for the mouse client depends on a couple of different libraries. We are going to use the python installer (pip) to install two libraries, pyautogui and pynput.

-
dfec@master:∼$ pip install pyautogui
-dfec@master:∼$ pip install pynput
-
-
-

Essentially the code you are going to create will develop the following graph. This graph below does not have the robot nodes on it, so you won’t see the /cmd_vel topic active. However, you will either need to use the turtlebot simulation or your actual robot to prove that the code is working. Part of your lab report will require you to show the complete graph.

-../_images/mouse_info_topic.png -
-

Mouse Node:#

-

The Mouse node will be created by the mouse_client_OO.py code. The framework for this code is provided here. Look for the TODO sections to see where you need to edit the code. The Mouse node will primarily detect mouse position and events and publish a custom MouseController message that you will create in a lab1 package within the master folder of your repo.

-
-
-

Controller Node:#

-

The controller node is created by the turtlebot_controller.py code. I have also provided the framework for this code here. The controller node will subscribe to the /mouse_info topic and publish a Twist message to the /cmd_vel topic

-
-
-
-

Your Custom MouseController Message.#

-

Your MouseController message needs to have precisely the following 4 fields in it:

-
    -
  1. Header

  2. -
  3. status (of type std_msgs/Bool)

  4. -
  5. xPos (of type float32)

  6. -
  7. yPos (of type float32)

  8. -
-

You will need to use the procedure highlighted in ICE5 to create this custom message. Notice that the second field in the messages above is a message from the std_msgs group in ROS. Remember, this is an acceptable field based on what you learned in Module05. If you want to get more familiar with the Bool -message, you can use the built in ROS functionality to see what the format of this message will be:

-
dfec@master:∼$ rosmsg info std_msgs/Bool
-
-
-
-
-

Additional Requirements.#

-

Your code must adhere to the following additional requirements:

-
    -
  1. All code and message files should be stored in a package within the master folder called lab1. DO NOT EDIT THE CODE FILES IN THE Module05 FOLDER IN THE ECE387 CURRICULUM!!!

  2. -
  3. The controller will only be activated by scrolling down with the mouse (on the center scroll wheel)

  4. -
  5. The controller will be immediately deactivated by scrolling up with the mouse.

  6. -
  7. If the controller is deactivated, the robot will stop all movement, until an override by another module.

  8. -
  9. You don’t need to handle the override at this point, but your controller code should be organized and flexible enough that it can handle additional capability in the future.

  10. -
-
-
-

Report.#

-

Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under Resources/Lab Template.

-
-
-

Turn-in Requirements#

-

[25 points] Demonstration of mouse control of Turtlebot (preferably in person, but can be recorded and posted to Teams under the Lab1 channel).

-

[50 points] Report via Gradescope.

-

[25 points] Code/ROS package: push your code to your repository. Also, include a screenshot or the raw text of the turtlebot_controller.py and mouse_client_OO.py files at the end of your report.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Lab 1: Custom Messages — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Lab 1: Custom Messages#

+
+

Synopsis#

+

This lab will provide practice in creating custom messages. You will use provided code that listens for events from the mouse. Specifically, the code is listening to the position of the cursor, and any buttons pressed. You will create a custom message that is going to pass the cursor position and mouse button events across the /mouse_info topic. You will develop a controller that will subscribe to the /mouse_info topic and then publish a Twist message to /cmd_vel according to the requirements in the lab handout.

+
+
+

Due by 0730 Lesson 13 (M or T depending on which day your class meets)#

+

Before starting this lab, you NEED to complete ICE5. We will work most of this together as a class, but some will be self effort to practice the concepts needed for this lab.

+
+
+

Lab Objectives:#

+

In this lab, we will be building upon everything we have learned in the first 5 modules of the course. We are +going add functionality to use basic code that I have created to:

+
    +
  1. Read the mouse position

  2. +
  3. Detect button or scroll wheel events from the mouse

  4. +
  5. Send custom message to controller

  6. +
  7. The Controller will then scale a Twist message on the \cmd_vel topic to drive the robot

  8. +
+

In short, we are going to use the mouse to drive the robot. Moving the mouse cursor up or down on the screen will drive the robot forward and backward. Moving the mouse cursor side to side on the screen will cause the robot to turn left or right. There should also be a dead zone within +/- .25 of the center of the screen where no movement will occur The code template that I have provided for the mouse client depends on a couple of different libraries. We are going to use the python installer (pip) to install two libraries, pyautogui and pynput.

+
dfec@master:∼$ pip install pyautogui
+dfec@master:∼$ pip install pynput
+
+
+

Essentially the code you are going to create will develop the following graph. This graph below does not have the robot nodes on it, so you won’t see the /cmd_vel topic active. However, you will either need to use the turtlebot simulation or your actual robot to prove that the code is working. Part of your lab report will require you to show the complete graph.

+../_images/mouse_info_topic.png +
+

Mouse Node:#

+

The Mouse node will be created by the mouse_client_OO.py code. The framework for this code is provided here. Look for the TODO sections to see where you need to edit the code. The Mouse node will primarily detect mouse position and events and publish a custom MouseController message that you will create in a lab1 package within the master folder of your repo.

+
+
+

Controller Node:#

+

The controller node is created by the turtlebot_controller.py code. I have also provided the framework for this code here. The controller node will subscribe to the /mouse_info topic and publish a Twist message to the /cmd_vel topic

+
+
+
+

Your Custom MouseController Message.#

+

Your MouseController message needs to have precisely the following 4 fields in it:

+
    +
  1. Header

  2. +
  3. status (of type std_msgs/Bool)

  4. +
  5. xPos (of type float32)

  6. +
  7. yPos (of type float32)

  8. +
+

You will need to use the procedure highlighted in ICE5 to create this custom message. Notice that the second field in the messages above is a message from the std_msgs group in ROS. Remember, this is an acceptable field based on what you learned in Module05. If you want to get more familiar with the Bool +message, you can use the built in ROS functionality to see what the format of this message will be:

+
dfec@master:∼$ rosmsg info std_msgs/Bool
+
+
+
+
+

Additional Requirements.#

+

Your code must adhere to the following additional requirements:

+
    +
  1. All code and message files should be stored in a package within the master folder called lab1. DO NOT EDIT THE CODE FILES IN THE Module05 FOLDER IN THE ECE387 CURRICULUM!!!

  2. +
  3. The controller will only be activated by scrolling down with the mouse (on the center scroll wheel)

  4. +
  5. The controller will be immediately deactivated by scrolling up with the mouse.

  6. +
  7. If the controller is deactivated, the robot will stop all movement, until an override by another module.

  8. +
  9. You don’t need to handle the override at this point, but your controller code should be organized and flexible enough that it can handle additional capability in the future.

  10. +
+
+
+

Report.#

+

Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under Resources/Lab Template.

+
+
+

Turn-in Requirements#

+

[25 points] Demonstration of mouse control of Turtlebot (preferably in person, but can be recorded and posted to Teams under the Lab1 channel).

+

[50 points] Report via Gradescope.

+

[25 points] Code/ROS package: push your code to your repository. Also, include a screenshot or the raw text of the turtlebot_controller.py and mouse_client_OO.py files at the end of your report.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module6_IMU/IMU.html b/Module6_IMU/IMU.html old mode 100755 new mode 100644 index f7c5948..4a8ba06 --- a/Module6_IMU/IMU.html +++ b/Module6_IMU/IMU.html @@ -1,702 +1,702 @@ - - - - - - - - - - - - Inertial Measurement Unit — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

Inertial Measurement Unit

- -
- -
-
- - - - -
- -
-

Inertial Measurement Unit#

-
-

Purpose#

-

In practice, an inertial measurement unit (IMU) device provides orientation, angular velocity, and linear acceleration. The ICM-20648 6-Axis MEMS MotionTracking Device from TDK includes a 3-axis gyroscope, 3-axis accelerometer, and a Digital Motion Processor (DMP). This IMU is integrated on the OpenCR1.0 board, an open source robot controller embedded with an ARM Cortex-M7 processor. The OpenCR combines sensor data using an EKF to generate IMU estimates 170 times a second.

-

logo

-

The IMU provides values that are part of the robot state and allow the robot to navigate more accurately. Combined with data from the tachometers these values provide the odometry of the robot to estimate change in position over time. We will primarily use the IMU to perform 90 and 180 degree turns.

-

logo

-
-
-

Calibrating the IMU#

-

As described above, there are a number of different sensors that work together to provide the attitude and heading estimates for the Turtlebot3. These sensors are sensitive to magnetic fields which are unique to locale and device. As you will learn in future ECE classes, all electronic devices create small magnetic fields. Even electrons traveling over a wire create magnetic fields. The OpenCR board and IMU are strategically placed in the center of the robot for best attitude and heading performance, however, this location is also in the center of a number of magnetic fields. Luckily for us, the creators of the Turtlebot3 were aware of these issues and whenever you run the serial node to connect to the robot the IMU is calibrated.

-
-
-

Setup#

-

The ICM-20648 is already integrated into the Turtlebot3 robot, therefore, there is no setup required. Whenever the serial node is ran to connect to the OpenCR board, the IMU is initialized and will start publishing data.

-
-
-

Test the IMU#

-

Open a new terminal on the master and run roscore and setup for statistics:

-
roscore
-rosparam set enable_statistics true
-
-
-

Create a secure shell connection to your Robot and launch the Turtlebot3 core launchfile.

-
roslaunch turtlebot3_bringup turtlebot3_core.launch
-
-
-

Open a new terminal on your Master and observe what topics are running.

-

You should note two topics of interest: /imu and /odom.

-

Echo the output of each of the topics and rotate the Robot to see the values change.

-

The /imu topic combines information from the gyroscope and accelerometer to provide orientation, angular velocity, and linear acceleration. The /odom topic combines the information from the /imu topic and tachometers to estimate position, orientation, and linear and angular velocities.

-

Both of these topics provide the orientation of the robot using a quaternion representation. While quaternions can make computation easier, they are not very human readable, so we will convert to Euler angles. To do this we will use a Python library called squaternion.

-

The two main functions we will use from the squaternion library:

-

First we will need to create a Quaternion object:

-
q = Quaternion(w, x, y, z)
-
-
-

Then we will conver that Quaternion to an Euler:

-
e = q.to_euler(degrees=True)
-
-
-

Now we have e, an array representing our Euler angles, e[roll, pitch, yaw].

-

You can keep the node running for the next portion of the lesson.

-
-
-

Write the Subscriber#

-
    -
  1. In a new terminal on the Master, create an ice6 package which depends on the geometry_msgs, rospy, and turtlebot3_bringup packages, compile and source the workspace:

    -
    cd ~/master_ws/src/ece387_master_sp2X-USERNAME/master
    -catkin_create_pkg ice6 std_msgs rospy turtlebot3_bringup
    -cd ~/master_ws
    -catkin_make
    -source ~/.bashrc
    -
    -
    -
  2. -
  3. Create an IMU node:

    -
    roscd ice6/src
    -touch imu_sub.py
    -
    -
    -
  4. -
  5. Copy and complete the below code using a GUI editor tool, such as Sublime or VS Code. Browse to the subscriber you just created and double-click. This will open the file in Sublime or VS Code (if it is open in any other editor, stop, raise your hand, and get help from an instructor)

  6. -
-
-

💡️ Tip: Look for the “TODO” tag which indicates where you should insert your own code.

-
-
#!/usr/bin/env python3
-import rospy
-from squaternion import Quaternion
-# TODO: import message type sent over imu topic
-
-
-class IMU:
-    """Class to read orientation data from Turtlebot3 IMU"""
-    def __init__(self):        
-        # TODO: subscribe to the imu topic that is published by the
-        # Turtlebot3 and provides the robot orientation
-
-
-        # nicely handle shutdown (Ctrl+c)
-        self.ctrl_c = False
-        rospy.on_shutdown(self.shutdownhook)
-
-    # The IMU provides yaw from -180 to 180. This function
-    # converts the yaw (in degrees) to 0 to 360
-    def convert_yaw (self, yaw):
-        return 360 + yaw if yaw < 0 else yaw		
-
-    # Print the current Yaw
-    def callback_imu(self, imu):
-        if not self.ctrl_c:
-            # TODO: create a quaternion using the x, y, z, and w values
-            # from the correct imu message
-            
-            
-            # TODO: convert the quaternion to euler in degrees
-            
-            
-            # TODO: get the yaw component of the euler
-            yaw = 
-
-            # convert yaw from -180 to 180 to 0 to 360
-            yaw = self.convert_yaw(yaw)
-            print("Current heading is %f degrees." % (yaw))
-
-    # clean shutdown
-    def shutdownhook(self):
-        print("Shutting down the IMU subscriber")
-        self.ctrl_c = True
-
-if __name__ == '__main__':
-    rospy.init_node('imu_sub')
-    IMU()
-    rospy.spin()
-
-
-
    -
  1. Save, exit, and make the node executable.

  2. -
  3. Open a new terminal on the Master and run the imu_sub.py node.

  4. -
  5. Rotate the Robot and observe the output.

  6. -
-
-
-

Checkpoint#

-

Once complete, get checked off by an instructor showing the output of your imu_sub and rqt_graph node. Push your ice6 package to your repo for credit

-
-
-

Summary#

-

In this lesson you learned how to utilize the on-board IMU and determine the orientation of the Turtlebot3. In the lab that corresponds to this lesson you will apply this knowledge to turn the robot in 90 and 180 degree turns.ROS

-
-
-

Cleanup#

-

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-lab in. Select ‘y’.

-

Ensure roscore is terminated before moving on to the lab.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Inertial Measurement Unit — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Inertial Measurement Unit

+ +
+ +
+
+ + + + +
+ +
+

Inertial Measurement Unit#

+
+

Purpose#

+

In practice, an inertial measurement unit (IMU) device provides orientation, angular velocity, and linear acceleration. The ICM-20648 6-Axis MEMS MotionTracking Device from TDK includes a 3-axis gyroscope, 3-axis accelerometer, and a Digital Motion Processor (DMP). This IMU is integrated on the OpenCR1.0 board, an open source robot controller embedded with an ARM Cortex-M7 processor. The OpenCR combines sensor data using an EKF to generate IMU estimates 170 times a second.

+

logo

+

The IMU provides values that are part of the robot state and allow the robot to navigate more accurately. Combined with data from the tachometers these values provide the odometry of the robot to estimate change in position over time. We will primarily use the IMU to perform 90 and 180 degree turns.

+

logo

+
+
+

Calibrating the IMU#

+

As described above, there are a number of different sensors that work together to provide the attitude and heading estimates for the Turtlebot3. These sensors are sensitive to magnetic fields which are unique to locale and device. As you will learn in future ECE classes, all electronic devices create small magnetic fields. Even electrons traveling over a wire create magnetic fields. The OpenCR board and IMU are strategically placed in the center of the robot for best attitude and heading performance, however, this location is also in the center of a number of magnetic fields. Luckily for us, the creators of the Turtlebot3 were aware of these issues and whenever you run the serial node to connect to the robot the IMU is calibrated.

+
+
+

Setup#

+

The ICM-20648 is already integrated into the Turtlebot3 robot, therefore, there is no setup required. Whenever the serial node is ran to connect to the OpenCR board, the IMU is initialized and will start publishing data.

+
+
+

Test the IMU#

+

Open a new terminal on the master and run roscore and setup for statistics:

+
roscore
+rosparam set enable_statistics true
+
+
+

Create a secure shell connection to your Robot and launch the Turtlebot3 core launchfile.

+
roslaunch turtlebot3_bringup turtlebot3_core.launch
+
+
+

Open a new terminal on your Master and observe what topics are running.

+

You should note two topics of interest: /imu and /odom.

+

Echo the output of each of the topics and rotate the Robot to see the values change.

+

The /imu topic combines information from the gyroscope and accelerometer to provide orientation, angular velocity, and linear acceleration. The /odom topic combines the information from the /imu topic and tachometers to estimate position, orientation, and linear and angular velocities.

+

Both of these topics provide the orientation of the robot using a quaternion representation. While quaternions can make computation easier, they are not very human readable, so we will convert to Euler angles. To do this we will use a Python library called squaternion.

+

The two main functions we will use from the squaternion library:

+

First we will need to create a Quaternion object:

+
q = Quaternion(w, x, y, z)
+
+
+

Then we will conver that Quaternion to an Euler:

+
e = q.to_euler(degrees=True)
+
+
+

Now we have e, an array representing our Euler angles, e[roll, pitch, yaw].

+

You can keep the node running for the next portion of the lesson.

+
+
+

Write the Subscriber#

+
    +
  1. In a new terminal on the Master, create an ice6 package which depends on the geometry_msgs, rospy, and turtlebot3_bringup packages, compile and source the workspace:

    +
    cd ~/master_ws/src/ece387_master_sp2X-USERNAME/master
    +catkin_create_pkg ice6 std_msgs rospy turtlebot3_bringup
    +cd ~/master_ws
    +catkin_make
    +source ~/.bashrc
    +
    +
    +
  2. +
  3. Create an IMU node:

    +
    roscd ice6/src
    +touch imu_sub.py
    +
    +
    +
  4. +
  5. Copy and complete the below code using a GUI editor tool, such as Sublime or VS Code. Browse to the subscriber you just created and double-click. This will open the file in Sublime or VS Code (if it is open in any other editor, stop, raise your hand, and get help from an instructor)

  6. +
+
+

💡️ Tip: Look for the “TODO” tag which indicates where you should insert your own code.

+
+
#!/usr/bin/env python3
+import rospy
+from squaternion import Quaternion
+# TODO: import message type sent over imu topic
+
+
+class IMU:
+    """Class to read orientation data from Turtlebot3 IMU"""
+    def __init__(self):        
+        # TODO: subscribe to the imu topic that is published by the
+        # Turtlebot3 and provides the robot orientation
+
+
+        # nicely handle shutdown (Ctrl+c)
+        self.ctrl_c = False
+        rospy.on_shutdown(self.shutdownhook)
+
+    # The IMU provides yaw from -180 to 180. This function
+    # converts the yaw (in degrees) to 0 to 360
+    def convert_yaw (self, yaw):
+        return 360 + yaw if yaw < 0 else yaw		
+
+    # Print the current Yaw
+    def callback_imu(self, imu):
+        if not self.ctrl_c:
+            # TODO: create a quaternion using the x, y, z, and w values
+            # from the correct imu message
+            
+            
+            # TODO: convert the quaternion to euler in degrees
+            
+            
+            # TODO: get the yaw component of the euler
+            yaw = 
+
+            # convert yaw from -180 to 180 to 0 to 360
+            yaw = self.convert_yaw(yaw)
+            print("Current heading is %f degrees." % (yaw))
+
+    # clean shutdown
+    def shutdownhook(self):
+        print("Shutting down the IMU subscriber")
+        self.ctrl_c = True
+
+if __name__ == '__main__':
+    rospy.init_node('imu_sub')
+    IMU()
+    rospy.spin()
+
+
+
    +
  1. Save, exit, and make the node executable.

  2. +
  3. Open a new terminal on the Master and run the imu_sub.py node.

  4. +
  5. Rotate the Robot and observe the output.

  6. +
+
+
+

Checkpoint#

+

Once complete, get checked off by an instructor showing the output of your imu_sub and rqt_graph node. Push your ice6 package to your repo for credit

+
+
+

Summary#

+

In this lesson you learned how to utilize the on-board IMU and determine the orientation of the Turtlebot3. In the lab that corresponds to this lesson you will apply this knowledge to turn the robot in 90 and 180 degree turns.ROS

+
+
+

Cleanup#

+

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-lab in. Select ‘y’.

+

Ensure roscore is terminated before moving on to the lab.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module6_IMU/Lab2_IMU.html b/Module6_IMU/Lab2_IMU.html old mode 100755 new mode 100644 index da77b3d..b779deb --- a/Module6_IMU/Lab2_IMU.html +++ b/Module6_IMU/Lab2_IMU.html @@ -1,681 +1,681 @@ - - - - - - - - - - - - Lab 2: Inertial Measurement Unit — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

Lab 2: Inertial Measurement Unit

- -
- -
-
- - - - -
- -
-

Lab 2: Inertial Measurement Unit#

-
-

Purpose#

-

This lab will integrate the on-board IMU with the Turtlebot3 controller to turn the robot 90 degrees left or right. Do not disable any of the mouse controller functionality in the turtlebot_controller.py code. The IMU and mouse control functionality should be able to coexist seemlessly.

-
-
-

Master#

-
-

Setup:#

-

In the /master_ws/src/ece387_master_sp2X-USERNAME/master folder, create a lab2 package which depends on std_msgs, rospy, geometry_msgs, and turtlebot3_bringup.

-
-
-

controller.py#

-
    -
  1. Copy the turtlebot_controller.py file from lab1 into the lab2 package.

  2. -
  3. Open the turtlebot_controller.py file from lab2 using an editor.

  4. -
  5. Import the squaternion library and Imu message used in ICE6.

  6. -
  7. Add the following Class variables within the class above the __init__() function:

    -
      -
    1. K_HDG = 0.1 # rotation controller constant

    2. -
    3. HDG_TOL = 10 # heading tolerance +/- degrees

    4. -
    5. MIN_ANG_Z = 0.5 # limit rad/s values sent to Turtlebot3

    6. -
    7. MAX_ANG_Z = 1.5 # limit rad/s values sent to Turtlebot3

    8. -
    -
  8. -
  9. Add the following to the __init__() function:

    -
      -
    1. Instance variable, self.curr_yaw, initialized to 0 to store the current orientation of the robot

    2. -
    3. Instance variable, self.goal_yaw, initialized to 0 to store the goal orientation of the robot

    4. -
    5. Instance variable, self.turning, initialized to False to store if the robot is currently turning

    6. -
    7. A subscriber to the IMU topic of interest with a callback to the callback_imu() function

    8. -
    -
  10. -
  11. Add the convert_yaw() function from ICE6.

  12. -
  13. Add the callback_imu() function from ICE6, removing print statements and setting the instance variable, self.curr_yaw.

  14. -
  15. Edit the callback_controller() function so it turns the robot 90 degrees in the direction inputed by the user (left or right). Below is some pseudo-code to help you code the controller function

  16. -
-
-

⚠️ WARNING: Pseudo-code is not actual code and cannot be copied and expected to work!

-
-
def callback_controller(self, event):
-    # local variables do not need the self
-	yaw_err = 0
-	ang_z = 0
-    # not turning, so get user input
-    if not turning:
-        read from user and set value to instance variable, self.goal_yaw
-        input("Input l or r to turn 90 deg")
-        
-        # check input and determine goal yaw
-        if input is equal to "l" 
-            set goal yaw to curr yaw plus/minus 90
-            turning equals True
-        else if input is equal to "r"
-           	set goal yaw to curr yaw plus/minus 90
-            turning equals True
-        else 
-        	print error and tell user valid inputs
-            
-        # check bounds
-        if goal_yaw is less than 0 then add 360
-        else if goal_yaw is greater than 360 then subtract 360
-    
-    # turn until goal is reached
-    elif turning:
-        yaw_err = self.goal_yaw - self.curr_yaw
-        
-        # determine if robot should turn clockwise or counterclockwise
-        if yaw_err > 180:
-            yaw_err = yaw_err - 360
-        elif yaw_err < -180:
-            yaw_err = yaw_err + 360
-            
-        # proportional controller that turns the robot until goal 
-        # yaw is reached
-        ang_z = self.K_HDG * yaw_err
-        
-        if ang_z < self.MIN: ang_z = self.MIN		# need to add negative test as well!
-        elif ang_Z > self.MAX: ang_z = self.MAX	# need to add negative test as well!
-        
-        # check goal orientation
-        if abs(yaw_err) < self.HDG_TOL
-            turning equals False
-            ang_z = 0
-            
-   # set twist message and publish
-   self.cmd.linear.x = 0
-   self.cmd.angular.z = ang_z
-   publish message
-
-
-
-
-
-

Run your nodes#

-
    -
  1. On the Master open a terminal and run roscore.

  2. -
  3. Open another terminal and enable statistics for rqt_graph.

  4. -
  5. Run the controller node.

  6. -
  7. Open secure shell into the Robot and run the turtlebot3_core launch file.

  8. -
  9. Type “l” or “r” to turn the robot 90 degrees.

  10. -
-
-
-

Report#

-

Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under Resources/Lab Template.

-
-
-

Turn-in Requirements#

-

[25 points] Demonstration of keyboard control of Turtlebot3 (preferably in person, but can be recorded and posted to Teams under the Lab 2 channel).

-

[50 points] Report via Gradescope.

-

[25 points] Code: push your code to your repository. Also, include a screen shot of the turtlebot_controller.py file at the end of your report.

-
-
- - - - -
- - - - - - -
- - - -
- - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Lab 2: Inertial Measurement Unit — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Lab 2: Inertial Measurement Unit

+ +
+ +
+
+ + + + +
+ +
+

Lab 2: Inertial Measurement Unit#

+
+

Purpose#

+

This lab will integrate the on-board IMU with the Turtlebot3 controller to turn the robot 90 degrees left or right. Do not disable any of the mouse controller functionality in the turtlebot_controller.py code. The IMU and mouse control functionality should be able to coexist seemlessly.

+
+
+

Master#

+
+

Setup:#

+

In the /master_ws/src/ece387_master_sp2X-USERNAME/master folder, create a lab2 package which depends on std_msgs, rospy, geometry_msgs, and turtlebot3_bringup.

+
+
+

controller.py#

+
    +
  1. Copy the turtlebot_controller.py file from lab1 into the lab2 package.

  2. +
  3. Open the turtlebot_controller.py file from lab2 using an editor.

  4. +
  5. Import the squaternion library and Imu message used in ICE6.

  6. +
  7. Add the following Class variables within the class above the __init__() function:

    +
      +
    1. K_HDG = 0.1 # rotation controller constant

    2. +
    3. HDG_TOL = 10 # heading tolerance +/- degrees

    4. +
    5. MIN_ANG_Z = 0.5 # limit rad/s values sent to Turtlebot3

    6. +
    7. MAX_ANG_Z = 1.5 # limit rad/s values sent to Turtlebot3

    8. +
    +
  8. +
  9. Add the following to the __init__() function:

    +
      +
    1. Instance variable, self.curr_yaw, initialized to 0 to store the current orientation of the robot

    2. +
    3. Instance variable, self.goal_yaw, initialized to 0 to store the goal orientation of the robot

    4. +
    5. Instance variable, self.turning, initialized to False to store if the robot is currently turning

    6. +
    7. A subscriber to the IMU topic of interest with a callback to the callback_imu() function

    8. +
    +
  10. +
  11. Add the convert_yaw() function from ICE6.

  12. +
  13. Add the callback_imu() function from ICE6, removing print statements and setting the instance variable, self.curr_yaw.

  14. +
  15. Edit the callback_controller() function so it turns the robot 90 degrees in the direction inputed by the user (left or right). Below is some pseudo-code to help you code the controller function

  16. +
+
+

⚠️ WARNING: Pseudo-code is not actual code and cannot be copied and expected to work!

+
+
def callback_controller(self, event):
+    # local variables do not need the self
+	yaw_err = 0
+	ang_z = 0
+    # not turning, so get user input
+    if not turning:
+        read from user and set value to instance variable, self.goal_yaw
+        input("Input l or r to turn 90 deg")
+        
+        # check input and determine goal yaw
+        if input is equal to "l" 
+            set goal yaw to curr yaw plus/minus 90
+            turning equals True
+        else if input is equal to "r"
+           	set goal yaw to curr yaw plus/minus 90
+            turning equals True
+        else 
+        	print error and tell user valid inputs
+            
+        # check bounds
+        if goal_yaw is less than 0 then add 360
+        else if goal_yaw is greater than 360 then subtract 360
+    
+    # turn until goal is reached
+    elif turning:
+        yaw_err = self.goal_yaw - self.curr_yaw
+        
+        # determine if robot should turn clockwise or counterclockwise
+        if yaw_err > 180:
+            yaw_err = yaw_err - 360
+        elif yaw_err < -180:
+            yaw_err = yaw_err + 360
+            
+        # proportional controller that turns the robot until goal 
+        # yaw is reached
+        ang_z = self.K_HDG * yaw_err
+        
+        if ang_z < self.MIN: ang_z = self.MIN		# need to add negative test as well!
+        elif ang_Z > self.MAX: ang_z = self.MAX	# need to add negative test as well!
+        
+        # check goal orientation
+        if abs(yaw_err) < self.HDG_TOL
+            turning equals False
+            ang_z = 0
+            
+   # set twist message and publish
+   self.cmd.linear.x = 0
+   self.cmd.angular.z = ang_z
+   publish message
+
+
+
+
+
+

Run your nodes#

+
    +
  1. On the Master open a terminal and run roscore.

  2. +
  3. Open another terminal and enable statistics for rqt_graph.

  4. +
  5. Run the controller node.

  6. +
  7. Open secure shell into the Robot and run the turtlebot3_core launch file.

  8. +
  9. Type “l” or “r” to turn the robot 90 degrees.

  10. +
+
+
+

Report#

+

Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under Resources/Lab Template.

+
+
+

Turn-in Requirements#

+

[25 points] Demonstration of keyboard control of Turtlebot3 (preferably in person, but can be recorded and posted to Teams under the Lab 2 channel).

+

[50 points] Report via Gradescope.

+

[25 points] Code: push your code to your repository. Also, include a screen shot of the turtlebot_controller.py file at the end of your report.

+
+
+ + + + +
+ + + + + + +
+ + + +
+ + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module7_LaunchFile/LaunchFile.html b/Module7_LaunchFile/LaunchFile.html old mode 100755 new mode 100644 index e087c54..798648d --- a/Module7_LaunchFile/LaunchFile.html +++ b/Module7_LaunchFile/LaunchFile.html @@ -1,787 +1,787 @@ - - - - - - - - - - - - Launch Files — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Launch Files#

-
-

Purpose#

-

Large applications in robotics typically involve several interconnected ROS nodes, each of which have many parameters. Your current setup is a good example: as you experienced in the IMU lab, you had to open 3 different terminals to run all of the nodes necessary for our system to that point:

-

Robot:

-
    -
  • turtlebot3_core.launch

  • -
-

Master:

-
    -
  • roscore

  • -
  • lab2/turtlebot_controller.py

  • -
-

This problem is only going to get more complex as we add additional functionality to our robot. As it stands right now, every node requires a separate terminal window and the associated command to run it. Using the roslaunch tool, we can eliminate that administrivia of running each node separately. We will create/edit two launch files to bring up the nodes on the master and robot.

-
-
-

roslaunch#

-

The roslaunch tool is used to launch multiple ROS nodes locally and remotely via SSH. We can run nodes that we have created, nodes from pre-built packages, and other launch files. The roslaunch tool takes in one or more XML configuration files (with the .launch extension) that specify the parameters to set and nodes to launch.

-

A launch file is an XML document which specifies:

-
    -
  • which nodes to execute

  • -
  • their parameters

  • -
  • what other files to include

  • -
-

An XML file stands for Extensible Markup Language (XML). This is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable. That isn’t necessarily important for this class, but you can read about XML on Wikipedia if you are interested.

-

We will then use a tool embedded within ROS called roslaunch to easily launch multiple nodes or even other launch files.

-

By convention, we will give our launch files the .launch extension and store them in a launch folder within our package. This isn’t required, but it is the common convention.

-
-
-

Current State#

-

In this section we will first use the conventional technique to bring up all of the nodes required for Lab 2 using the currently understood techniques.

-

Open a new terminal on your Master and start roscore:

-
roscore
-
-
-

Notice, running roscore now monopolized that terminal and you can no longer use it for anything else.

-

Open a new terminal or tab on your Master and run the controller.py node:

-
rosrun lab2 turtlebot_controller.py
-
-
-

At this point we are done with the master. We only needed to bring up two terminals, however, this is still a relatively simple system in teh grand scheme of things.

-
-

📝️ Note: We did need to keep the terminal with the turtlebot_controller.py node open so we can enter commands.

-
-

We can now transition to the robot and bring up the required nodes.

-

Open a new terminal window on the Master and use SSH to create a secure shell into the Robot:

-
ssh pi@robotX
-
-
-

Utilize the SSH instance to start the turtlebot3_core launch file:

-
roslaunch turtlebot3_bringup turtlebot3_core.launch
-
-
-

Overall you needed 3 terminal windows, including one SSH connection, to bring up this relatively simple system of sensors.

-

Kill all nodes and roscore.

-
-
-

roslaunch on the Robot#

-

Navigate to one of the terminals with a secure shell connection to the robot.

-

Open the turtlebot3_core.launch file:

-
rosed turtlebot3_bringup turtlebot3_core.launch
-
-
-

At this time, the turtlebot3_core.launch file includes everything you need to run the robot’s core functionality (driving and reading data from the IMU). Let’s talk about each line of the file:

-

The XML declaration is typically included at the beginning of an XML file to indicate the version of XML being used (in this case, version 1.0). However, when writing ROS (Robot Operating System) launch files in XML format, you generally do not need to include this declaration.

-

ROS launch files typically have a specific structure and do not require the XML version declaration. Instead, a ROS launch file typically starts with the tag. For example:

-
<launch>
-    <!-- Your ROS launch file content goes here -->
-</launch>
-
-
-

In ROS launch files, the tag serves as the root element for defining various nodes, parameters, and other configurations. Therefore, every launch file opens and closes with the launch root element.

-
<launch>
-    <node pkg="rosserial_python" type="serial_node.py" name="turtlebot3_core" output="screen">
-        <param name="port" value="/dev/ttyACM0"/>
-        <param name="baud" value="115200"/>
-        <param name="tf_prefix" value="$(arg multi_robot_name)"/>
-    </node>
-</launch>
-
-
-

The above establishes the node to create a serial connection to the Turtlebot3. We can see a few parameters were set, port and baud, to help in establishing the connection.

-

Close the editor: ctrl+x

-
-
-

roslaunch on the Master#

-

Navigate to your lab2 package on the Master and create a launch directory:

-
roscd lab2
-mkdir launch
-cd launch
-touch lab2.launch
-
-
-

Open the launch file to edit (I recommend using sublime).

-

Now we are going to edit the launch file to bring up all of the nodes (both on the Master locally and remotely on the Robot)

-
-

📝️ Note: roslaunch will look to see if roscore is started. If it is not, it will automatically run roscore.

-
-

Add the following to the lab2.launch file

-
<launch>
-  <!-- Bring up all local nodes first -->
-    
-  <!-- model to visualize the Turtlebot3 in RVIZ -->
-  <include file="$(find turtlebot3_bringup)/launch/turtlebot3_model.launch"/>
-
-  <!-- controller to rotate the robot --> 
-  <node
-    name="controller" pkg="lab2" type="turtlebot_controller.py"
-    output="screen" launch-prefix="xterm -e"
-  />
-
-  <!-- remote nodes -->
-  <machine
-    name="robot0"
-    address="robot0"
-    env-loader="/home/pi/robot_ws/devel/remote_env_loader.sh"
-    default="true"
-    user="pi"
-  />
-
-  <!-- core functionality of the Turtlebot3 -->
-  <node machine="robot0" pkg="rosserial_python" type="serial_node.py" name="turtlebot3_core" output="screen">
-    <param name="port" value="/dev/ttyACM0"/>
-    <param name="baud" value="115200"/>
-  </node>
-
-</launch>
-
-
-
-

📝️ Note: Remember earlier how we reminded you that we need to keep the terminal available for the controller to type commands. By using the two additional parameters screen and launch-prefix, we can ensure the terminal is available for use.

-
-

Save and close the launch file.

-
-
-

Setting up remote connectivity#

-

Before we can run the above launch file we need to accomplish some steps to ensure we can connect remotely to our Robot.

-
    -
  1. Open a terminal on your Master and type the following:

    -
    ssh -oHostKeyAlgorithms='ssh-rsa' pi@robotX
    -
    -
    -
  2. -
  3. Type yes and press Enter.

  4. -
  5. When prompted, type in the password for the pi user on the robot.

  6. -
  7. Type exit.

  8. -
  9. On the Master type the following to create private and public keys:

    -
    ssh-keygen -t rsa
    -
    -
    -
  10. -
  11. Press Enter

  12. -
  13. Press Enter twice more for no passphrase*

  14. -
  15. On the Master send the public key to the Robot

    -
    ssh-copy-id pi@robotX
    -
    -
    -
  16. -
  17. Test that you can now create an SSH connection to the Robot without having to enter a password.

  18. -
  19. In your secure shell to the Robot create a remote ROS environment shell script file:

    -
    cd robot_ws/devel/
    -nano remote_env_loader.sh
    -
    -
    -
  20. -
  21. Copy the following environmental variables into the file:

    -
    #!/bin/bash
    -
    -source /opt/ros/noetic/setup.bash
    -source ~/robot_ws/devel/setup.bash
    -export ROS_PACKAGE_PATH=~/robot_ws/src:/opt/ros/noetic/share
    -export ROS_HOSTNAME=`hostname`
    -export ROS_MASTER_URI=http://masterX:11311
    -export EDITOR='nano -w'
    -export HOSTNAME=`hostname`
    -export TURTLEBOT3_MODEL=burger
    -export LDS_MODEL=LDS-01
    -
    -exec "$@"
    -
    -
    -
    -

    📝️ Note: These are the same environmental variables we have inserted to our .bashrc files on the Master and Robot. When we run nodes remotely on a system the remote_env_loader.sh file is loaded instead of the .bashrc file.

    -
    -
  22. -
  23. Save and exit.

  24. -
  25. Make the file executable:

    -
    chmod +x remote_env_loader.sh
    -
    -
    -
  26. -
-
-
-

Running Launch Files#

-

Browse to a terminal on the Master and make and source your workspace:

-

Utilize the roslaunch utility to execute the lab2 launch file on your Master:

-
roslaunch lab2 lab2.launch
-
-
-

Open a new terminal and list the running nodes. You should see rosout (roscore), controller, joint and robot state publishers, rviz and the remote node, turtlebot3_core.

-

In a separate terminal, bring up rqt_graph. Your output should look similar to this:

-

logo

-
-
-

Checkpoint#

-

Once complete, get checked off by an instructor showing the output of your rqt_graph node.

-
-
-

Summary#

-

There is clearly a lot more we can do with launch files, but this will get you started. You now know how to run nodes, other launch files, and provide parameters to a node using a launch file. I encourage you to visit the ROS tutorials online if you need to do more complex functions with the launch files.

-
-
-

Cleanup#

-

In each terminal window, close the nodes by typing ctrl+c. Exit any SSH connections. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-

Ensure roscore is terminated before moving on to the next lesson.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Launch Files — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Launch Files#

+
+

Purpose#

+

Large applications in robotics typically involve several interconnected ROS nodes, each of which have many parameters. Your current setup is a good example: as you experienced in the IMU lab, you had to open 3 different terminals to run all of the nodes necessary for our system to that point:

+

Robot:

+
    +
  • turtlebot3_core.launch

  • +
+

Master:

+
    +
  • roscore

  • +
  • lab2/turtlebot_controller.py

  • +
+

This problem is only going to get more complex as we add additional functionality to our robot. As it stands right now, every node requires a separate terminal window and the associated command to run it. Using the roslaunch tool, we can eliminate that administrivia of running each node separately. We will create/edit two launch files to bring up the nodes on the master and robot.

+
+
+

roslaunch#

+

The roslaunch tool is used to launch multiple ROS nodes locally and remotely via SSH. We can run nodes that we have created, nodes from pre-built packages, and other launch files. The roslaunch tool takes in one or more XML configuration files (with the .launch extension) that specify the parameters to set and nodes to launch.

+

A launch file is an XML document which specifies:

+
    +
  • which nodes to execute

  • +
  • their parameters

  • +
  • what other files to include

  • +
+

An XML file stands for Extensible Markup Language (XML). This is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable. That isn’t necessarily important for this class, but you can read about XML on Wikipedia if you are interested.

+

We will then use a tool embedded within ROS called roslaunch to easily launch multiple nodes or even other launch files.

+

By convention, we will give our launch files the .launch extension and store them in a launch folder within our package. This isn’t required, but it is the common convention.

+
+
+

Current State#

+

In this section we will first use the conventional technique to bring up all of the nodes required for Lab 2 using the currently understood techniques.

+

Open a new terminal on your Master and start roscore:

+
roscore
+
+
+

Notice, running roscore now monopolized that terminal and you can no longer use it for anything else.

+

Open a new terminal or tab on your Master and run the controller.py node:

+
rosrun lab2 turtlebot_controller.py
+
+
+

At this point we are done with the master. We only needed to bring up two terminals, however, this is still a relatively simple system in teh grand scheme of things.

+
+

📝️ Note: We did need to keep the terminal with the turtlebot_controller.py node open so we can enter commands.

+
+

We can now transition to the robot and bring up the required nodes.

+

Open a new terminal window on the Master and use SSH to create a secure shell into the Robot:

+
ssh pi@robotX
+
+
+

Utilize the SSH instance to start the turtlebot3_core launch file:

+
roslaunch turtlebot3_bringup turtlebot3_core.launch
+
+
+

Overall you needed 3 terminal windows, including one SSH connection, to bring up this relatively simple system of sensors.

+

Kill all nodes and roscore.

+
+
+

roslaunch on the Robot#

+

Navigate to one of the terminals with a secure shell connection to the robot.

+

Open the turtlebot3_core.launch file:

+
rosed turtlebot3_bringup turtlebot3_core.launch
+
+
+

At this time, the turtlebot3_core.launch file includes everything you need to run the robot’s core functionality (driving and reading data from the IMU). Let’s talk about each line of the file:

+

The XML declaration is typically included at the beginning of an XML file to indicate the version of XML being used (in this case, version 1.0). However, when writing ROS (Robot Operating System) launch files in XML format, you generally do not need to include this declaration.

+

ROS launch files typically have a specific structure and do not require the XML version declaration. Instead, a ROS launch file typically starts with the tag. For example:

+
<launch>
+    <!-- Your ROS launch file content goes here -->
+</launch>
+
+
+

In ROS launch files, the tag serves as the root element for defining various nodes, parameters, and other configurations. Therefore, every launch file opens and closes with the launch root element.

+
<launch>
+    <node pkg="rosserial_python" type="serial_node.py" name="turtlebot3_core" output="screen">
+        <param name="port" value="/dev/ttyACM0"/>
+        <param name="baud" value="115200"/>
+        <param name="tf_prefix" value="$(arg multi_robot_name)"/>
+    </node>
+</launch>
+
+
+

The above establishes the node to create a serial connection to the Turtlebot3. We can see a few parameters were set, port and baud, to help in establishing the connection.

+

Close the editor: ctrl+x

+
+
+

roslaunch on the Master#

+

Navigate to your lab2 package on the Master and create a launch directory:

+
roscd lab2
+mkdir launch
+cd launch
+touch lab2.launch
+
+
+

Open the launch file to edit (I recommend using sublime).

+

Now we are going to edit the launch file to bring up all of the nodes (both on the Master locally and remotely on the Robot)

+
+

📝️ Note: roslaunch will look to see if roscore is started. If it is not, it will automatically run roscore.

+
+

Add the following to the lab2.launch file

+
<launch>
+  <!-- Bring up all local nodes first -->
+    
+  <!-- model to visualize the Turtlebot3 in RVIZ -->
+  <include file="$(find turtlebot3_bringup)/launch/turtlebot3_model.launch"/>
+
+  <!-- controller to rotate the robot --> 
+  <node
+    name="controller" pkg="lab2" type="turtlebot_controller.py"
+    output="screen" launch-prefix="xterm -e"
+  />
+
+  <!-- remote nodes -->
+  <machine
+    name="robot0"
+    address="robot0"
+    env-loader="/home/pi/robot_ws/devel/remote_env_loader.sh"
+    default="true"
+    user="pi"
+  />
+
+  <!-- core functionality of the Turtlebot3 -->
+  <node machine="robot0" pkg="rosserial_python" type="serial_node.py" name="turtlebot3_core" output="screen">
+    <param name="port" value="/dev/ttyACM0"/>
+    <param name="baud" value="115200"/>
+  </node>
+
+</launch>
+
+
+
+

📝️ Note: Remember earlier how we reminded you that we need to keep the terminal available for the controller to type commands. By using the two additional parameters screen and launch-prefix, we can ensure the terminal is available for use.

+
+

Save and close the launch file.

+
+
+

Setting up remote connectivity#

+

Before we can run the above launch file we need to accomplish some steps to ensure we can connect remotely to our Robot.

+
    +
  1. Open a terminal on your Master and type the following:

    +
    ssh -oHostKeyAlgorithms='ssh-rsa' pi@robotX
    +
    +
    +
  2. +
  3. Type yes and press Enter.

  4. +
  5. When prompted, type in the password for the pi user on the robot.

  6. +
  7. Type exit.

  8. +
  9. On the Master type the following to create private and public keys:

    +
    ssh-keygen -t rsa
    +
    +
    +
  10. +
  11. Press Enter

  12. +
  13. Press Enter twice more for no passphrase*

  14. +
  15. On the Master send the public key to the Robot

    +
    ssh-copy-id pi@robotX
    +
    +
    +
  16. +
  17. Test that you can now create an SSH connection to the Robot without having to enter a password.

  18. +
  19. In your secure shell to the Robot create a remote ROS environment shell script file:

    +
    cd robot_ws/devel/
    +nano remote_env_loader.sh
    +
    +
    +
  20. +
  21. Copy the following environmental variables into the file:

    +
    #!/bin/bash
    +
    +source /opt/ros/noetic/setup.bash
    +source ~/robot_ws/devel/setup.bash
    +export ROS_PACKAGE_PATH=~/robot_ws/src:/opt/ros/noetic/share
    +export ROS_HOSTNAME=`hostname`
    +export ROS_MASTER_URI=http://masterX:11311
    +export EDITOR='nano -w'
    +export HOSTNAME=`hostname`
    +export TURTLEBOT3_MODEL=burger
    +export LDS_MODEL=LDS-01
    +
    +exec "$@"
    +
    +
    +
    +

    📝️ Note: These are the same environmental variables we have inserted to our .bashrc files on the Master and Robot. When we run nodes remotely on a system the remote_env_loader.sh file is loaded instead of the .bashrc file.

    +
    +
  22. +
  23. Save and exit.

  24. +
  25. Make the file executable:

    +
    chmod +x remote_env_loader.sh
    +
    +
    +
  26. +
+
+
+

Running Launch Files#

+

Browse to a terminal on the Master and make and source your workspace:

+

Utilize the roslaunch utility to execute the lab2 launch file on your Master:

+
roslaunch lab2 lab2.launch
+
+
+

Open a new terminal and list the running nodes. You should see rosout (roscore), controller, joint and robot state publishers, rviz and the remote node, turtlebot3_core.

+

In a separate terminal, bring up rqt_graph. Your output should look similar to this:

+

logo

+
+
+

Checkpoint#

+

Once complete, get checked off by an instructor showing the output of your rqt_graph node.

+
+
+

Summary#

+

There is clearly a lot more we can do with launch files, but this will get you started. You now know how to run nodes, other launch files, and provide parameters to a node using a launch file. I encourage you to visit the ROS tutorials online if you need to do more complex functions with the launch files.

+
+
+

Cleanup#

+

In each terminal window, close the nodes by typing ctrl+c. Exit any SSH connections. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+

Ensure roscore is terminated before moving on to the next lesson.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module8_LIDAR/LIDAR.html b/Module8_LIDAR/LIDAR.html old mode 100755 new mode 100644 index 9d65e28..9e2fd00 --- a/Module8_LIDAR/LIDAR.html +++ b/Module8_LIDAR/LIDAR.html @@ -1,750 +1,751 @@ - - - - - - - - - - - - LIDAR — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

LIDAR#

-
-

Purpose#

-

In this lesson we will enable the robot to avoid obstacles. Many sensors provide obstacle avoidance capabilities: camera, sonar, infrared, LIDAR, etc. All of these will work to enable the robot to avoid obstacles, but we will use LIDAR as it is an affordable, but very capable solution.

-
-
-

LIDAR#

-

Robotis’s LDS-01 is a 360 deg Laser Distance Sensor (LDS). It is based on laser triangulation ranging principles and uses high-speed vision acquisition and processing hardware. It measures distance data in more than 1800 times per second. It has a detection range between .12 m and 3.5 m and an angular resolution of 1 degree. The distance accuracy is .015 m between .12 m and .499 m then +/- 5% up to 3.5 m.

-

logo

-
-

Videos:#

-

Airborne LiDAR

-

Turtlebot3 LDS

-
-
-
-

Quick Check on LIDAR Variant#

-

The robots for our class have two different LIDAR variants. The older bots have the LDS-01 which is exactly what is pictured above. The newer bots likely have the LDS-02 (pictured below). If you have the LDS-02, you will need to go into the .bashrc file and change the last line in the file to indicate the proper variant.

-

logo

-
sudo nano ~/.bashrc
-
-
-

You are looking for a line that looks like this:

-
export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR
-
-
-

You will need to change that line to LDS-02. You will need to accomplish this on both the master and the robot

-
-
-

Setup#

-

The hls_lfcd_lds_driver package enables data to be received from the LIDAR over the /scan topic. The package is pre-installed on your Robot, but as always, trust, but verify. Open a new secure shell into your Robot and run the following:

-
rospack find hls_lfcd_lds_driver
-
-
-

If installed, the command should return the absolute path to the package, similar to:

-
/opt/ros/noetic/share/hls_lfcd_lds_driver
-
-
-

If the command instead returns an error, then you need to install the package.

-
sudo apt install ros-noetic-hls-lfcd-lds-driver
-
-
-
-
-

Testing LIDAR#

-

Open a new terminal on the master and run roscore and setup for statistics:

-
roscore
-rosparam set enable_statistics true
-
-
-

Select the terminal with the secure shell connection to your Robot and open the turtlebot3_lidar.launch file:

-
rosed turtlebot3_bringup turtlebot3_lidar.launch
-
-
-

We can see that this launch file is pretty simple and only launches the hls_laser_publisher node.

-

Run the launch file on the Robot:

-
roslaunch turtlebot3_bringup turtlebot3_lidar.launch
-
-
-

In a new terminal on the Master, we can visualize the Turtlebot3 and LIDAR data using another launch file from the Turtlebot3:

-
roslaunch turtlebot3_bringup turtlebot3_model.launch
-
-
-

This should open an RVIZ window where we can visualize ROS components of our system. In the “Displays” menu on the left you should see two submenus of interest: “LaserScan” and “RobotModel”. These allow us to depict the Turtlebot3 and LIDAR data.

-

You should see red dots fill the rviz map where obstacles exist.

-

Investigate what data the hls_laser_publisher is sending. Type the following and observe the command output:

-
rostopic list
-rostopic info /scan
-rostopic type /scan
-rostopic type /scan | rosmsg show
-rostopic echo /scan
-
-
-

At this point you can kill all nodes on the master, but keep the turtlebot3_lidar launch file running on the Robot.

-
-
-

LIDAR Subscriber#

-

In this section we will build a subscriber that will print the range data from the Turtlebot3 LIDAR.

-
    -
  1. Browse to a terminal on the Master and create an ice8 package:

    -
    cd ~/master_ws/src/ece387_master_sp2X-USERNAME/master
    -catkin_create_pkg ice8 rospy sensor_msgs geometry_msgs turtlebot3_bringup
    -cd ~/master_ws
    -catkin_make
    -source ~/.bashrc
    -
    -
    -
  2. -
  3. Create an lidar node:

    -
    roscd ice8/src
    -touch lidar_sub.py
    -
    -
    -
  4. -
  5. Copy and complete the below code using the GUI editor tool, Atom. Browse to the subscriber you just created and double-click. This will open the file in Atom (if it is open in any other editor, stop, raise your hand, and get help from an instructor)

  6. -
-
-

💡️ Tip: Look for the “TODO” tag which indicates where you should insert your own code.

-
-

The code should obtain the list of range data from the LIDAR launch file running on the robot, convert the angles from 0 to 180 degrees and 0 to -180 degrees to 0 to 360 degrees. Lastly, the subscriber will print the average distance of obstacles 30 degrees off the nose of the robot.

-
#!/usr/bin/env python3
-import rospy, math
-# TODO: import correct message
-
-
-# lambda function to convert rad to deg
-RAD2DEG = lambda x: ((x)*180./math.pi)
-# convert LaserScan degree from -180 - 180 degs to 0 - 360 degs
-DEG_CONV = lambda deg: deg + 360 if deg < 0 else deg
-
-class LIDAR:    
-    """Class to read lidar data from the Turtlebot3 LIDAR"""
-    def __init__(self):
-        # TODO: create a subscriber to the scan topic published by the lidar launch file
-
-        
-        self.ctrl_c = False
-        rospy.on_shutdown(self.shutdownhook)
-        
-    def callback_lidar(self, scan):
-    	if not self.ctrl_c:
-	    	degrees = []
-	    	ranges = []
-	    	
-	    	# determine how many scans were taken during rotation
-	        count = int(scan.scan_time / scan.time_increment)
-	        
-	        for i in range(count):
-	            # using min angle and incr data determine curr angle, 
-	            # convert to degrees, convert to 360 scale
-	            degrees.append(int(DEG_CONV(RAD2DEG(scan.angle_min + scan.angle_increment*i))))
-	            rng = scan.ranges[i]
-	            
-	            # ensure range values are valid; set to 0 if not
-	            if rng < scan.range_min or rng > scan.range_max:
-	                ranges.append(0.0)
-	            else:
-	            	ranges.append(rng)
-	        
-	        # python way to iterate two lists at once!
-	        for deg, rng in zip(degrees, ranges):
-	        	# TODO: sum and count the ranges 30 degrees off the nose of the robot
-                
-                
-            # TODO: ensure you don't divide by 0 and print average off the nose
-	        	
-            
-	def shutdownhook(self):
-		print("Shutting down lidar subscriber")
-		self.ctrl_c = True
-        
-if __name__ == '__main__':
-    rospy.init_node('lidar_sub')
-    LIDAR()
-    rospy.spin()
-
-
-
    -
  1. Save, exit, and make the node executable.

  2. -
  3. Open a new terminal on the Master and run the lidar_sub.py node.

  4. -
  5. Rotate the Robot and observe the distance off the nose.

  6. -
-
-
-

Checkpoint#

-

Once complete, get checked off by an instructor showing the output of your lidar_sub and rqt_graph node.

-
-
-

Summary#

-

In this lesson you learned how to integrate the LIDAR and get the distance of objects off the nose of the robot using the pre-built LIDAR package. In the lab that corresponds to this lesson you will apply this knowledge to stop the robot a specified distance from an obstacle and turn.

-
-
-

Cleanup#

-

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-

Ensure roscore is terminated before moving on to the lab.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + LIDAR — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

LIDAR#

+
+

Purpose#

+

In this lesson we will enable the robot to avoid obstacles. Many sensors provide obstacle avoidance capabilities: camera, sonar, infrared, LIDAR, etc. All of these will work to enable the robot to avoid obstacles, but we will use LIDAR as it is an affordable, but very capable solution.

+
+
+

LIDAR#

+

Robotis’s LDS-01 is a 360 deg Laser Distance Sensor (LDS). It is based on laser triangulation ranging principles and uses high-speed vision acquisition and processing hardware. It measures distance data in more than 1800 times per second. It has a detection range between .12 m and 3.5 m and an angular resolution of 1 degree. The distance accuracy is .015 m between .12 m and .499 m then +/- 5% up to 3.5 m.

+

logo

+
+

Videos:#

+

Airborne LiDAR

+

Turtlebot3 LDS

+
+
+
+

Quick Check on LIDAR Variant#

+

The robots for our class have two different LIDAR variants. The older bots have the LDS-01 which is exactly what is pictured above. The newer bots likely have the LDS-02 (pictured below). If you have the LDS-02, you will need to go into the .bashrc file and change the last line in the file to indicate the proper variant.

+

logo

+
sudo nano ~/.bashrc
+
+
+

You are looking for a line that looks like this:

+
export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR
+
+
+

You will need to change that line to LDS-02. You will need to accomplish this on both the master and the robot

+
+
+

Setup#

+

The hls_lfcd_lds_driver package enables data to be received from the LIDAR over the /scan topic. The package is pre-installed on your Robot, but as always, trust, but verify. Open a new secure shell into your Robot and run the following:

+
rospack find hls_lfcd_lds_driver
+
+
+

If installed, the command should return the absolute path to the package, similar to:

+
/opt/ros/noetic/share/hls_lfcd_lds_driver
+
+
+

If the command instead returns an error, then you need to install the package.

+
sudo apt install ros-noetic-hls-lfcd-lds-driver
+
+
+
+
+

Testing LIDAR#

+

Open a new terminal on the master and run roscore and setup for statistics:

+
roscore
+rosparam set enable_statistics true
+
+
+

Select the terminal with the secure shell connection to your Robot and open the turtlebot3_lidar.launch file:

+
rosed turtlebot3_bringup turtlebot3_lidar.launch
+
+
+

We can see that this launch file is pretty simple and only launches the hls_laser_publisher node.

+

Run the launch file on the Robot:

+
roslaunch turtlebot3_bringup turtlebot3_lidar.launch
+
+
+

In a new terminal on the Master, we can visualize the Turtlebot3 and LIDAR data using another launch file from the Turtlebot3:

+
roslaunch turtlebot3_bringup turtlebot3_model.launch
+
+
+

This should open an RVIZ window where we can visualize ROS components of our system. In the “Displays” menu on the left you should see two submenus of interest: “LaserScan” and “RobotModel”. These allow us to depict the Turtlebot3 and LIDAR data.

+

You should see red dots fill the rviz map where obstacles exist as shown below.

+

logo

+

Investigate what data the hls_laser_publisher is sending. Type the following and observe the command output:

+
rostopic list
+rostopic info /scan
+rostopic type /scan
+rostopic type /scan | rosmsg show
+rostopic echo /scan
+
+
+

At this point you can kill all nodes on the master, but keep the turtlebot3_lidar launch file running on the Robot.

+
+
+

LIDAR Subscriber#

+

In this section we will build a subscriber that will print the range data from the Turtlebot3 LIDAR.

+
    +
  1. Browse to a terminal on the Master and create an ice8 package:

    +
    cd ~/master_ws/src/ece387_master_sp2X-USERNAME/master
    +catkin_create_pkg ice8 rospy sensor_msgs geometry_msgs turtlebot3_bringup
    +cd ~/master_ws
    +catkin_make
    +source ~/.bashrc
    +
    +
    +
  2. +
  3. Create an lidar node:

    +
    roscd ice8/src
    +touch lidar_sub.py
    +
    +
    +
  4. +
  5. Copy and complete the below code using the GUI editor tool, Atom. Browse to the subscriber you just created and double-click. This will open the file in Atom (if it is open in any other editor, stop, raise your hand, and get help from an instructor)

  6. +
+
+

💡️ Tip: Look for the “TODO” tag which indicates where you should insert your own code.

+
+

The code should obtain the list of range data from the LIDAR launch file running on the robot, convert the angles from 0 to 180 degrees and 0 to -180 degrees to 0 to 360 degrees. Lastly, the subscriber will print the average distance of obstacles 30 degrees off the nose of the robot.

+
#!/usr/bin/env python3
+import rospy, math
+# TODO: import correct message
+
+
+# lambda function to convert rad to deg
+RAD2DEG = lambda x: ((x)*180./math.pi)
+# convert LaserScan degree from -180 - 180 degs to 0 - 360 degs
+DEG_CONV = lambda deg: deg + 360 if deg < 0 else deg
+
+class LIDAR:    
+    """Class to read lidar data from the Turtlebot3 LIDAR"""
+    def __init__(self):
+        # TODO: create a subscriber to the scan topic published by the lidar launch file
+
+        
+        self.ctrl_c = False
+        rospy.on_shutdown(self.shutdownhook)
+        
+    def callback_lidar(self, scan):
+    	if not self.ctrl_c:
+	    	degrees = []
+	    	ranges = []
+	    	
+	    	# determine how many scans were taken during rotation
+	        count = int(scan.scan_time / scan.time_increment)
+	        
+	        for i in range(count):
+	            # using min angle and incr data determine curr angle, 
+	            # convert to degrees, convert to 360 scale
+	            degrees.append(int(DEG_CONV(RAD2DEG(scan.angle_min + scan.angle_increment*i))))
+	            rng = scan.ranges[i]
+	            
+	            # ensure range values are valid; set to 0 if not
+	            if rng < scan.range_min or rng > scan.range_max:
+	                ranges.append(0.0)
+	            else:
+	            	ranges.append(rng)
+	        
+	        # python way to iterate two lists at once!
+	        for deg, rng in zip(degrees, ranges):
+	        	# TODO: sum and count the ranges 30 degrees off the nose of the robot
+                
+                
+            # TODO: ensure you don't divide by 0 and print average off the nose
+	        	
+            
+	def shutdownhook(self):
+		print("Shutting down lidar subscriber")
+		self.ctrl_c = True
+        
+if __name__ == '__main__':
+    rospy.init_node('lidar_sub')
+    LIDAR()
+    rospy.spin()
+
+
+
    +
  1. Save, exit, and make the node executable.

  2. +
  3. Open a new terminal on the Master and run the lidar_sub.py node.

  4. +
  5. Rotate the Robot and observe the distance off the nose.

  6. +
+
+
+

Checkpoint#

+

Once complete, get checked off by an instructor showing the output of your lidar_sub and rqt_graph node.

+
+
+

Summary#

+

In this lesson you learned how to integrate the LIDAR and get the distance of objects off the nose of the robot using the pre-built LIDAR package. In the lab that corresponds to this lesson you will apply this knowledge to stop the robot a specified distance from an obstacle and turn.

+
+
+

Cleanup#

+

In each terminal window, close the node by typing ctrl+c. Exit any SSH connections. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+

Ensure roscore is terminated before moving on to the lab.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module8_LIDAR/Lab3_LIDAR.html b/Module8_LIDAR/Lab3_LIDAR.html old mode 100755 new mode 100644 index a860ce0..006dcf2 --- a/Module8_LIDAR/Lab3_LIDAR.html +++ b/Module8_LIDAR/Lab3_LIDAR.html @@ -1,651 +1,651 @@ - - - - - - - - - - - - Lab 3: LIDAR — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

Lab 3: LIDAR

- -
- -
-
- - - - -
- -
-

Lab 3: LIDAR#

-
-

Purpose#

-

This lab will integrate the Turtlebot3 LIDAR with the existing controller to drive the robot forward and turn 90 degrees when there is an obstacle.

-
-
-

Master#

-
-

Setup:#

-

In the /master_ws/src/ece387_master_spring202X-USERNAME/ folder, create a lab3 package which depends on rospy, std_msgs, geometry_msgs, and turtlebot3_bringup.

-

Make and source your workspace.

-
-
-

controller.py#

-
    -
  1. Copy the controller.py file from lab2 into the lab3 package.

  2. -
  3. Open the controller.py file from lab3 using the Atom editor.

  4. -
  5. Import the laser message used in ICE8.

  6. -
  7. Copy the 2 lambda functions from ICE8 (RAD2DEG & DEG_CONV).

  8. -
  9. Add the following Class variables within the class above the __init__() function:

    -
      -
    1. DISTANCE = 0.4 # distance from the wall to stop

    2. -
    3. K_POS = 100 # proportional constant for slowly stopping as you get closer to the wall

    4. -
    5. MIN_LIN_X = 0.05 # limit m/s values sent to Turtlebot3

    6. -
    7. MAX_LIN_X = 0.2 # limit m/s values sent to Turtlebot3

    8. -
    -
  10. -
  11. Add the following to the __init__() function:

    -
      -
    1. Instance variable, self.avg_dist, initialized to 0 to store the average dist off the nose.

    2. -
    3. Instance variable, self.got_avg, initialized to False to store when an average is calculated.

    4. -
    5. A subscriber to the LIDAR topic of interest with a callback to the callback_lidar() function.

    6. -
    -
  12. -
  13. Add the callback_lidar() function from ICE8, removing print statements and setting the instance variables, self.avg_dist and self.got_avg.

  14. -
  15. Edit the callback_controller() to accomplish the following:

    -
      -
    1. Remove user input.

    2. -
    3. When not turning and you have an average LIDAR reading, calculate the distance error (actual dist - desired dist) and use that to drive your robot straight at a proportional rate (very similar to how we calculated the turn rate in lab 2).

    4. -
    5. Limit the linear speed of the robot to MIN_LIN_X and MAX_LIN_X.

    6. -
    7. If within DISTANCE of a wall, then stop and start turning (left or right, you decide).

    8. -
    -
    -

    💡️ Tip: You should be able to reuse a lot of code for this step!

    -
    -
      -
    1. Save the linear x and angular z values to the Twist message and publish.

    2. -
    -
  16. -
  17. Save, exit, and make executable if necessary.

  18. -
-
-
-
-

Create a launch file#

-
    -
  1. Create a launch directory in your lab3 folder.

  2. -
  3. Copy the launch file from lab2 to lab3.

  4. -
  5. Open the turtlebot3_lidar.launch file from the turtlebot3_bringup package and copy the arguments and nodes to your lab3 launch file.

  6. -
  7. Add the machine tag to the lidar node.

  8. -
-
-
-

Run your nodes#

-
    -
  1. On the Master, open a terminal and run the lab3.launch file

  2. -
-
-
-

Report#

-

Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under Resources/Lab Template.

-
-

📝️ NOTE: We will be primarily grading sections 3.3 System level design and 3.4 Testing for this lab, but do include the entire lab as you will need other components for the final project report.

-
-
-
-

Turn-in Requirements#

-

[25 points] Demonstration of Turtlebot3 driving and not hitting a wall (preferably in person, but can be recorded and posted to Teams under the Lab3 channel).

-

[50 points] Report via Gradescope.

-

[25 points] Code: push your code to your repository. Also, include a screen shot of the controller.py file at the end of your report.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Lab 3: LIDAR — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Lab 3: LIDAR

+ +
+ +
+
+ + + + +
+ +
+

Lab 3: LIDAR#

+
+

Purpose#

+

This lab will integrate the Turtlebot3 LIDAR with the existing controller to drive the robot forward and turn 90 degrees when there is an obstacle.

+
+
+

Master#

+
+

Setup:#

+

In the /master_ws/src/ece387_master_spring202X-USERNAME/ folder, create a lab3 package which depends on rospy, std_msgs, sensor_msgs, geometry_msgs, and turtlebot3_bringup.

+

Make and source your workspace.

+
+
+

controller.py#

+
    +
  1. Copy the controller.py file from lab2 into the lab3 package.

  2. +
  3. Open the controller.py file from lab3 using the Atom editor.

  4. +
  5. Import the laser message used in ICE8.

  6. +
  7. Copy the 2 lambda functions from ICE8 (RAD2DEG & DEG_CONV).

  8. +
  9. Add the following Class variables within the class above the __init__() function:

    +
      +
    1. DISTANCE = 0.4 # distance from the wall to stop

    2. +
    3. K_POS = 100 # proportional constant for slowly stopping as you get closer to the wall

    4. +
    5. MIN_LIN_X = 0.05 # limit m/s values sent to Turtlebot3

    6. +
    7. MAX_LIN_X = 0.2 # limit m/s values sent to Turtlebot3

    8. +
    +
  10. +
  11. Add the following to the __init__() function:

    +
      +
    1. Instance variable, self.avg_dist, initialized to 0 to store the average dist off the nose.

    2. +
    3. Instance variable, self.got_avg, initialized to False to store when an average is calculated.

    4. +
    5. A subscriber to the LIDAR topic of interest with a callback to the callback_lidar() function.

    6. +
    +
  12. +
  13. Add the callback_lidar() function from ICE8, removing print statements and setting the instance variables, self.avg_dist and self.got_avg.

  14. +
  15. Edit the callback_controller() to accomplish the following:

    +
      +
    1. Remove user input.

    2. +
    3. When not turning and you have an average LIDAR reading, calculate the distance error (actual dist - desired dist) and use that to drive your robot straight at a proportional rate (very similar to how we calculated the turn rate in lab 2).

    4. +
    5. Limit the linear speed of the robot to MIN_LIN_X and MAX_LIN_X.

    6. +
    7. If within DISTANCE of a wall, then stop and start turning (left or right, you decide).

    8. +
    +
    +

    💡️ Tip: You should be able to reuse a lot of code for this step!

    +
    +
      +
    1. Save the linear x and angular z values to the Twist message and publish.

    2. +
    +
  16. +
  17. Save, exit, and make executable if necessary.

  18. +
+
+
+
+

Create a launch file#

+
    +
  1. Create a launch directory in your lab3 folder.

  2. +
  3. Copy the launch file from lab2 to lab3.

  4. +
  5. Open the turtlebot3_lidar.launch file from the turtlebot3_bringup package and copy the arguments and nodes to your lab3 launch file.

  6. +
  7. Add the machine tag to the lidar node.

  8. +
+
+
+

Run your nodes#

+
    +
  1. On the Master, open a terminal and run the lab3.launch file

  2. +
+
+
+

Report#

+

Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under Resources/Lab Template.

+
+

📝️ NOTE: We will be primarily grading sections 3.3 System level design and 3.4 Testing for this lab, but do include the entire lab as you will need other components for the final project report.

+
+
+
+

Turn-in Requirements#

+

[25 points] Demonstration of Turtlebot3 driving and not hitting a wall (preferably in person, but can be recorded and posted to Teams under the Lab3 channel).

+

[50 points] Report via Gradescope.

+

[25 points] Code: push your code to your repository. Also, include a screen shot of the controller.py file at the end of your report.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module9_CV/Lab4_ComputerVision.html b/Module9_CV/Lab4_ComputerVision.html old mode 100755 new mode 100644 index 84dd120..6736b7a --- a/Module9_CV/Lab4_ComputerVision.html +++ b/Module9_CV/Lab4_ComputerVision.html @@ -1,811 +1,811 @@ - - - - - - - - - - - - Lab 4: Computer Vision — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Lab 4: Computer Vision#

-
-

Purpose#

-

This lab will integrate a USB Camera with the Robot. You will use a Python script to take pictures of the stop sign and build a stop sign detector then test it using a live video feed. You will then use the detector and known size of the stop sign to estimate how far the stop sign is from the camera. Lastly, you will create a node to identify and determine how far an April Tag is from the robot.

-
-
-

Setup packages#

-

Open a terminal on the Master and create a lab4 package:

-
cd ~/master_ws/src/ece387_master_spring202X-USERNAME/
-catkin_create_pkg lab4 rospy sensor_msgs std_msgs cv_bridge apriltag_ros
-
-
-

Make and source your workspace.

-

If you have not already done so, repeat on the Robot

-
-
-

Create a ROS node to save images#

-

Browse to your lab4 source folder on the Master and create a node called image_capture.py.

-
#!/usr/bin/env python3
-import rospy, cv2, argparse
-from sensor_msgs.msg import Image
-from cv_bridge import CvBridge, CvBridgeError
-
-class SavingImage(object):
-	def __init__(self, img_dest):
-		self.img_dest = img_dest
-		self.ctrl_c = False
-		self.count = 0
-        
-        # subscribe to the topic created by the usb_cam node
-		self.image_sub = rospy.Subscriber("/usb_cam/image_raw",Image,self.camera_callback)
-        
-        # CV bridge converts between ROS Image messages and OpenCV images
-		self.bridge_object = CvBridge()
-        
-        # callback to save images when user presses button
-		rospy.Timer(rospy.Duration(.1), self.callback_save)
-		
-		rospy.on_shutdown(self.shutdownhook)
-
-	def camera_callback(self,img):
-		if not self.ctrl_c:
-			try:
-                # convert ROS image to OpenCV image
-				self.cv_image = self.bridge_object.imgmsg_to_cv2(img, desired_encoding="bgr8")
-			except CvBridgeError as e:
-				print(e)
-			
-            # show the image (waitKey(1) allows for automatic refressh creating video)
-			cv2.imshow('image', self.cv_image)
-			cv2.waitKey(1)
-		
-	def callback_save(self, event):
-        # when user is ready to take picture press button
-		_ = input("Press enter to save the next image.")
-		dest = self.img_dest + "img" + str(self.count) + ".jpg"
-		self.count += 1
-		print(dest)
-		try:
-            # write to file
-			cv2.imwrite(dest, self.cv_image)
-		except:
-			print("Not valid image name. Try restarting with valid path.")
-			
-	def shutdownhook(self):
-		print("Shutting down")
-		cv2.destroyAllWindows()
-
-if __name__ == '__main__':
-	rospy.init_node('image_saver', anonymous=True)
-	ap = argparse.ArgumentParser()
-	ap.add_argument("-o", "--output", required=True, help="path to output img")
-	args = vars(ap.parse_args())
-	saving_image_object = SavingImage(args["output"])
-	try:
-		rospy.spin()
-	except KeyboardInterrupt:
-		pass
-
-
-

Save, exit, and make executable.

-
-
-

Train your stop detector#

-

Create a new folder in your lab4 package called training_images.

-

Run the image_capture.py node on the Master using the following command:

-
rosrun lab4 image_capture.py -o /home/dfec/master_ws/src/ece387_master_spring202X-NAME/lab4/training_images/
-
-
-

Store images of the stop sign by pressing enter when prompted. You decide how many and at what orientations to properly train your detector. When complete, hit ctrl+c to exit.

-

Utilize the steps from Module 9: Building a detector using HOG features to label your images and train your object detector using the new images, saving the stop_detector.svm file within the training_images folder.

-
-
-

Test your stop detector#

-

Create a node in the lab4 package on the Master called stop_detector.py and copy the below into it:

-
#!/usr/bin/env python3
-import rospy, cv2, dlib
-from cv_bridge import CvBridge, CvBridgeError
-
-# TODO: import usb_cam message type
-
-
-class StopDetector(object):
-
-    def __init__(self, detectorLoc):
-        self.ctrl_c = False
-        
-        #TODO: create subscriber to usb_cam image topic
-
-        
-        self.bridge_object = CvBridge()
-        self.detector = dlib.simple_object_detector(detectorLoc)
-        
-        rospy.on_shutdown(self.shutdownhook)
-        
-    def camera_callback(self,data):
-        if not self.ctrl_c:
-            #TODO: write code to get ROS image, convert to OpenCV image,
-            # apply detector, add boxes to image, and display image
-            
-
-    def shutdownhook(self):
-        print("Shutting down")
-        self.ctrl_c = True
-        cv2.destroyAllWindows()
-        
-if __name__ == '__main__':
-    rospy.init_node('stop_detector')
-    detector = rospy.get_param("/stop_detector/detector")
-    stop_detector = StopDetector(detector)
-    try:
-        rospy.spin()
-    except KeyboardInterrupt:
-        pass
-
-
-

Edit the stop_detector.py node so it utilizes the camera_callback() function we used above to get images from the camera.

-

After getting the cv_image within the camera_callback(), apply the detector in a similar method as Module 9: Testing a detector creating boxes around all detected stop signs. Using a waitKey(1) will allow for the image to refresh automatically without user input and display the video.

-
-
-

Checkpoint 1#

-

Demonstrate the stop detector on the Master detecting a stop sign from the Robot’s camera.

-
rosrun lab4 stop_detector.py _detector:=/home/dfec/master_ws/src/ece387_master_spring202X-NAME/lab4/training_images/stop_detector.svm
-
-
-
-

📝️ Note: You must have the lab4.launch file running.

-
-
-
-

Move detector to robot#

-

Copy the detector and node to the robot:

-
roscd lab4/training_images
-scp stop_detector.svm pi@robotX:/home/pi/robot_ws/src/ece387_robot_spring202X-NAME/lab4/training_images/stop_detector.svm
-roscd lab4/src
-scp stop_detector.py pi@robotX:/home/pi/robot_ws/src/ece387_robot_spring202X-NAME/lab4/src/stop_detector.py
-
-
-

Remove the lines that display the video and instead print “Stop detected” if boxes is not empty.

-

Do you note a difference in processing speed?

-
-
-

Launch file#

-

Edit the lab4.launch file so it will run the stop detector node with the detector param set to the location of the detector. For example:

-
<node machine="robotX" name="stop_detector" pkg="lab4" type="stop_detector.py" output="screen">
-    <param name="detector" value="/home/pi/robot_ws/src/ece387_robot_spring202X-Name/robot/lab4/training_images/sl_detector.svm"/>
-</node>
-
-
-
-
-

Checkpoint 2#

-

Demonstrate the stop detector on the Robot detecting a stop sign.

-
-
-

Determine distance from stop sign#

-
-

Edit stop_detector.py#

-

You will edit your stop sign detector on the Robot to calculate an estimated distance between the camera and the stop sign using triangle similarity.

-

Given a stop sign with a known width, \(W\), we can place the stop sign at a known distance, \(D\), from our camera. The detector will then detect the stop sign and provide a perceived width in pixels, \(P\). Using these values we can calculate the focal length, \(F\) of our camera:

-

\(F = \frac{(P\times D)}{W}\)

-

We can then use the calculated focal length, \(F\), known width, \(W\), and perceived width in pixels, \(P\) to calculate the distance from the camera:

-

\(D' = \frac{(W\times F)}{P}\)

-

Use the above information and create two class variables, FOCAL and STOP_WIDTH, and a class function to calculate distance given a known FOCAL length and a known width of the stop sign, STOP_WIDTH. You will need to print the perceived width of the stop sign to determine the \(P\) value used in the calculation to find the focal length.

-
-

💡️ Tip: Pay attention to what the x and w variables of the box actually represent!

-
-

Create a new publisher that will publish the distance using Float32 std_msgs messages over the /stop_dist topic.

-

Publish the distance of each object seen in the image.

-

Remove any print statements after troubleshooting!

-
-
-
-

Checkpoint 3#

-

Demonstrate the stop_detector node publishing distance from the stop sign.

-
-
-

Printing April Tag information#

-

Create a node on the master in lab4 called apriltag_dist.py. Import the appropriate AprilTag message. Subscribe to the tag_detections topic. Print the identified AprilTag ID and distance. If the camera sees multiple tags, it should print the information for each tag.

-

In your callback function you will want to create a for loop such as:

-
for tag in data.detections:
-
-
-

Use print statements to determine the characteristics of the message (you can also google the message).

-

Add the apriltag_dist node to the lab4 launch file.

-
-
-

Checkpoint 4#

-

Demonstrate the apriltag_dist node printing the ID and distance of each April Tag.

-
-
-

Report#

-

Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under Resources/Lab Template.

-
-

📝️ Note: We will be primarily grading sections 3.1, 3.2, and 3.3 for this lab, but do include the entire lab as you will need other components for the final project report.

-
-
-
-

Turn-in Requirements#

-

[25 points] All checkpoints marked off.

-

[50 points] Report via Gradescope.

-

[25 points] Code: push your code to your repository. Also, include a screen shot of the apriltag_dist.py and stop_detector.py files at the end of your report.

-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Lab 4: Computer Vision — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Lab 4: Computer Vision#

+
+

Purpose#

+

This lab will integrate a USB Camera with the Robot. You will use a Python script to take pictures of the stop sign and build a stop sign detector then test it using a live video feed. You will then use the detector and known size of the stop sign to estimate how far the stop sign is from the camera. Lastly, you will create a node to identify and determine how far an April Tag is from the robot.

+
+
+

Setup packages#

+

Open a terminal on the Master and create a lab4 package:

+
cd ~/master_ws/src/ece387_master_spring202X-USERNAME/
+catkin_create_pkg lab4 rospy sensor_msgs std_msgs cv_bridge apriltag_ros
+
+
+

Make and source your workspace.

+

If you have not already done so, repeat on the Robot

+
+
+

Create a ROS node to save images#

+

Browse to your lab4 source folder on the Master and create a node called image_capture.py.

+
#!/usr/bin/env python3
+import rospy, cv2, argparse
+from sensor_msgs.msg import Image
+from cv_bridge import CvBridge, CvBridgeError
+
+class SavingImage(object):
+	def __init__(self, img_dest):
+		self.img_dest = img_dest
+		self.ctrl_c = False
+		self.count = 0
+        
+        # subscribe to the topic created by the usb_cam node
+		self.image_sub = rospy.Subscriber("/usb_cam/image_raw",Image,self.camera_callback)
+        
+        # CV bridge converts between ROS Image messages and OpenCV images
+		self.bridge_object = CvBridge()
+        
+        # callback to save images when user presses button
+		rospy.Timer(rospy.Duration(.1), self.callback_save)
+		
+		rospy.on_shutdown(self.shutdownhook)
+
+	def camera_callback(self,img):
+		if not self.ctrl_c:
+			try:
+                # convert ROS image to OpenCV image
+				self.cv_image = self.bridge_object.imgmsg_to_cv2(img, desired_encoding="bgr8")
+			except CvBridgeError as e:
+				print(e)
+			
+            # show the image (waitKey(1) allows for automatic refressh creating video)
+			cv2.imshow('image', self.cv_image)
+			cv2.waitKey(1)
+		
+	def callback_save(self, event):
+        # when user is ready to take picture press button
+		_ = input("Press enter to save the next image.")
+		dest = self.img_dest + "img" + str(self.count) + ".jpg"
+		self.count += 1
+		print(dest)
+		try:
+            # write to file
+			cv2.imwrite(dest, self.cv_image)
+		except:
+			print("Not valid image name. Try restarting with valid path.")
+			
+	def shutdownhook(self):
+		print("Shutting down")
+		cv2.destroyAllWindows()
+
+if __name__ == '__main__':
+	rospy.init_node('image_saver', anonymous=True)
+	ap = argparse.ArgumentParser()
+	ap.add_argument("-o", "--output", required=True, help="path to output img")
+	args = vars(ap.parse_args())
+	saving_image_object = SavingImage(args["output"])
+	try:
+		rospy.spin()
+	except KeyboardInterrupt:
+		pass
+
+
+

Save, exit, and make executable.

+
+
+

Train your stop detector#

+

Create a new folder in your lab4 package called training_images.

+

Run the image_capture.py node on the Master using the following command:

+
rosrun lab4 image_capture.py -o /home/dfec/master_ws/src/ece387_master_spring202X-NAME/lab4/training_images/
+
+
+

Store images of the stop sign by pressing enter when prompted. You decide how many and at what orientations to properly train your detector. When complete, hit ctrl+c to exit.

+

Utilize the steps from Module 9: Building a detector using HOG features to label your images and train your object detector using the new images, saving the stop_detector.svm file within the training_images folder.

+
+
+

Test your stop detector#

+

Create a node in the lab4 package on the Master called stop_detector.py and copy the below into it:

+
#!/usr/bin/env python3
+import rospy, cv2, dlib
+from cv_bridge import CvBridge, CvBridgeError
+
+# TODO: import usb_cam message type
+
+
+class StopDetector(object):
+
+    def __init__(self, detectorLoc):
+        self.ctrl_c = False
+        
+        #TODO: create subscriber to usb_cam image topic
+
+        
+        self.bridge_object = CvBridge()
+        self.detector = dlib.simple_object_detector(detectorLoc)
+        
+        rospy.on_shutdown(self.shutdownhook)
+        
+    def camera_callback(self,data):
+        if not self.ctrl_c:
+            #TODO: write code to get ROS image, convert to OpenCV image,
+            # apply detector, add boxes to image, and display image
+            
+
+    def shutdownhook(self):
+        print("Shutting down")
+        self.ctrl_c = True
+        cv2.destroyAllWindows()
+        
+if __name__ == '__main__':
+    rospy.init_node('stop_detector')
+    detector = rospy.get_param("/stop_detector/detector")
+    stop_detector = StopDetector(detector)
+    try:
+        rospy.spin()
+    except KeyboardInterrupt:
+        pass
+
+
+

Edit the stop_detector.py node so it utilizes the camera_callback() function we used above to get images from the camera.

+

After getting the cv_image within the camera_callback(), apply the detector in a similar method as Module 9: Testing a detector creating boxes around all detected stop signs. Using a waitKey(1) will allow for the image to refresh automatically without user input and display the video.

+
+
+

Checkpoint 1#

+

Demonstrate the stop detector on the Master detecting a stop sign from the Robot’s camera.

+
rosrun lab4 stop_detector.py _detector:=/home/dfec/master_ws/src/ece387_master_spring202X-NAME/lab4/training_images/stop_detector.svm
+
+
+
+

📝️ Note: You must have the lab4.launch file running.

+
+
+
+

Move detector to robot#

+

Copy the detector and node to the robot:

+
roscd lab4/training_images
+scp stop_detector.svm pi@robotX:/home/pi/robot_ws/src/ece387_robot_spring202X-NAME/lab4/training_images/stop_detector.svm
+roscd lab4/src
+scp stop_detector.py pi@robotX:/home/pi/robot_ws/src/ece387_robot_spring202X-NAME/lab4/src/stop_detector.py
+
+
+

Remove the lines that display the video and instead print “Stop detected” if boxes is not empty.

+

Do you note a difference in processing speed?

+
+
+

Launch file#

+

Edit the lab4.launch file so it will run the stop detector node with the detector param set to the location of the detector. For example:

+
<node machine="robotX" name="stop_detector" pkg="lab4" type="stop_detector.py" output="screen">
+    <param name="detector" value="/home/pi/robot_ws/src/ece387_robot_spring202X-Name/robot/lab4/training_images/sl_detector.svm"/>
+</node>
+
+
+
+
+

Checkpoint 2#

+

Demonstrate the stop detector on the Robot detecting a stop sign.

+
+
+

Determine distance from stop sign#

+
+

Edit stop_detector.py#

+

You will edit your stop sign detector on the Robot to calculate an estimated distance between the camera and the stop sign using triangle similarity.

+

Given a stop sign with a known width, \(W\), we can place the stop sign at a known distance, \(D\), from our camera. The detector will then detect the stop sign and provide a perceived width in pixels, \(P\). Using these values we can calculate the focal length, \(F\) of our camera:

+

\(F = \frac{(P\times D)}{W}\)

+

We can then use the calculated focal length, \(F\), known width, \(W\), and perceived width in pixels, \(P\) to calculate the distance from the camera:

+

\(D' = \frac{(W\times F)}{P}\)

+

Use the above information and create two class variables, FOCAL and STOP_WIDTH, and a class function to calculate distance given a known FOCAL length and a known width of the stop sign, STOP_WIDTH. You will need to print the perceived width of the stop sign to determine the \(P\) value used in the calculation to find the focal length.

+
+

💡️ Tip: Pay attention to what the x and w variables of the box actually represent!

+
+

Create a new publisher that will publish the distance using Float32 std_msgs messages over the /stop_dist topic.

+

Publish the distance of each object seen in the image.

+

Remove any print statements after troubleshooting!

+
+
+
+

Checkpoint 3#

+

Demonstrate the stop_detector node publishing distance from the stop sign.

+
+
+

Printing April Tag information#

+

Create a node on the master in lab4 called apriltag_dist.py. Import the appropriate AprilTag message. Subscribe to the tag_detections topic. Print the identified AprilTag ID and distance. If the camera sees multiple tags, it should print the information for each tag.

+

In your callback function you will want to create a for loop such as:

+
for tag in data.detections:
+
+
+

Use print statements to determine the characteristics of the message (you can also google the message).

+

Add the apriltag_dist node to the lab4 launch file.

+
+
+

Checkpoint 4#

+

Demonstrate the apriltag_dist node printing the ID and distance of each April Tag.

+
+
+

Report#

+

Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under Resources/Lab Template.

+
+

📝️ Note: We will be primarily grading sections 3.1, 3.2, and 3.3 for this lab, but do include the entire lab as you will need other components for the final project report.

+
+
+
+

Turn-in Requirements#

+

[25 points] All checkpoints marked off.

+

[50 points] Report via Gradescope.

+

[25 points] Code: push your code to your repository. Also, include a screen shot of the apriltag_dist.py and stop_detector.py files at the end of your report.

+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/Module9_CV/computer_vision.html b/Module9_CV/computer_vision.html old mode 100755 new mode 100644 index c44f25f..3501a0b --- a/Module9_CV/computer_vision.html +++ b/Module9_CV/computer_vision.html @@ -1,1181 +1,1181 @@ - - - - - - - - - - - - Computer Vision — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Computer Vision#

-
-

Part 1: Image Basics#

-

When we talk about the sizes of images, we generally talk about them in terms of the number of pixels the image possesses in the x(horizontal) or y(vertical) direction. If the image is a color image, we also need to concern ourselves with the depth of the image as well. Normally, each individual pixel is represented by the “color” or the “intensity” of light that appears in a given place in our image.

-

If we think of an image as a grid, each square in the grid contains a single pixel.

-

Most pixels are represented in two ways: grayscale and color. In a grayscale image, each pixel has a value between 0 and 255, where zero is corresponds to “black” and 255 being “white”. The values in between 0 and 255 are varying shades of gray, where values closer to 0 are darker and values closer 255 are lighter:

-

logo

-

The grayscale gradient image in the figure above demonstrates darker pixels on the left-hand side and progressively lighter pixels on the right-hand side.

-

Color pixels, however, are normally represented in the RGB color space (this is where the term color-depth comes from)— one value for the Red component, one for Green, and one for Blue, leading to a total of 3 values per pixel:

-

logo

-

Other color spaces exist, and ordering of the colors may differ as well, but let’s start with the common RGB system. If we say the image is a 24-bit image, each of the three Red, Green, and Blue colors are represented by an integer in the range 0 to 255 (8-bits), which indicates how “much” of the color there is. Given that the pixel value only needs to be in the range [0, 255] we normally use an 8-bit unsigned integer to represent each color intensity. We then combine these values into a RGB tuple in the form (red, green, blue) . This tuple represents our color. For example:

-
    -
  • To construct a white color, we would fill each of the red, green, and blue buckets completely up, like this: (255, 255, 255) — since white is the presence of all color.

  • -
  • Then, to create a black color, we would empty each of the buckets out: (0, 0, 0) — since black is the absence of color.

  • -
  • To create a pure red color, we would fill up the red bucket (and only the red bucket) up completely: (255, 0, 0) .

  • -
  • etc

  • -
-

Take a look at the following image to make this concept more clear:

-

logo

-

For your reference, here are some common colors represented as RGB tuples:

-
    -
  • Black: (0, 0, 0)

  • -
  • White: (255, 255, 255)

  • -
  • Red: (255, 0, 0)

  • -
  • Green: (0, 255, 0)

  • -
  • Blue: (0, 0, 255)

  • -
  • Aqua: (0, 255, 255)

  • -
  • Fuchsia: (255, 0, 255)

  • -
  • Maroon: (128, 0, 0)

  • -
  • Navy: (0, 0, 128)

  • -
  • Olive: (128, 128, 0)

  • -
  • Purple: (128, 0, 128)

  • -
  • Teal: (0, 128, 128)

  • -
  • Yellow: (255, 255, 0)

  • -
-
-
-

Part 2: Coding with OpenCV-Python#

-

It is time to build our first bit of code working with OpenCV. Just like ROS, OpenCV is well supported by both Python and C++. For simplicity, we will use Python throughout this course. However, continue to recognize that if speed and efficiency become important, switching to a more robust language like C++ may become necessary. To make use of OpenCV with Python, we need to import cv2. The code below will simply load in the RGB figure above and print out the pixel values in each of the 4-quadrants.

-

First we need to import the OpenCV Python library, cv2:

-
import cv2
-
-
-

Then we can load the image:

-
image = cv2.imread("RGB_Tuple.JPG")
-
-
-

The shape characteristic of the image returns a tuple of the number of rows, columns, and channels (if the image is color):

-
print("width: %d pixels" %(image.shape[1]))
-print("height: %d pixels" % (image.shape[0]))
-print("color channels: %d" % (image.shape[2]))
-
-
-

You an also access specific pixels within the image (the image variable is really just an array of pixel values) by the row and column coordinates. Each pixel values is an array of Blue, Green, and Red values.

-
# print the BGR values of a pixel in the upper left of the image
-print(image[10, 10, :])
-
-# print the red value of a pixel in the bottom left of the image
-print(image[700, 100, 2])
-
-
-

Fill in the code to do the following:

-
# TODO: print BGR values of a pixel in the upper right of the image
-print(image[ , , :])
-
-# TODO: print BGR values of a pixel in the lower left of the image
-print(image[ , , :])
-
-# TODO: print blue value of a pixel in the lower right of the image
-print(image[ , , ])
-
-
-

We can display the image as well.

-
-

⚠️ WARNING: To exit the image just press any key. DO NOT press the ‘X’ in the corner. If you do press the ‘X’ (smh) you will have to Restart & Clear the Kernel: in the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”.

-
-
cv2.imshow("Loaded image", image)
-cv2.waitKey(0)
-cv2.destroyAllWindows() # close the image window
-
-
-

When executing the code above, there were two minor surprises. What do you think they were? Now lets take a look at additional functionality embedded within OpenCV.

-

Convert image to RGB and print the same pixel values. Remember that the image is already loaded within the image variable.

-
# TODO: Convert image to RGB
-
-
-# TODO: print the RGB values of a pixel in the upper left of the image
-
-
-# TODO: print the red value of a pixel in the bottom left of the image
-
-
-# TODO: print RGB values of a pixel in the upper right of the image
-
-
-# TODO: print RGB values of a pixel in the lower left of the image
-
-
-# TODO: print blue value of a pixel in the lower right of the image
-
-
-
-

Modify the code to convert to grayscale and print the same pixel values.

-
# TODO: Convert image to Grayscale
-
-
-# TODO: print the Grayscale values of a pixel in the upper left of the image
-
-
-# TODO: print Grayscale values of a pixel in the upper right of the image
-
-
-# TODO: print Grayscale values of a pixel in the lower left of the image
-
-
-
-
-

Summary#

-

These examples barely scratch the surface of what is possible with OpenCV. In the upcoming lessons we will learn a few more ways to manipulate images, but if you want to learn more you can either explore the OpenCV-Python Source Documentation or the OpenCV-Python Tutorial.

-
-
-

Assignment#

-

Scan the article on the Histogram of Oriented Gradients (HOG) feature descriptor and be prepared to discuss. I don’t need you to understand the math, but you should be able to understand the advantages of the technique.

-
-
-

Cleanup#

-

In the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-
-
-
-

Part 3: Gradients#

-

The objective of this portion of the lesson is for you to start the process of learning how to create custom object detectors in an image. There are many techniques, but the one technique I am interested in applying first is what is known as Histogram of Oriented Gradients. Before we can dig into the technique, we should first understand a bit about image gradients and contours.

-

By the end of today’s lesson you will be able to:

-
    -
  • Define what an image gradient is

  • -
  • Compute changes in direction of an input image

  • -
  • Define both gradient magnitude and gradient orientation

  • -
  • Use OpenCV to approximate image gradients

  • -
-

The image gradient is one of the fundamental building blocks in computer vision image processing.

-

We use gradients for detecting edges in images, which allows us to find contours and outlines of objects in images. We use them as inputs for quantifying images through feature extraction — in fact, highly successful and well-known image descriptors such as Histogram of Oriented Gradients and SIFT are built upon image gradient representations. Gradient images are even used to construct saliency maps, which highlight the subjects of an image. We use gradients all the time in computer vision and image processing. I would go as far as to say they are one of the most important building blocks you will learn about in this module. While they are not often discussed in detail since other more powerful and interesting methods build on top of them, we are going to take the time and discuss them in detail.

-

As I mentioned in the introduction, image gradients are used as the basic building blocks in many computer vision and image processing applications. However, the main application of image gradients lies within edge detection. As the name suggests, edge detection is the process of finding edges in an image, which reveals structural information regarding the objects in an image. Edges could therefore correspond to:

-
    -
  • Boundaries of an object in an image.

  • -
  • Boundaries of shadowing or lighting conditions in an image.

  • -
  • Boundaries of “parts” within an object.

  • -
-

As we mentioned in the previous portion of the lab, we will often work with grayscale images, because of the massive reduction in images. OpenCV will convert to grayscale using the following conversion formula:

-

\(Y = 0.299 R + 0.587 G + 0.114 B\)

-

Let’s see if that matches our expectations in the figure below:

-

logo

-

The below figure is an image of edges being detected simply by looking for the contours in an image:

-

logo

-

As you can see, all of the edges (or changes in contrast are clearly identified), but how did we do it? Lets look at the math below, and then we will look at how simple the code is by taking advantage of OpenCV.

-

Formally, an image gradient is defined as a directional change in image intensity. Or put more simply, at each pixel of the input (grayscale) image, a gradient measures the change in pixel intensity in a given direction. By estimating the direction or orientation along with the magnitude (i.e. how strong the change in direction is), we are able to detect regions of an image that look like edges.

-

Lets look at a blown up version of a basic pixel map. Our goal here is to establish the basic framework for how we will eventually compute the gradient:

-

logo

-

In the image above we essentially wish to examine the (3 \times 3) neighborhood surrounding the central pixel. Our x values run from left to right, and our y values from top to bottom. In order to compute any changes in direction we will need the north, south, east, and west pixels, which are marked on the above figure.

-

If we denote our input image as I, then we define the north, south, east, and west pixels using the following notation:

-
    -
  • North: \(I(x, y - 1)\)

  • -
  • South: \(I(x, y + 1)\)

  • -
  • East: \(I(x + 1, y)\)

  • -
  • West: \(I(x - 1, y)\)

  • -
-

Again, these four values are critical in computing the changes in image intensity in both the x and y direction.

-

To demonstrate this, let us compute the vertical change or the y-change by taking the difference between the south and north pixels:

-

\(G_{y} = I(x, y + 1) - I(x, y - 1)\)

-

Similarly, we can compute the horizontal change or the x-change by taking the difference between the east and west pixels:

-

\(G_{x} = I(x + 1, y) - I(x - 1, y)\)

-

Awesome — so now we have \(G_{x}\) and \(G_{y}\), which represent the change in image intensity for the central pixel in both the x and y direction. Lets look at a relatively intuitive example at first without all the math.

-

logo

-

On the left we have a \(3 \times 3\) region of an image where the top half of the image is white and the bottom half of the image is black. The gradient orientation is thus equal to \(\theta=-90^{\circ}\)

-

And on the right we have another \(3 \times 3\) neighborhood of an image, where the upper triangular region is white and the lower triangular region is black. Here we can see the change in direction is equal to \(\theta=-45^{\circ}\). While these two examples are both relatively easy to understand, lets use our knowledge of the Pythagorean theorem to actually compute the magnitude and orientation of the gradient with actual values now.

-

logo

-

Inspecting the triangle in the above figure you can see that the gradient magnitude G is the hypotenuse of the triangle. Therefore, all we need to do is apply the Pythagorean theorem and we will end up with the gradient magnitude:

-

\(G = \sqrt{G_{x}^{2} + G_{y}^{2}}\)

-

The gradient orientation can then be given as the ratio of \(G_{y}\) to \(G_{x}\). We can use the \(tan^{-1}\) to compute the gradient orientation,

-

\(\theta = tan^{-1}(\frac{G_{y}}{G_{x}}) \times (\frac{180}{\pi})\)

-

We converted to degrees by multiplying by the ratio of \(180/\pi\). Lets now add pixel intensity values and put this to the test.

-

logo

-

In the above image we have an image where the upper-third is white and the bottom two-thirds is black. Using the equations for \(G_{x}\) and \(G_{y}\), we arrive at:

-

\(G_{x} = \)

-

and

-

\(G_{y} = \)

-

Plugging these values into our gradient magnitude equation we get:

-

\(G = \)

-

As for our gradient orientation:

-

\(\theta = \)

-

Now you try with the following example:

-

logo

-

\(G_{x} = \)

-

and

-

\(G_{y} = \)

-

Plugging these values into our gradient magnitude equation we get:

-

\(G = \)

-

As for our gradient orientation:

-

\(\theta = \)

-

Now that you know how to compute both the orientation and the magnitude of the gradients, you essentially have the most basic building block established for computing the necessary information for HOG w/ SVM. Additionally, you can use the following code to compute very effective contours in images.

-

Fortunately, in practice we don’t need to do any of the math above. Instead we can use what is known as the Sobel Kernel to compute the values for \(G_{x}\) and \(G_{y}\). OpenCV and numpy have functionality built in that allow us to do all of this very quickly.

-
-

To exit the image just press any key. DO NOT press the ‘X’ in the corner. If you do press the ‘X’ (smh) you will have to Restart & Clear the Kernel: in the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”.

-
-
import cv2
-import numpy as np
-
-
-
#load the image
-image=cv2.imread("RGB_Tuple.JPG")
-gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
-
-
-
#Show the original image along with the grayscale image
-cv2.imshow("Original image", image)
-cv2.imshow("Grayscale Image", gray)
-
-# Lets now compute the gradients along the X and Y axis, respectively
-gX = cv2.Sobel(gray,cv2.CV_64F,1,0)
-gY = cv2.Sobel(gray,cv2.CV_64F,0,1)
-
-# the `gX` and `gY` images are now of the floating point data type,
-# so we need to take care to convert them back to an unsigned 8-bit
-# integer representation so other OpenCV functions can utilize them
-gX = cv2.convertScaleAbs(gX)
-gY = cv2.convertScaleAbs(gY)
-
-# combine the sobel X and Y representations into a single image
-sobelCombined = cv2.addWeighted(gX, 0.5, gY, 0.5, 0)
-cv2.imshow("Gradient Image", sobelCombined)
-cv2.waitKey(0)
-cv2.destroyAllWindows() # close the image window
-
-
-
-

Summary#

-

Gradients are one important tool used in object detection. Next lesson we will learn how to apply gradients using the Histogram of Oriented Gradients to train an object detector.

-
-
-

Assignment#

-

Watch the following video on Histogram of Oriented Gradients.

-
-
-

Cleanup#

-

In the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-
-
-
-

Part 4: Histogram of Oriented Gradients (HOG) Features#

-

The objective of this portion of the lesson is to demonstrate the functionality of the HOG with SVM (Support Vector Machine) algorithm for object detection. By this point, we should all be well aware of what a histogram is. The application of the histogram for the HOG feature extraction is to further simplify the tested image to enable our computer to rapidly and accurately identify the presence of an object within the image.
-Instead of using each individual gradient direction of each individual pixel of an image, we group the pixels into small cells. For each cell, we compute all the gradient directions and group them into a number of orientation bins. We sum up the gradient magnitude in each sample. So stronger gradients contribute more weight to their bins, and effects of small random orientations due to noise is reduced. Doing this for all cells gives us a representation of the structure of the image. The HOG features keep the representation of an object distinct but also allow for some variations in shape. For example, lets consider an object detector for a car, see the below figure.

-

logo

-

Comparing each individual pixel of this training image with another test image would not only be time consuming, but it would also be highly subject to noise. As previously mentioned, the HOG feature will consider a block of pixels. The size of this block is variable and will naturally impact both accuracy and speed of execution for the algorithm. Once the block size is determined, the gradient for each pixel within the block is computed. Once the gradients are computed for a block, the entire cell can then be represented by this histogram. Not only does this reduce the amount of data to compare with a test image, but it also reduces the impacts of noise in the image and measurements.

-

logo

-

Now that we have an understanding of the HOG features, lets use tools embedded within OpenCV and Dlib to build our first detector for a stop sign. But first we need to download a pre-created repository of test and training data. Remember, we won’t use our training data to test the effectiveness of the algorithm. Of course the algorithm will work effectively on the training data. Our hope is that we can create a large enough sampling of test data that we can have a highly effective detector that is robust against new images.

-
-

Building a detector using HOG features#

-

Download the example demo into the my_scripts folder you created earlier in the semester. It should be located under ~/master_ws/src/ece387_curriculum/.

-
roscd ece387_curriculum/my_scripts
-git clone git@github.com:ECE495/HOG_Demo.git
-cd HOG_Demo
-
-
-

Take a look at what is contained within the repo. Essentially you have both a training data folder and a test folder. We will now use a tool called imglab to annotate the images for building our detector.

-

Browse to the imglab tool and select “UMM, MAYBE NEXT TIME!”.

-

In the bottom left of the site, select the load button and browse to the training folder:

-

logo

-

Select the first stop sign and the “Rectangle” tool.

-

logo

-

Highlight the border of the stop sign: drag-and-draw a bounding rectangle, ensuring to only select the stop sign and to select all examples of the object in the image.

-
-

📝️ NOTE: It is important to label all examples of objects in an image; otherwise, Dlib will implicitly assume that regions not labeled are regions that should not be detected (i.e., hard-negative mining applied during extraction time).

-
-

You can select a bounding box and hit the delete key to remove it.

-

If you press alt+left/right arrow you can navigate through images in the slider and repeat highlighting the objects.

-

Once all stop signs are complete hit ctrl+e to save the annotations (bounding box information) as a “Dlib XML” file within the training folder using a descriptive name such as stop_annotations.xml.

-

logo

-

We now need to create the code to build the detector based on our annotated training data.

-
cd ~/master_ws/src/ece387_curriculum/my_scripts/HOG_Demo
-touch trainDetector.py
-
-
-

Now open this in your favorite editor to add the following code. I have built into the code the ability to provide command line arguments. This will make the code a bit more flexible such that you don’t need to recreate it in the future if you want to reuse if for another project. You will provide two arguments at runtime. First you need to tell the program where the .xml file is. Second, you will state where you want to put the detector that you create… the detector should have a .svm extension.

-
# import the necessary packages
-from __future__ import print_function
-import argparse
-import dlib
-
-# construct the argument parser and parse the arguments
-ap = argparse.ArgumentParser()
-ap.add_argument("-x", "--xml", required=True, help="path to input XML file")
-ap.add_argument("-d", "--detector", required=True, help="path to output detector")
-args = vars(ap.parse_args())
-
-# grab the default training options for the HOG + Linear SVM detector, then
-# train the detector -- in practice, the `C` parameter can be adjusted...
-# feel free to research and see if you can improve
-print("[INFO] training detector...")
-options = dlib.simple_object_detector_training_options()
-options.C = 1.0
-options.num_threads = 4
-options.be_verbose = True
-dlib.train_simple_object_detector(args["xml"], args["detector"], options)
-
-# show the training accuracy
-print("[INFO] training accuracy: {}".format(
-	dlib.test_simple_object_detector(args["xml"], args["detector"])))
-	
-# load the detector and visualize the HOG filter
-detector = dlib.simple_object_detector(args["detector"])
-win = dlib.image_window()
-win.set_image(detector)
-dlib.hit_enter_to_continue()
-
-
-

Once you have the code entered, you can run it with the following command. Remember, you need to provide two command line arguments:

-
roscd ece387_curriculum/my_scripts/HOG_Demo
-python3 trainDetector.py --xml training/stop_annotations.xml --detector training/stop_detector.svm
-
-
-

You may get a few errors pop up during execution based on your choice for bounding boxes. Make sure you address those errors before continuing. If everything executed correctly, you should ultimately see a picture of the HOG feature you designed.

-
-
-

Testing a detector#

-

Now it is time to build our code to test the detector. The following code will make use of the imutils library as well.

-

You may get a few errors pop up during execution based on your choice for bounding boxes. Make sure you address those errors before continuing. If everything executed correctly, you should ultimately see a picture of the HOG feature you designed.

-

Now it is time to build our code to test the detector.

-
roscd ece387_curriculum/my_scripts/HOG_Demo
-touch testDetector.py
-
-
-

Again, use your preferred editor to enter the code below:

-
# import the necessary packages
-from imutils import paths
-import argparse
-import dlib
-import cv2
-
-# construct the argument parser and parse the arguments
-ap = argparse.ArgumentParser()
-ap.add_argument("-d", "--detector", required=True, help="Path to trained object detector")
-ap.add_argument("-t", "--testing", required=True, help="Path to directory of testing images")
-args = vars(ap.parse_args())
-
-# load the detector
-detector = dlib.simple_object_detector(args["detector"])
-
-# loop over the testing images
-for testingPath in paths.list_images(args["testing"]):
-	# load the image and make predictions
-	image = cv2.imread(testingPath)
-	boxes = detector(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
-	
-	# loop over the bounding boxes and draw them
-	for b in boxes:
-		(x, y, w, h) = (b.left(), b.top(), b.right(), b.bottom())
-		cv2.rectangle(image, (x, y), (w, h), (0, 255, 0), 2)
-		
-	# show the image
-	cv2.imshow("Image", image)
-	cv2.waitKey(0)
-
-
-

Run the test detector:

-
roscd ece387_curriculum/my_scripts/HOG_Demo
-python3 testDetector.py --detector training/stop_detector.svm --testing test
-
-
-

OK, so how did you do? What surprises did you have? What might you consider to improve the detector?

-
-
-

Summary#

-

You have now trained and tested your first detector! In the future you will train a new detector using the camera on your robot and a real stop sign. This will be used in your final project to detect and react to stop signs in the wild!

-
-
-

Assignment#

-

Research Dlib’s simple object detector, and see how you might want to tune the options to improve the performance.

-
-
-

Cleanup#

-

In the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

-
-
-
-

Part 5: ROS and Image Capture#

-

ROS provides a number of tools to interact with a commercial-off-the-shelf camera such as the USB camera connected to your robot. The primary tool we will use is the usb_cam package which is already installed on your robot.

-

Let’s create a lab4 package on the Master we can use to start developing a launch file to run our computer vision tools.

-

In a terminal create a lab4 package, launch folder, and lab4.launch file:

-
cd ~/master_ws/src/ece387_robot_spring202X-USERNAME/
-catkin_create_pkg lab4 rospy sensor_msgs std_msgs cv_bridge apriltag_ros
-cd lab4
-mkdir launch
-cd launch
-touch lab4.launch
-
-
-

Make and source your workspace.

-
-

Launch File - USB Cam#

-

Edit the lab4.launch file to call the usb_cam_node on the robot which will automatically connect to the camera and publish the video over a topic.

-
<launch>
-    
-    <machine
-        name="robot0"
-        address="robot0"
-        default="true"
-        user="pi"
-    />
-
-    <!-- usb camera -->
-    <node machine="robot0" name="usb_cam" pkg="usb_cam" type="usb_cam_node" output="screen" >
-        <param name="video_device" value="/dev/video0" />
-        <param name="image_width" value="640" />
-        <param name="image_height" value="480" />
-        <param name="pixel_format" value="yuyv" />
-        <param name="camera_frame_id" value="usb_cam" />
-        <param name="io_method" value="mmap"/>
-  </node>
-    
-</launch>
-
-
-

Save and exit.

-

Ensure roscore is running on the Master.

-

Run the usb_cam node on the Robot using the lab4 launch file.

-

Open a terminal on the Master and view the topics created by the node.

-

The primary topic we will look at is /usb_cam/image_raw. What type of message is sent over this topic? Take note as we will use this in the lab!

-

Let’s display the video using the image_view tool on the Master.

-
rostopic list
-rosrun rqt_image_view rqt_image_view
-
-
-

Ensure the /usb_cam/image_raw topic is selected.

-
-
-

Calibrate USB Camera#

-

A camera must first be calibrated to utilize computer vision based tasks. Otherwise, there is no reference for how large objects are in regards to the camera frame. The ROS Calibration Tool creates a calibration file that is then used by other ROS packages to enable size and distance calculations. The camera_calibration package utilizes OpenCV camera calibration to allow easy calibration of monocular or stereo cameras using a checkerboard calibration target. The complete guide can be found on the Camera Calibration Tutorial.

-

Connect to the camera using the usb_cam node:

-
roslaunch lab4 lab4.launch
-
-
-

Run the camera calibrate package with the correct parameters (even though the checkerboard says it is a 9x6 board with 3.0 cm squares it is actually a 8x5 board with 2.7 cm squares - the size the calibration tool uses is actually the interior vertex points, not the squares).

-

Open a new terminal on the Master and run the folowing:

-
rosrun camera_calibration cameracalibrator.py --size 8x5 --square 0.027 image:=/usb_cam/image_raw camera:=/usb_cam
-
-
-

You should see a window open that looks like this: -logo

-

In order to get a good calibration you will need to move the checkerboard around in the camera frame such that:

-
    -
  • checkerboard on the camera’s left, right, top and bottom of field of view

    -
      -
    • X bar - left/right in field of view

    • -
    • Y bar - top/bottom in field of view

    • -
    • Size bar - toward/away and tilt from the

    • -
    -
  • -
  • checkerboard filling the whole field of view

  • -
  • checkerboard tilted to the left, right, top and bottom (Skew)

  • -
-

As you move the checkerboard around you will see three bars on the calibration sidebar increase in length.

-

When the CALIBRATE button lights, you have enough data for calibration and can click CALIBRATE to see the results. Calibration can take a couple minutes. The windows might be greyed out but just wait, it is working.

-

logo

-

When complete, select the save button and then commit.

-

Browse to the location of the calibration data, extract, and move to the appropriate ROS folder on the robot:

-
cd /tmp
-tar xf calibrationdata.tar.gz
-scp ost.yaml pi@robotX:/home/pi/.ros/camera_info/head_camera.yaml
-
-
-

Kill the lab4.launch.

-

Create a secure shell to the robot and edit the calibration data and replace “narrow_stero” with “head_camera”:

-
ssh pi@robotX
-nano /home/pi/.ros/camera_info/head_camera.yaml
-
-
-

Rerun the lab4.launch file on the robot. You should see the camera feed reopen and see no errors in the command line (you may need to unplug and plug your camera back in).

-
-
-

Checkpoint#

-

Show an instructor the working camera feed and that the usb_cam node was able to open the camera calibration file.

-
-
-

Summary#

-

You now are able to connect to a USB camera using ROS, display the image provided by the node, and have a calibration file that ROS can use to identify the size of objects in the frame.

-
-
-

Cleanup#

-

Kill all rosnodes and roscore!

-
-
-
-

Part 6: Fiducial Markers#

-

In this lesson we will learn how fiducial markers are used in image processing. Specifically, we will utilize ROS tools to identify different April Tags and use the 3D position and orientation to determine the robot’s distance from an object.

-

A fiducial marker is an artificial feature used in creating controllable experiments, ground truthing, and in simplifying the development of systems where perception is not the central objective. A few examples of fiducial markers include ArUco Markers, AprilTags, and QR codes. Each of these different tags hold information such as an ID or, in the case of QR codes, websites, messages, and etc. We will primarily be focusing on AprilTags as there is a very robust ROS package already built. This library identifies AprilTags and will provide information about the tags size, distance, and orientation.

-
-

AprilTag ROS#

-

Browse to the AprilTag_ROS package on the Master and edit the config file:

-
roscd apriltag_ros/config
-sudo nano tags.yaml
-
-
-

This is where you provide the package with information about the tags it should identify. You should have gotten tags 0-3. Each of these tags is \(.165 m\) wide and should have a corresponding name: “tag_0” (in the final project, you might want to change these names as we will be providing you commands that correspond to each tag). In the tags.yaml file, add a line for each tag under “standalone tags” (replace … with last two tags):

-
standalone_tags:
-  [
-  	{id: 0, size: .165, name: tag_0},
-  	{id: 1, size: .165, name: tag_1},
-  	...
-  ]
-
-
-

Repeat these steps on the Robot.

-
-
-

Launch File - Apriltag_Ros#

-

Edit the lab4.launch file on the Master, calling the continuous_detection node provided by the apriltag_ros package. We need to set the arguments to the values provided by the usb_cam node:

-

Add the following arguments and parameters to the top of the launch file:

-
<arg name="launch_prefix" default="" />
-<arg name="node_namespace" default="apriltag_ros_continuous_node" />
-<arg name="camera_name" default="/usb_cam" />
-<arg name="image_topic" default="image_raw" />
-
-<!-- Set parameters -->
-<rosparam command="load" file="$(find apriltag_ros)/config/settings.yaml" ns="$(arg node_namespace)" />
-<rosparam command="load" file="$(find apriltag_ros)/config/tags.yaml" ns="$(arg node_namespace)" />
-
-
-

Add the apriltag node in the remote section:

-
<!-- apriltag_ros -->
-<node machine="robot0" pkg="apriltag_ros" type="apriltag_ros_continuous_node" name="$(arg node_namespace)" clear_params="true" output="screen" launch-prefix="$(arg launch_prefix)" >
-<!-- Remap topics from those used in code to those on the ROS network -->
-<remap from="image_rect" to="$(arg camera_name)/$(arg image_topic)" />
-<remap from="camera_info" to="$(arg camera_name)/camera_info" />
-
-<param name="publish_tag_detections_image" type="bool" value="true" />      <!-- default: false -->
-</node>
-
-
-

Save and exit.

-

Launch the lab4.launch file.

-

In a terminal on the master open the rqt_image_view node (rosrun rqt_image_view rqt_image_view) and select the tag_detections_image topic. If you hold up each tag, you should see a yellow box highlight the tag with an id in the middle of the tag.

-

In another terminal on the master echo the topic tag_detections. What information do you see? Will the apriltag_ros node identify only one tag at a time? Which value do you think we would use to determine distance from the tag? What kind of message is this? What package does this message come from?

-
-
-

Checkpoint#

-

Show an instructor that the apriltag_ros can identify tags and provides position data.

-
-
-

Summary#

-

You now have the ability to identify AprilTags and because you have a calibrated camera, you can detect the size, orientation, and distance of a tag.

-
-
-

Cleanup#

-

Kill all rosnodes and roscore!

-
-
-
- - - - -
- - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - - - + + + + + + + + + + + + Computer Vision — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + + +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Computer Vision#

+
+

Part 1: Image Basics#

+

When we talk about the sizes of images, we generally talk about them in terms of the number of pixels the image possesses in the x(horizontal) or y(vertical) direction. If the image is a color image, we also need to concern ourselves with the depth of the image as well. Normally, each individual pixel is represented by the “color” or the “intensity” of light that appears in a given place in our image.

+

If we think of an image as a grid, each square in the grid contains a single pixel.

+

Most pixels are represented in two ways: grayscale and color. In a grayscale image, each pixel has a value between 0 and 255, where zero is corresponds to “black” and 255 being “white”. The values in between 0 and 255 are varying shades of gray, where values closer to 0 are darker and values closer 255 are lighter:

+

logo

+

The grayscale gradient image in the figure above demonstrates darker pixels on the left-hand side and progressively lighter pixels on the right-hand side.

+

Color pixels, however, are normally represented in the RGB color space (this is where the term color-depth comes from)— one value for the Red component, one for Green, and one for Blue, leading to a total of 3 values per pixel:

+

logo

+

Other color spaces exist, and ordering of the colors may differ as well, but let’s start with the common RGB system. If we say the image is a 24-bit image, each of the three Red, Green, and Blue colors are represented by an integer in the range 0 to 255 (8-bits), which indicates how “much” of the color there is. Given that the pixel value only needs to be in the range [0, 255] we normally use an 8-bit unsigned integer to represent each color intensity. We then combine these values into a RGB tuple in the form (red, green, blue) . This tuple represents our color. For example:

+
    +
  • To construct a white color, we would fill each of the red, green, and blue buckets completely up, like this: (255, 255, 255) — since white is the presence of all color.

  • +
  • Then, to create a black color, we would empty each of the buckets out: (0, 0, 0) — since black is the absence of color.

  • +
  • To create a pure red color, we would fill up the red bucket (and only the red bucket) up completely: (255, 0, 0) .

  • +
  • etc

  • +
+

Take a look at the following image to make this concept more clear:

+

logo

+

For your reference, here are some common colors represented as RGB tuples:

+
    +
  • Black: (0, 0, 0)

  • +
  • White: (255, 255, 255)

  • +
  • Red: (255, 0, 0)

  • +
  • Green: (0, 255, 0)

  • +
  • Blue: (0, 0, 255)

  • +
  • Aqua: (0, 255, 255)

  • +
  • Fuchsia: (255, 0, 255)

  • +
  • Maroon: (128, 0, 0)

  • +
  • Navy: (0, 0, 128)

  • +
  • Olive: (128, 128, 0)

  • +
  • Purple: (128, 0, 128)

  • +
  • Teal: (0, 128, 128)

  • +
  • Yellow: (255, 255, 0)

  • +
+
+
+

Part 2: Coding with OpenCV-Python#

+

It is time to build our first bit of code working with OpenCV. Just like ROS, OpenCV is well supported by both Python and C++. For simplicity, we will use Python throughout this course. However, continue to recognize that if speed and efficiency become important, switching to a more robust language like C++ may become necessary. To make use of OpenCV with Python, we need to import cv2. The code below will simply load in the RGB figure above and print out the pixel values in each of the 4-quadrants.

+

First we need to import the OpenCV Python library, cv2:

+
import cv2
+
+
+

Then we can load the image:

+
image = cv2.imread("RGB_Tuple.JPG")
+
+
+

The shape characteristic of the image returns a tuple of the number of rows, columns, and channels (if the image is color):

+
print("width: %d pixels" %(image.shape[1]))
+print("height: %d pixels" % (image.shape[0]))
+print("color channels: %d" % (image.shape[2]))
+
+
+

You an also access specific pixels within the image (the image variable is really just an array of pixel values) by the row and column coordinates. Each pixel values is an array of Blue, Green, and Red values.

+
# print the BGR values of a pixel in the upper left of the image
+print(image[10, 10, :])
+
+# print the red value of a pixel in the bottom left of the image
+print(image[700, 100, 2])
+
+
+

Fill in the code to do the following:

+
# TODO: print BGR values of a pixel in the upper right of the image
+print(image[ , , :])
+
+# TODO: print BGR values of a pixel in the lower left of the image
+print(image[ , , :])
+
+# TODO: print blue value of a pixel in the lower right of the image
+print(image[ , , ])
+
+
+

We can display the image as well.

+
+

⚠️ WARNING: To exit the image just press any key. DO NOT press the ‘X’ in the corner. If you do press the ‘X’ (smh) you will have to Restart & Clear the Kernel: in the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”.

+
+
cv2.imshow("Loaded image", image)
+cv2.waitKey(0)
+cv2.destroyAllWindows() # close the image window
+
+
+

When executing the code above, there were two minor surprises. What do you think they were? Now lets take a look at additional functionality embedded within OpenCV.

+

Convert image to RGB and print the same pixel values. Remember that the image is already loaded within the image variable.

+
# TODO: Convert image to RGB
+
+
+# TODO: print the RGB values of a pixel in the upper left of the image
+
+
+# TODO: print the red value of a pixel in the bottom left of the image
+
+
+# TODO: print RGB values of a pixel in the upper right of the image
+
+
+# TODO: print RGB values of a pixel in the lower left of the image
+
+
+# TODO: print blue value of a pixel in the lower right of the image
+
+
+
+

Modify the code to convert to grayscale and print the same pixel values.

+
# TODO: Convert image to Grayscale
+
+
+# TODO: print the Grayscale values of a pixel in the upper left of the image
+
+
+# TODO: print Grayscale values of a pixel in the upper right of the image
+
+
+# TODO: print Grayscale values of a pixel in the lower left of the image
+
+
+
+
+

Summary#

+

These examples barely scratch the surface of what is possible with OpenCV. In the upcoming lessons we will learn a few more ways to manipulate images, but if you want to learn more you can either explore the OpenCV-Python Source Documentation or the OpenCV-Python Tutorial.

+
+
+

Assignment#

+

Scan the article on the Histogram of Oriented Gradients (HOG) feature descriptor and be prepared to discuss. I don’t need you to understand the math, but you should be able to understand the advantages of the technique.

+
+
+

Cleanup#

+

In the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+
+
+
+

Part 3: Gradients#

+

The objective of this portion of the lesson is for you to start the process of learning how to create custom object detectors in an image. There are many techniques, but the one technique I am interested in applying first is what is known as Histogram of Oriented Gradients. Before we can dig into the technique, we should first understand a bit about image gradients and contours.

+

By the end of today’s lesson you will be able to:

+
    +
  • Define what an image gradient is

  • +
  • Compute changes in direction of an input image

  • +
  • Define both gradient magnitude and gradient orientation

  • +
  • Use OpenCV to approximate image gradients

  • +
+

The image gradient is one of the fundamental building blocks in computer vision image processing.

+

We use gradients for detecting edges in images, which allows us to find contours and outlines of objects in images. We use them as inputs for quantifying images through feature extraction — in fact, highly successful and well-known image descriptors such as Histogram of Oriented Gradients and SIFT are built upon image gradient representations. Gradient images are even used to construct saliency maps, which highlight the subjects of an image. We use gradients all the time in computer vision and image processing. I would go as far as to say they are one of the most important building blocks you will learn about in this module. While they are not often discussed in detail since other more powerful and interesting methods build on top of them, we are going to take the time and discuss them in detail.

+

As I mentioned in the introduction, image gradients are used as the basic building blocks in many computer vision and image processing applications. However, the main application of image gradients lies within edge detection. As the name suggests, edge detection is the process of finding edges in an image, which reveals structural information regarding the objects in an image. Edges could therefore correspond to:

+
    +
  • Boundaries of an object in an image.

  • +
  • Boundaries of shadowing or lighting conditions in an image.

  • +
  • Boundaries of “parts” within an object.

  • +
+

As we mentioned in the previous portion of the lab, we will often work with grayscale images, because of the massive reduction in images. OpenCV will convert to grayscale using the following conversion formula:

+

\(Y = 0.299 R + 0.587 G + 0.114 B\)

+

Let’s see if that matches our expectations in the figure below:

+

logo

+

The below figure is an image of edges being detected simply by looking for the contours in an image:

+

logo

+

As you can see, all of the edges (or changes in contrast are clearly identified), but how did we do it? Lets look at the math below, and then we will look at how simple the code is by taking advantage of OpenCV.

+

Formally, an image gradient is defined as a directional change in image intensity. Or put more simply, at each pixel of the input (grayscale) image, a gradient measures the change in pixel intensity in a given direction. By estimating the direction or orientation along with the magnitude (i.e. how strong the change in direction is), we are able to detect regions of an image that look like edges.

+

Lets look at a blown up version of a basic pixel map. Our goal here is to establish the basic framework for how we will eventually compute the gradient:

+

logo

+

In the image above we essentially wish to examine the (3 \times 3) neighborhood surrounding the central pixel. Our x values run from left to right, and our y values from top to bottom. In order to compute any changes in direction we will need the north, south, east, and west pixels, which are marked on the above figure.

+

If we denote our input image as I, then we define the north, south, east, and west pixels using the following notation:

+
    +
  • North: \(I(x, y - 1)\)

  • +
  • South: \(I(x, y + 1)\)

  • +
  • East: \(I(x + 1, y)\)

  • +
  • West: \(I(x - 1, y)\)

  • +
+

Again, these four values are critical in computing the changes in image intensity in both the x and y direction.

+

To demonstrate this, let us compute the vertical change or the y-change by taking the difference between the south and north pixels:

+

\(G_{y} = I(x, y + 1) - I(x, y - 1)\)

+

Similarly, we can compute the horizontal change or the x-change by taking the difference between the east and west pixels:

+

\(G_{x} = I(x + 1, y) - I(x - 1, y)\)

+

Awesome — so now we have \(G_{x}\) and \(G_{y}\), which represent the change in image intensity for the central pixel in both the x and y direction. Lets look at a relatively intuitive example at first without all the math.

+

logo

+

On the left we have a \(3 \times 3\) region of an image where the top half of the image is white and the bottom half of the image is black. The gradient orientation is thus equal to \(\theta=-90^{\circ}\)

+

And on the right we have another \(3 \times 3\) neighborhood of an image, where the upper triangular region is white and the lower triangular region is black. Here we can see the change in direction is equal to \(\theta=-45^{\circ}\). While these two examples are both relatively easy to understand, lets use our knowledge of the Pythagorean theorem to actually compute the magnitude and orientation of the gradient with actual values now.

+

logo

+

Inspecting the triangle in the above figure you can see that the gradient magnitude G is the hypotenuse of the triangle. Therefore, all we need to do is apply the Pythagorean theorem and we will end up with the gradient magnitude:

+

\(G = \sqrt{G_{x}^{2} + G_{y}^{2}}\)

+

The gradient orientation can then be given as the ratio of \(G_{y}\) to \(G_{x}\). We can use the \(tan^{-1}\) to compute the gradient orientation,

+

\(\theta = tan^{-1}(\frac{G_{y}}{G_{x}}) \times (\frac{180}{\pi})\)

+

We converted to degrees by multiplying by the ratio of \(180/\pi\). Lets now add pixel intensity values and put this to the test.

+

logo

+

In the above image we have an image where the upper-third is white and the bottom two-thirds is black. Using the equations for \(G_{x}\) and \(G_{y}\), we arrive at:

+

\(G_{x} = \)

+

and

+

\(G_{y} = \)

+

Plugging these values into our gradient magnitude equation we get:

+

\(G = \)

+

As for our gradient orientation:

+

\(\theta = \)

+

Now you try with the following example:

+

logo

+

\(G_{x} = \)

+

and

+

\(G_{y} = \)

+

Plugging these values into our gradient magnitude equation we get:

+

\(G = \)

+

As for our gradient orientation:

+

\(\theta = \)

+

Now that you know how to compute both the orientation and the magnitude of the gradients, you essentially have the most basic building block established for computing the necessary information for HOG w/ SVM. Additionally, you can use the following code to compute very effective contours in images.

+

Fortunately, in practice we don’t need to do any of the math above. Instead we can use what is known as the Sobel Kernel to compute the values for \(G_{x}\) and \(G_{y}\). OpenCV and numpy have functionality built in that allow us to do all of this very quickly.

+
+

To exit the image just press any key. DO NOT press the ‘X’ in the corner. If you do press the ‘X’ (smh) you will have to Restart & Clear the Kernel: in the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”.

+
+
import cv2
+import numpy as np
+
+
+
#load the image
+image=cv2.imread("RGB_Tuple.JPG")
+gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+
+
+
#Show the original image along with the grayscale image
+cv2.imshow("Original image", image)
+cv2.imshow("Grayscale Image", gray)
+
+# Lets now compute the gradients along the X and Y axis, respectively
+gX = cv2.Sobel(gray,cv2.CV_64F,1,0)
+gY = cv2.Sobel(gray,cv2.CV_64F,0,1)
+
+# the `gX` and `gY` images are now of the floating point data type,
+# so we need to take care to convert them back to an unsigned 8-bit
+# integer representation so other OpenCV functions can utilize them
+gX = cv2.convertScaleAbs(gX)
+gY = cv2.convertScaleAbs(gY)
+
+# combine the sobel X and Y representations into a single image
+sobelCombined = cv2.addWeighted(gX, 0.5, gY, 0.5, 0)
+cv2.imshow("Gradient Image", sobelCombined)
+cv2.waitKey(0)
+cv2.destroyAllWindows() # close the image window
+
+
+
+

Summary#

+

Gradients are one important tool used in object detection. Next lesson we will learn how to apply gradients using the Histogram of Oriented Gradients to train an object detector.

+
+
+

Assignment#

+

Watch the following video on Histogram of Oriented Gradients.

+
+
+

Cleanup#

+

In the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+
+
+
+

Part 4: Histogram of Oriented Gradients (HOG) Features#

+

The objective of this portion of the lesson is to demonstrate the functionality of the HOG with SVM (Support Vector Machine) algorithm for object detection. By this point, we should all be well aware of what a histogram is. The application of the histogram for the HOG feature extraction is to further simplify the tested image to enable our computer to rapidly and accurately identify the presence of an object within the image.
+Instead of using each individual gradient direction of each individual pixel of an image, we group the pixels into small cells. For each cell, we compute all the gradient directions and group them into a number of orientation bins. We sum up the gradient magnitude in each sample. So stronger gradients contribute more weight to their bins, and effects of small random orientations due to noise is reduced. Doing this for all cells gives us a representation of the structure of the image. The HOG features keep the representation of an object distinct but also allow for some variations in shape. For example, lets consider an object detector for a car, see the below figure.

+

logo

+

Comparing each individual pixel of this training image with another test image would not only be time consuming, but it would also be highly subject to noise. As previously mentioned, the HOG feature will consider a block of pixels. The size of this block is variable and will naturally impact both accuracy and speed of execution for the algorithm. Once the block size is determined, the gradient for each pixel within the block is computed. Once the gradients are computed for a block, the entire cell can then be represented by this histogram. Not only does this reduce the amount of data to compare with a test image, but it also reduces the impacts of noise in the image and measurements.

+

logo

+

Now that we have an understanding of the HOG features, lets use tools embedded within OpenCV and Dlib to build our first detector for a stop sign. But first we need to download a pre-created repository of test and training data. Remember, we won’t use our training data to test the effectiveness of the algorithm. Of course the algorithm will work effectively on the training data. Our hope is that we can create a large enough sampling of test data that we can have a highly effective detector that is robust against new images.

+
+

Building a detector using HOG features#

+

Download the example demo into the my_scripts folder you created earlier in the semester. It should be located under ~/master_ws/src/ece387_curriculum/.

+
roscd ece387_curriculum/my_scripts
+git clone git@github.com:ECE495/HOG_Demo.git
+cd HOG_Demo
+
+
+

Take a look at what is contained within the repo. Essentially you have both a training data folder and a test folder. We will now use a tool called imglab to annotate the images for building our detector.

+

Browse to the imglab tool and select “UMM, MAYBE NEXT TIME!”.

+

In the bottom left of the site, select the load button and browse to the training folder:

+

logo

+

Select the first stop sign and the “Rectangle” tool.

+

logo

+

Highlight the border of the stop sign: drag-and-draw a bounding rectangle, ensuring to only select the stop sign and to select all examples of the object in the image.

+
+

📝️ NOTE: It is important to label all examples of objects in an image; otherwise, Dlib will implicitly assume that regions not labeled are regions that should not be detected (i.e., hard-negative mining applied during extraction time).

+
+

You can select a bounding box and hit the delete key to remove it.

+

If you press alt+left/right arrow you can navigate through images in the slider and repeat highlighting the objects.

+

Once all stop signs are complete hit ctrl+e to save the annotations (bounding box information) as a “Dlib XML” file within the training folder using a descriptive name such as stop_annotations.xml.

+

logo

+

We now need to create the code to build the detector based on our annotated training data.

+
cd ~/master_ws/src/ece387_curriculum/my_scripts/HOG_Demo
+touch trainDetector.py
+
+
+

Now open this in your favorite editor to add the following code. I have built into the code the ability to provide command line arguments. This will make the code a bit more flexible such that you don’t need to recreate it in the future if you want to reuse if for another project. You will provide two arguments at runtime. First you need to tell the program where the .xml file is. Second, you will state where you want to put the detector that you create… the detector should have a .svm extension.

+
# import the necessary packages
+from __future__ import print_function
+import argparse
+import dlib
+
+# construct the argument parser and parse the arguments
+ap = argparse.ArgumentParser()
+ap.add_argument("-x", "--xml", required=True, help="path to input XML file")
+ap.add_argument("-d", "--detector", required=True, help="path to output detector")
+args = vars(ap.parse_args())
+
+# grab the default training options for the HOG + Linear SVM detector, then
+# train the detector -- in practice, the `C` parameter can be adjusted...
+# feel free to research and see if you can improve
+print("[INFO] training detector...")
+options = dlib.simple_object_detector_training_options()
+options.C = 1.0
+options.num_threads = 4
+options.be_verbose = True
+dlib.train_simple_object_detector(args["xml"], args["detector"], options)
+
+# show the training accuracy
+print("[INFO] training accuracy: {}".format(
+	dlib.test_simple_object_detector(args["xml"], args["detector"])))
+	
+# load the detector and visualize the HOG filter
+detector = dlib.simple_object_detector(args["detector"])
+win = dlib.image_window()
+win.set_image(detector)
+dlib.hit_enter_to_continue()
+
+
+

Once you have the code entered, you can run it with the following command. Remember, you need to provide two command line arguments:

+
roscd ece387_curriculum/my_scripts/HOG_Demo
+python3 trainDetector.py --xml training/stop_annotations.xml --detector training/stop_detector.svm
+
+
+

You may get a few errors pop up during execution based on your choice for bounding boxes. Make sure you address those errors before continuing. If everything executed correctly, you should ultimately see a picture of the HOG feature you designed.

+
+
+

Testing a detector#

+

Now it is time to build our code to test the detector. The following code will make use of the imutils library as well.

+

You may get a few errors pop up during execution based on your choice for bounding boxes. Make sure you address those errors before continuing. If everything executed correctly, you should ultimately see a picture of the HOG feature you designed.

+

Now it is time to build our code to test the detector.

+
roscd ece387_curriculum/my_scripts/HOG_Demo
+touch testDetector.py
+
+
+

Again, use your preferred editor to enter the code below:

+
# import the necessary packages
+from imutils import paths
+import argparse
+import dlib
+import cv2
+
+# construct the argument parser and parse the arguments
+ap = argparse.ArgumentParser()
+ap.add_argument("-d", "--detector", required=True, help="Path to trained object detector")
+ap.add_argument("-t", "--testing", required=True, help="Path to directory of testing images")
+args = vars(ap.parse_args())
+
+# load the detector
+detector = dlib.simple_object_detector(args["detector"])
+
+# loop over the testing images
+for testingPath in paths.list_images(args["testing"]):
+	# load the image and make predictions
+	image = cv2.imread(testingPath)
+	boxes = detector(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
+	
+	# loop over the bounding boxes and draw them
+	for b in boxes:
+		(x, y, w, h) = (b.left(), b.top(), b.right(), b.bottom())
+		cv2.rectangle(image, (x, y), (w, h), (0, 255, 0), 2)
+		
+	# show the image
+	cv2.imshow("Image", image)
+	cv2.waitKey(0)
+
+
+

Run the test detector:

+
roscd ece387_curriculum/my_scripts/HOG_Demo
+python3 testDetector.py --detector training/stop_detector.svm --testing test
+
+
+

OK, so how did you do? What surprises did you have? What might you consider to improve the detector?

+
+
+

Summary#

+

You have now trained and tested your first detector! In the future you will train a new detector using the camera on your robot and a real stop sign. This will be used in your final project to detect and react to stop signs in the wild!

+
+
+

Assignment#

+

Research Dlib’s simple object detector, and see how you might want to tune the options to improve the performance.

+
+
+

Cleanup#

+

In the Jupyter Notebook at the top menu bar select “Kernel” and “Restart & Clear Output”. Shutdown the notebook server by typing ctrl+c within the terminal you ran jupyter-notebook in. Select ‘y’.

+
+
+
+

Part 5: ROS and Image Capture#

+

ROS provides a number of tools to interact with a commercial-off-the-shelf camera such as the USB camera connected to your robot. The primary tool we will use is the usb_cam package which is already installed on your robot.

+

Let’s create a lab4 package on the Master we can use to start developing a launch file to run our computer vision tools.

+

In a terminal create a lab4 package, launch folder, and lab4.launch file:

+
cd ~/master_ws/src/ece387_robot_spring202X-USERNAME/
+catkin_create_pkg lab4 rospy sensor_msgs std_msgs cv_bridge apriltag_ros
+cd lab4
+mkdir launch
+cd launch
+touch lab4.launch
+
+
+

Make and source your workspace.

+
+

Launch File - USB Cam#

+

Edit the lab4.launch file to call the usb_cam_node on the robot which will automatically connect to the camera and publish the video over a topic.

+
<launch>
+    
+    <machine
+        name="robot0"
+        address="robot0"
+        default="true"
+        user="pi"
+    />
+
+    <!-- usb camera -->
+    <node machine="robot0" name="usb_cam" pkg="usb_cam" type="usb_cam_node" output="screen" >
+        <param name="video_device" value="/dev/video0" />
+        <param name="image_width" value="640" />
+        <param name="image_height" value="480" />
+        <param name="pixel_format" value="yuyv" />
+        <param name="camera_frame_id" value="usb_cam" />
+        <param name="io_method" value="mmap"/>
+  </node>
+    
+</launch>
+
+
+

Save and exit.

+

Ensure roscore is running on the Master.

+

Run the usb_cam node on the Robot using the lab4 launch file.

+

Open a terminal on the Master and view the topics created by the node.

+

The primary topic we will look at is /usb_cam/image_raw. What type of message is sent over this topic? Take note as we will use this in the lab!

+

Let’s display the video using the image_view tool on the Master.

+
rostopic list
+rosrun rqt_image_view rqt_image_view
+
+
+

Ensure the /usb_cam/image_raw topic is selected.

+
+
+

Calibrate USB Camera#

+

A camera must first be calibrated to utilize computer vision based tasks. Otherwise, there is no reference for how large objects are in regards to the camera frame. The ROS Calibration Tool creates a calibration file that is then used by other ROS packages to enable size and distance calculations. The camera_calibration package utilizes OpenCV camera calibration to allow easy calibration of monocular or stereo cameras using a checkerboard calibration target. The complete guide can be found on the Camera Calibration Tutorial.

+

Connect to the camera using the usb_cam node:

+
roslaunch lab4 lab4.launch
+
+
+

Run the camera calibrate package with the correct parameters (even though the checkerboard says it is a 9x6 board with 3.0 cm squares it is actually a 8x5 board with 2.7 cm squares - the size the calibration tool uses is actually the interior vertex points, not the squares).

+

Open a new terminal on the Master and run the folowing:

+
rosrun camera_calibration cameracalibrator.py --size 8x5 --square 0.027 image:=/usb_cam/image_raw camera:=/usb_cam
+
+
+

You should see a window open that looks like this: +logo

+

In order to get a good calibration you will need to move the checkerboard around in the camera frame such that:

+
    +
  • checkerboard on the camera’s left, right, top and bottom of field of view

    +
      +
    • X bar - left/right in field of view

    • +
    • Y bar - top/bottom in field of view

    • +
    • Size bar - toward/away and tilt from the

    • +
    +
  • +
  • checkerboard filling the whole field of view

  • +
  • checkerboard tilted to the left, right, top and bottom (Skew)

  • +
+

As you move the checkerboard around you will see three bars on the calibration sidebar increase in length.

+

When the CALIBRATE button lights, you have enough data for calibration and can click CALIBRATE to see the results. Calibration can take a couple minutes. The windows might be greyed out but just wait, it is working.

+

logo

+

When complete, select the save button and then commit.

+

Browse to the location of the calibration data, extract, and move to the appropriate ROS folder on the robot:

+
cd /tmp
+tar xf calibrationdata.tar.gz
+scp ost.yaml pi@robotX:/home/pi/.ros/camera_info/head_camera.yaml
+
+
+

Kill the lab4.launch.

+

Create a secure shell to the robot and edit the calibration data and replace “narrow_stero” with “head_camera”:

+
ssh pi@robotX
+nano /home/pi/.ros/camera_info/head_camera.yaml
+
+
+

Rerun the lab4.launch file on the robot. You should see the camera feed reopen and see no errors in the command line (you may need to unplug and plug your camera back in).

+
+
+

Checkpoint#

+

Show an instructor the working camera feed and that the usb_cam node was able to open the camera calibration file.

+
+
+

Summary#

+

You now are able to connect to a USB camera using ROS, display the image provided by the node, and have a calibration file that ROS can use to identify the size of objects in the frame.

+
+
+

Cleanup#

+

Kill all rosnodes and roscore!

+
+
+
+

Part 6: Fiducial Markers#

+

In this lesson we will learn how fiducial markers are used in image processing. Specifically, we will utilize ROS tools to identify different April Tags and use the 3D position and orientation to determine the robot’s distance from an object.

+

A fiducial marker is an artificial feature used in creating controllable experiments, ground truthing, and in simplifying the development of systems where perception is not the central objective. A few examples of fiducial markers include ArUco Markers, AprilTags, and QR codes. Each of these different tags hold information such as an ID or, in the case of QR codes, websites, messages, and etc. We will primarily be focusing on AprilTags as there is a very robust ROS package already built. This library identifies AprilTags and will provide information about the tags size, distance, and orientation.

+
+

AprilTag ROS#

+

Browse to the AprilTag_ROS package on the Master and edit the config file:

+
roscd apriltag_ros/config
+sudo nano tags.yaml
+
+
+

This is where you provide the package with information about the tags it should identify. You should have gotten tags 0-3. Each of these tags is \(.165 m\) wide and should have a corresponding name: “tag_0” (in the final project, you might want to change these names as we will be providing you commands that correspond to each tag). In the tags.yaml file, add a line for each tag under “standalone tags” (replace … with last two tags):

+
standalone_tags:
+  [
+  	{id: 0, size: .165, name: tag_0},
+  	{id: 1, size: .165, name: tag_1},
+  	...
+  ]
+
+
+

Repeat these steps on the Robot.

+
+
+

Launch File - Apriltag_Ros#

+

Edit the lab4.launch file on the Master, calling the continuous_detection node provided by the apriltag_ros package. We need to set the arguments to the values provided by the usb_cam node:

+

Add the following arguments and parameters to the top of the launch file:

+
<arg name="launch_prefix" default="" />
+<arg name="node_namespace" default="apriltag_ros_continuous_node" />
+<arg name="camera_name" default="/usb_cam" />
+<arg name="image_topic" default="image_raw" />
+
+<!-- Set parameters -->
+<rosparam command="load" file="$(find apriltag_ros)/config/settings.yaml" ns="$(arg node_namespace)" />
+<rosparam command="load" file="$(find apriltag_ros)/config/tags.yaml" ns="$(arg node_namespace)" />
+
+
+

Add the apriltag node in the remote section:

+
<!-- apriltag_ros -->
+<node machine="robot0" pkg="apriltag_ros" type="apriltag_ros_continuous_node" name="$(arg node_namespace)" clear_params="true" output="screen" launch-prefix="$(arg launch_prefix)" >
+<!-- Remap topics from those used in code to those on the ROS network -->
+<remap from="image_rect" to="$(arg camera_name)/$(arg image_topic)" />
+<remap from="camera_info" to="$(arg camera_name)/camera_info" />
+
+<param name="publish_tag_detections_image" type="bool" value="true" />      <!-- default: false -->
+</node>
+
+
+

Save and exit.

+

Launch the lab4.launch file.

+

In a terminal on the master open the rqt_image_view node (rosrun rqt_image_view rqt_image_view) and select the tag_detections_image topic. If you hold up each tag, you should see a yellow box highlight the tag with an id in the middle of the tag.

+

In another terminal on the master echo the topic tag_detections. What information do you see? Will the apriltag_ros node identify only one tag at a time? Which value do you think we would use to determine distance from the tag? What kind of message is this? What package does this message come from?

+
+
+

Checkpoint#

+

Show an instructor that the apriltag_ros can identify tags and provides position data.

+
+
+

Summary#

+

You now have the ability to identify AprilTags and because you have a calibrated camera, you can detect the size, orientation, and distance of a tag.

+
+
+

Cleanup#

+

Kill all rosnodes and roscore!

+
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/_downloads/3a3cd348ff4a2bc81ddea473f4ae906f/burger_usbcam_mount.stl b/_downloads/3a3cd348ff4a2bc81ddea473f4ae906f/burger_usbcam_mount.stl old mode 100755 new mode 100644 diff --git a/_downloads/6fe569f9320a6bd641887792fd967ce2/turtlebot_controller.py b/_downloads/6fe569f9320a6bd641887792fd967ce2/turtlebot_controller.py old mode 100755 new mode 100644 index 5971044..67b0a6e --- a/_downloads/6fe569f9320a6bd641887792fd967ce2/turtlebot_controller.py +++ b/_downloads/6fe569f9320a6bd641887792fd967ce2/turtlebot_controller.py @@ -1,58 +1,58 @@ -#!/usr/bin/env python3 - -#TODO 1 Modify this header so that the correct information is displayed -#Name -#Name of code -#For lab1, this will subscribe to mouse_client and publish to cmd_vel -#Will convert messages of type MouseController to Twist -#Deactivates when mouse wheel is scrolled up -#last modified DATE 2023 - - -import rospy -#TODO 2 Import the appropriate message types that we will need - - -class Controller: - """Class that controls subsystems on Turtlebot3""" - def __init__(self): - #TODO 3 initialize the appropriate Controller class attributes - - - - - self.pub = rospy.Publisher('cmd_vel', Twist, queue_size = 1) - - rospy.Subscriber('mouse_info', MouseController, self.callback_mouseControl) - - self.ctrl_c = False - rospy.on_shutdown(self.shutdownhook) - - def callback_mouseControl(self, mouseInfo): - #TODO 4 Scale xPos from -1 to 1 to -.5 to .5 - - #TODO 5 set angular z in Twist message to the scaled value in the appropriate direction - - #TODO 6 Scale yPos from -1 to 1 to -.5 to .5 - - #TODO 7 set linear x in Twist message to the scaled value in the appropriate direction - - #TODO 8 publish the Twist message - - - - def shutdownhook(self): - print("Controller exiting. Halting robot.") - self.ctrl_c = True - #TODO 9 force the linear x and angular z commands to 0 before halting - - - self.pub.publish(self.cmd) - - - - -if __name__ == '__main__': - rospy.init_node('controller') - c = Controller() +#!/usr/bin/env python3 + +#TODO 1 Modify this header so that the correct information is displayed +#Name +#Name of code +#For lab1, this will subscribe to mouse_client and publish to cmd_vel +#Will convert messages of type MouseController to Twist +#Deactivates when mouse wheel is scrolled up +#last modified DATE 2023 + + +import rospy +#TODO 2 Import the appropriate message types that we will need + + +class Controller: + """Class that controls subsystems on Turtlebot3""" + def __init__(self): + #TODO 3 initialize the appropriate Controller class attributes + + + + + self.pub = rospy.Publisher('cmd_vel', Twist, queue_size = 1) + + rospy.Subscriber('mouse_info', MouseController, self.callback_mouseControl) + + self.ctrl_c = False + rospy.on_shutdown(self.shutdownhook) + + def callback_mouseControl(self, mouseInfo): + #TODO 4 Scale xPos from -1 to 1 to -.5 to .5 + + #TODO 5 set angular z in Twist message to the scaled value in the appropriate direction + + #TODO 6 Scale yPos from -1 to 1 to -.5 to .5 + + #TODO 7 set linear x in Twist message to the scaled value in the appropriate direction + + #TODO 8 publish the Twist message + + + + def shutdownhook(self): + print("Controller exiting. Halting robot.") + self.ctrl_c = True + #TODO 9 force the linear x and angular z commands to 0 before halting + + + self.pub.publish(self.cmd) + + + + +if __name__ == '__main__': + rospy.init_node('controller') + c = Controller() rospy.spin() \ No newline at end of file diff --git a/_downloads/cb5daeab4a0e775f3d6d21655764a2e9/mouse_client_OO.py b/_downloads/cb5daeab4a0e775f3d6d21655764a2e9/mouse_client_OO.py old mode 100755 new mode 100644 index ada34dd..dfbabfb --- a/_downloads/cb5daeab4a0e775f3d6d21655764a2e9/mouse_client_OO.py +++ b/_downloads/cb5daeab4a0e775f3d6d21655764a2e9/mouse_client_OO.py @@ -1,109 +1,109 @@ -#!/usr/bin/env python3 - -#TODO 0 Update the header below with your information -#Name -#ROS Enabled mouse client -#Activates when mouse wheel is scrolled down and -#Deactivates when mouse wheel is scrolled up -#last modified 8 Jan 2023 -#mouse_client.py - -import rospy -import pyautogui -import sys -import time -import os -from pynput.mouse import Listener -# TODO 1 Import the lab1 message file you created for the Mouse Controller data - - -#Initialize global variable that will keep track of mouse button status -global activate -activate = False - - -#get dimensions of screen so that we can scale for (-1,1) in both x and y -xDim,yDim = pyautogui.size() - -class Mouse: - def __init__(self): - global activate - listener = Listener(on_move=self.on_move, on_click=self.on_click, on_scroll=self.on_scroll) - listener.start() - self.ctrl_c = False - rospy.on_shutdown(self.shutdownhook) - - # TODO 2 Create class variables to store activation status, and x and y position - # Initialize the controller status to False, and in the center of the screen - - # TODO 3 Create a publisher to publish MouseController data on the mouse_info topic - - - - # Handle the event of the user moving the mouse - def on_move(self,x,y): - #We need to establish the use of the global variable for button activation - global activate - - - if(activate == False): - messageString = "No movement sent, activate cursor first by scrolling down on the mouse wheel" - print(messageString, end='') - # overwrite same line - print('\b' *len(messageString), end='', flush=True) - xScale = 0.0 - yScale = 0.0 - else: - # Use the dimensions of the monitor to convert output into x & y between -1 and 1 - xScale = ((x/(xDim/2))-1) - yScale = -((y/(yDim/2))-1) - xFormat = "{:+.3f}".format(xScale) - yFormat = "{:+.3f}".format(yScale) - # create 19 byte message with x,y coordinates embedded and print to screen - positionStr = 'X: ' + str(xFormat) + ' Y: ' + str(yFormat) + " " - print(positionStr, end='') - # overwrite same line - print('\b' *len(positionStr), end='', flush=True) - # TODO 4 Update the appropriate class variables and publish - - - #Currently I'm not doing anything with the button click. - #Eventually we could add additional modes if we wanted to. - def on_click(self,x,y,button,pressed): - pass - - # Handle the event of the user using the scroll wheel on the mouse - def on_scroll(self,x,y,dx,dy): - global activate - if dy == 1: - os.system("clear") - print("Welcome to the mouse controller!\n\n") - print("In order to ACTIVATE the controller scroll down on the mouse wheel.\n\n") - print("In order to DEACTIVATE the controller scroll up on the mouse wheel.\n\n") - print("To close the program, left-click on the terminal window and type ctrl-c.\n\n") - activate = False - if dy == -1: - activate = True - # TODO 5 Update the appropriate class variables and publish - - - # Handle the event of the user pressing ctrl_c - def shutdownhook(self): - # TODO 6 Update the appropriate class variables and publish before shutdown - - - print("Shutting down the client ") - self.ctrl_c = True - - - - -if __name__ == "__main__": - os.system("clear") - print("Welcome to the mouse controller!\n\n") - print("In order to ACTIVATE the controller scroll down on the mouse wheel.\n\n") - print("In order to DEACTIVATE the controller scroll up on the mouse wheel.\n\n") - print("To close the program, left-click on the terminal window and type ctrl-c.\n\n") - rospy.init_node('Mouse') - Mouse() +#!/usr/bin/env python3 + +#TODO 0 Update the header below with your information +#Name +#ROS Enabled mouse client +#Activates when mouse wheel is scrolled down and +#Deactivates when mouse wheel is scrolled up +#last modified 8 Jan 2023 +#mouse_client.py + +import rospy +import pyautogui +import sys +import time +import os +from pynput.mouse import Listener +# TODO 1 Import the lab1 message file you created for the Mouse Controller data + + +#Initialize global variable that will keep track of mouse button status +global activate +activate = False + + +#get dimensions of screen so that we can scale for (-1,1) in both x and y +xDim,yDim = pyautogui.size() + +class Mouse: + def __init__(self): + global activate + listener = Listener(on_move=self.on_move, on_click=self.on_click, on_scroll=self.on_scroll) + listener.start() + self.ctrl_c = False + rospy.on_shutdown(self.shutdownhook) + + # TODO 2 Create class variables to store activation status, and x and y position + # Initialize the controller status to False, and in the center of the screen + + # TODO 3 Create a publisher to publish MouseController data on the mouse_info topic + + + + # Handle the event of the user moving the mouse + def on_move(self,x,y): + #We need to establish the use of the global variable for button activation + global activate + + + if(activate == False): + messageString = "No movement sent, activate cursor first by scrolling down on the mouse wheel" + print(messageString, end='') + # overwrite same line + print('\b' *len(messageString), end='', flush=True) + xScale = 0.0 + yScale = 0.0 + else: + # Use the dimensions of the monitor to convert output into x & y between -1 and 1 + xScale = ((x/(xDim/2))-1) + yScale = -((y/(yDim/2))-1) + xFormat = "{:+.3f}".format(xScale) + yFormat = "{:+.3f}".format(yScale) + # create 19 byte message with x,y coordinates embedded and print to screen + positionStr = 'X: ' + str(xFormat) + ' Y: ' + str(yFormat) + " " + print(positionStr, end='') + # overwrite same line + print('\b' *len(positionStr), end='', flush=True) + # TODO 4 Update the appropriate class variables and publish + + + #Currently I'm not doing anything with the button click. + #Eventually we could add additional modes if we wanted to. + def on_click(self,x,y,button,pressed): + pass + + # Handle the event of the user using the scroll wheel on the mouse + def on_scroll(self,x,y,dx,dy): + global activate + if dy == 1: + os.system("clear") + print("Welcome to the mouse controller!\n\n") + print("In order to ACTIVATE the controller scroll down on the mouse wheel.\n\n") + print("In order to DEACTIVATE the controller scroll up on the mouse wheel.\n\n") + print("To close the program, left-click on the terminal window and type ctrl-c.\n\n") + activate = False + if dy == -1: + activate = True + # TODO 5 Update the appropriate class variables and publish + + + # Handle the event of the user pressing ctrl_c + def shutdownhook(self): + # TODO 6 Update the appropriate class variables and publish before shutdown + + + print("Shutting down the client ") + self.ctrl_c = True + + + + +if __name__ == "__main__": + os.system("clear") + print("Welcome to the mouse controller!\n\n") + print("In order to ACTIVATE the controller scroll down on the mouse wheel.\n\n") + print("In order to DEACTIVATE the controller scroll up on the mouse wheel.\n\n") + print("To close the program, left-click on the terminal window and type ctrl-c.\n\n") + rospy.init_node('Mouse') + Mouse() rospy.spin() \ No newline at end of file diff --git a/_images/EdgeDet.png b/_images/EdgeDet.png old mode 100755 new mode 100644 diff --git a/_images/GradEx2.png b/_images/GradEx2.png old mode 100755 new mode 100644 diff --git a/_images/GradEx3.png b/_images/GradEx3.png old mode 100755 new mode 100644 diff --git a/_images/GradTrig.png b/_images/GradTrig.png old mode 100755 new mode 100644 diff --git a/_images/GradientEx.png b/_images/GradientEx.png old mode 100755 new mode 100644 diff --git a/_images/Grayscale.JPG b/_images/Grayscale.JPG old mode 100755 new mode 100644 diff --git a/_images/HOG_Features.JPG b/_images/HOG_Features.JPG old mode 100755 new mode 100644 diff --git a/_images/HOG_Histogram.JPG b/_images/HOG_Histogram.JPG old mode 100755 new mode 100644 diff --git a/_images/IMU.jpg b/_images/IMU.jpg old mode 100755 new mode 100644 diff --git a/_images/LDS02.jpeg b/_images/LDS02.jpeg old mode 100755 new mode 100644 diff --git a/_images/PixelCont.png b/_images/PixelCont.png old mode 100755 new mode 100644 diff --git a/_images/RGB.JPG b/_images/RGB.JPG old mode 100755 new mode 100644 diff --git a/_images/RGB_Gray.png b/_images/RGB_Gray.png old mode 100755 new mode 100644 diff --git a/_images/RGB_Tuple.JPG b/_images/RGB_Tuple.JPG old mode 100755 new mode 100644 diff --git a/_images/bot.PNG b/_images/bot.PNG old mode 100755 new mode 100644 diff --git a/_images/callibrate.png b/_images/callibrate.png old mode 100755 new mode 100644 diff --git a/_images/callibration.png b/_images/callibration.png old mode 100755 new mode 100644 diff --git a/_images/camera_mount.jpg b/_images/camera_mount.jpg old mode 100755 new mode 100644 diff --git a/_images/clone.PNG b/_images/clone.PNG old mode 100755 new mode 100644 diff --git a/_images/fan.jpg b/_images/fan.jpg old mode 100755 new mode 100644 diff --git a/_images/firmware.png b/_images/firmware.png deleted file mode 100755 index da5b03262fa98f7161f864ab9aa9cc8c133a9466..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172807 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
  • 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

    r*Rz_rotMNRpOn0@ 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

    p2wP3!K6-z# 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 diff --git a/_images/installer1.png b/_images/installer1.png old mode 100755 new mode 100644 diff --git a/_images/installer2.png b/_images/installer2.png old mode 100755 new mode 100644 diff --git a/_images/installer3.png b/_images/installer3.png old mode 100755 new mode 100644 diff --git a/_images/installer4.png b/_images/installer4.png old mode 100755 new mode 100644 diff --git a/_images/lds_small.png b/_images/lds_small.png deleted file mode 100755 index 4803dac5bc3d53d34d04036d6a6f9905ec13bbc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107096 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

  • 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�NiqSlf$>OYQ(^Ky3D-;d?qAql}o2Puft#+)-FTPd;0t@uxIO z0mc3w*}1h98Logqc63 zC$Fq43wcZMMl5>STCz7?#cG&M1tpX+kF*FS8ls<&!SEdpu3biI*54tR3?1 zISrE?%UJ)xGdd{AhSIlp_m|V-bLGO^dU^cWrNP{+EUsGG`;?c2(}eFw^`UwxPNjj)7)LJgxpmyj^}z<8kE*!O_k z<0cNgrtaMdins8)>zgQwZwAP~M}Oxd<>PyzeX&FIIFRztJUGtLp1<}nWKA#e+nt@k&3Mz7E-qFd4 z&j9&*1PXtJ7dkmknmc7AKE|`-86#l9Mwnd>>!fvj0lZ@LB2X&dz4=&iDPMERFo1L{ zzv7cR0Df6dn7@~NNE^Ie=dB5a$7a7OjuX=HfIoB#bU~tgYLC#2LwVpWJdHrS_3QzH zejSo2j4RqXeFlaD^-0Sn9UWX}n@0O;FKz1VF#s@n*-pJl9r9-B8D@=o+gr*I?Mv!} z&4|6N9c7z3xxwC!(j$YqTllzh{B)n)aIG13?`p(o)xI*>NyP3F=Qw5e|C#9^g*=XbxW-1CO} z8n~_o*K9nH$fH!1V?~zM$N%`_1#2?<}aE{%tDHvoZF#nD({_z3T?4h-3VK2NL&JNbYcwczc4UiEYhxbr7q{W)$PCnqKyyPA9#dBWfM-_$=M_`HL zJdqQRGBGT~Q{l6}(#b<ZQkS}n12>~T0qk@cit6)mBe)h8u=svGG!T@3Fl*X=6 zTbuCo2>;F<+sfYEyUOlo#Q48p|Rrh%$EF_CFAu@W2UV&plD$4usV5OJKWya zZNq`r5C2$FZ{o=(Xv8-h6$j&=}l-bcYw;?m;0mucC*fT4^2wSLfZ z=mvBlOB%f1+EGRZhsqDW=l$j8+iq{j)z#pdj0e`rELd_=KrH0*ljqCteDwFrr|$o( zl`YmE(>anHPqWk5yoRmu)Dj{Wq ziaR^SrClMEbYSU$fin4@1%6ffR!Y<%aa6V V3sA8B0~9q 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%0DkTCk{CZ7qQ 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 G>VE)Abg16| diff --git a/_images/lidarscan_example.png b/_images/lidarscan_example.png new file mode 100644 index 0000000000000000000000000000000000000000..52152338e1c386580c27159472b7fb72dc2cd7ca GIT binary patch literal 29179 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 diff --git a/_images/ssh2.png b/_images/ssh2.png old mode 100755 new mode 100644 diff --git a/_images/ssh21.png b/_images/ssh21.png deleted file mode 100755 index d261c4735ff9e203a4fdbe2e029889446857b4d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17145 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 diff --git a/_images/ssh3.png b/_images/ssh3.png old mode 100755 new mode 100644 diff --git a/_images/ssh31.png b/_images/ssh31.png deleted file mode 100755 index 97e6d449788a52f2e94dc7c6c7336b91e872cbd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11964 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 E7i`jj6#xJL diff --git a/_images/swap1.png b/_images/swap1.png deleted file mode 100755 index 0e63c08ded44fe5087271ca5a6d7c151434c57d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14852 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 {zPSXL1;Z+c!oP5@ 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 S`T93b7)dcX(TcD70sjkO;M%YN diff --git a/_images/swap3.png b/_images/swap3.png deleted file mode 100755 index 5b68fa3484c2a247f1087d70e6db20d081d59d59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10295 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 diff --git a/_images/swap4.png b/_images/swap4.png deleted file mode 100755 index b68a4ba1dd04d62df7762ae5be436c99c3477b0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20284 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 BFu(u+ diff --git a/_images/swap5.png b/_images/swap5.png deleted file mode 100755 index 47474dddef294154373d40397c5a3e71dda905bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12523 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

    nPHiP5o)7%pFnd(cn5@W5KE(P5`Ya}Mge}5~c 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 Re}5~0{g70Ys1-8^`d_BXtdjr$ diff --git a/_images/swap6.png b/_images/swap6.png deleted file mode 100755 index 40e4580f081716af5e504e5ee8c4766d190a55d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12275 zcmaKSWmH>Rv^9kSZGl2@rxbU0FYfN{?he6=Qz-84#ob*}T!L!}?gfHNK6>vP-+N;` z$&X~8jO=|f=H6$mIoFzzN(z!FAMiiGz`&qLONpt#z`#ztj%D8=yuL^CwA;UaymkF5 zt@iHq1$bu`@p_HtCa&eC>S*ESY3yPSW9i^%Z_eOq>SAv0;A-XQb`IAg2m|vOMp{fn z%`5A4-O>wFeYN)jI(}B-`Sn0P1M*HwiALtrAFNPB>)^mI!Ly%~F%Pko&^{H>z(##h zC;v=TL?LWNik5*XP4yi!;pA-mku)D(iW2SNxr0&gn}Dx}#kuR`n(y9t4ES6{MDiW! zH?*(soK=syHemi4lAd`cup|95j1Na-v({iA#{xOo{s>N7?ApL}UZi$VJHH_%={k

    ZE0!FFnMALd3lAHXt;c>}K$AHcK_O}#8-PxB!B2G6rX+Y}yhHXPPXg%BSZ$bv z=!W|DPhJGF3L2LlaBD~dPl`mF+~@;%;*F=YB?eS(VGX{kIMWx1{0xoJ&mpgL9=4!c zLSd5nI8jLbkbuQ79rtH5yY*~TDiLKr75xIhOB79KH4|G@eL;DccmGh|GPcWNfd9hc z=;?*PrHN#iNz`_v?>|E#C5u>dXl87!936v2g2&Zsp)nG)^Q@rXxfW?4ze{ zo6I=o?naMx6s41CPamgd#_5TW)TBEaFBl|Y_=Ux&F!y3dY1%v*ebT@G#`*p+--uRpU4mlzw;pYf9z)NvgZ zB_WMhZ{xfZyFm*1Z^}02j(8#r*7239f>CwUsC7#fe`l8WksGY=kePq@(bk9d!hH=d zF5sRJdv10gF*AP0mvHP4eE;{Ez(iw<3KJbdrZ@O-LycIe!IzSN<9z-jXh&l-TZWf{ZBqGgrf6wK@ssyJ z1D@VPE-EA2YSqb)E$r+3=?{lte7F)5oQs33pb%AMlV(E>p6Y`%e)mdc3u=p@a(}|^ zjL5Rx*aL*4wnjsnc;u`nc8Hhc>_Efq_~C=$6~t)y_9x&+TVxp*GH*Cm}rvSd!9{RCWV!0xyMqs7JcJ71PT)VWFL$KepRM0 zXkCh-Qcn5D<`D~|LvXB#1PTd>d}dcbolJ1I1u zEj0P79^)^*Yj~QhO6y`PB>v=CuwzIpnD)s*AfyrQGYI8&E7g7+TsIE&rSq3X!^{#> z8f)=7o*#G)z$!_8hvMFsTr!VbH^MCUBtj$+2s6nSiw@(KH=j4ZIdgCFp2uC9u zOyMcGX-4y84=mpWowsu4tSseuxgHb@f{(kRjAlzr;FFxl z5UwJip}L|+N)eGKB`7DXp#iHQywAr31t6;Fc$x7<3?W!xBx)Z;)@sn+u($D)%d6ZA zLym(M2ER2`q)HzEcIQ(-fF#zsH!Jt0P4eFd`FjC`?9v4 zEzO~MTlzDa&j8edem9T|)f~s3QyrqI{F)WSI-F?1)!kf<@ywq!fmdRnM|zumG9 z=r)MHkNbm}9OZ97Ej3I3dh{arwufd6@n#!mZKKRT! zLr#%qDk+rcgu+#->7$6Fu?YiHeCER}2GQ!RcuAxE%`C#@+4)K{ohH*8Sd%x=u_o}R1+WVEGVM_oh(n7}yVyCx#zNbH z`eVI%lqi;pnkpFX<1gjh8G&MmtQZauMxurQodr%R?jPQy0oMoVA9(Y;R_jwt4gy1B z6J9skm7I+YeqW}%^`Y$tA>1C7s*?-MbCKvmXmn~hrcbrNse#)2j0*x^Q^u~ceCQ+9 zO#o9eUr6&U^2x;UYAt$`#T(nW=Iv9JAt&X}SBwh9D_>Dv*c(tdYvDf%w5Uh^B^hEt9SI9RIoEu|z_>L=v=-vxeeJ8?V=62^fC#1I)SPIOgDjy^ zdI@fF3JQaMXCVxu!7oKv(U~~blImF%rN39FqSpBWv~yl+_;C7FF3Nr4zmiD&j(=P7 zg;Py!cCjPTA!JOkS?Q#;`%`G515jl!Hdy9i?__lgube8AVeO6l)SwzoP{Dvot))#` zC!rMXsjSXVwvmnrZh_R;+dL_~?Owg{u%GGD%V3w?NjwnrvAG!BSj_V`(-6I`l9n2a{s1*CR;HMq>&zPQ~zgJ z#^Ne1HMRv)Y&VRhk8MBhU0@xibR%_kWD{~CR<{qiscD;?5bY#NKfANhxJKu-NDUTx z@LC8>n`B9C9Ei`JolMNjn-RMX>THCKtVV@M_Yoh;tzf@0iS+M_u7_lCKIlC&VT&NV zG$d_hHZ{9Xlx|2r@)Nq(;#yC~5WuGVDv28(zq3npNMe`S3zp zgxT7_JDQ;j`YSVwi+Bwt+x>^4CwE{0O8s{DHT~q_mw3n7tDYCg(FJF&+trY^Ea{#W z+Ep_&C64uPkU~S{9NO$kI(xQeKg+R5D8(e(nyV5d(H*E$XZL4XszdN~BAt#Le~X1l zB+h00J&~jIerwu<|Jc8o`6on}b7La|@o$oO=bY(aJby7!W>yIy<76W~mx#Pu=(>#Z zFGJgE&_C~!EB}l1H?)=-m_!RXC(SYx<9?wjZIx|19x!M#CewJ=PD=l zR<>~}D(JJ=6UYRi5%&BUKvS)D${#^`KGzv=`)uHYwlZ7@7PP8JU>7>llT|A8Ci{7} zvBwO^yrvFaW#M??epG3Afq$Mw2d=Juac)VcyPD0LWcOMVPuK(eFZ8-|llDUxSmb-` zx|Mzp_w8wmf9XRiSh|W)z-qPD?9FLM{7qI5XN`tWsv#H-Z@s@*xS83I_*=6xiIY8Z zQs_QxI$P_C`+NH!qD_>>dA@AfU#O*$S>>YyG1}ALRTNo zS_%h~2O(=uM@cb3YVKr4#rI5(taOjYq>zkK(#?tO$Ltq+>5QT0z8o<#E%Y++vTRB& zw@+~&o9xU-JH%=|`jHpju3Y+upxv^No$?skZ>R+gSJA~c`h4+KIqyY?hRwscJ^sx5 zEv4TB@(#zf8^9HJ?s?Z><~>|EdWk7I(7)8MOEk)zQn;E@dji6fT;^^POp)Ck7cb{B zL%)hUp`d0=3zWz3ejF;$RQQftDM_=IwG{-d<+*v+7m29dkdF`F!5& z+;d&id9z-1-C`%b^6R&cCKoLRR@lo(^&e?=4=#9Ew*&LMSdpbEaoAs>vI)Xck z$9b9tnJrF3*8Nh8C9aqLwKtnCyboVKijV&Gp?z`@@CJ@)3H;DpZG}E)8a(E-dkb`c z48Y5&#QUq+;k9sSqe{@-D7`iA#C4kCqnT|@wueIj?ut+=7C*-Iv)`PCF01y}oL~y+ zD>Az5y8y-boy2eIQ{;!^(h4LU9t`y=C+6)v>S3maUHxU{RCRDc(5zk+XLtdGmYse3 z04IZ~4q})}%cUmB_^wdgH*V$I>ybMlePoV<6H!2K_h#A!BN+aD=5mKOS9twmF3Flp z@i*{dg6Bh$nPrbfj3I6=$2)kEAr(s`Q(pFA#}|N!%o9oW3O4w6#AHn73kiO$YeV*m zKyJ}=*HRT?LSJ{H{*72K`a)I53t~yUm-yqJKIKvV={N6|d)Y<|KJ6|S0=(WFX2+Zl zcrqbj2J{)J&)ImyKnEs++k$}QU~E?RVOmxgPNJd9JKBwa8VZmG)8`c>6p0mA{}q0t zatYAlC|ZjP`(yQFI*|>?9WvHxk0%|z7(fMf!Q!j-3oy7~)j@4*K|vmbRNuZn0|L$j z$6O^Wd%KisguW5X1ZBw?phgK+T`Ie+V24^_U)C95qKG`jS-U$PeCNb6G}l&_oCUMqg4?M4coEzV4Rq-;kMNnb|{1;6Xrbb0f1Fj1MG^cqulsFZH+9H?FoD8w$KsbXZDnXQVKf4D7l z;9YlGk56F5%4qib-tn$o1;Uy{C%Lu%Tybe)dF$3*KI|eV@#+>@KkKSehd-WNM}3N= z5#w0mwX6{e&!BWgolmV=iG@9t0)Q`)M>fuKB8%G#BfWYvZ(Dn0e6DW-3kRNcM*@G? z`eHD9(aE=xK6Ea?C3m@Y*p>b_PAvRWKO|~Bz25%abNiF@?TBS z^nPxUddm$vA6N>h=XpHG^3Gl8Si7#pki_0qB{3ao5+Kcx-T>GI(V~ z;FHn;?)VeYZPbVQ#l}Rh29n*Zeyzq6>YHE5UPT#4EKkCuU*lr%YMvRNa?Y60{U=5 z;mDK2#RYIab21k4=l!T~mdq2b*wG8b6dOk|jzcEoRXV*l4fsB4Fhb8u+14SYEF#Z2 z#j9U_{V5Yy#*)rEXEnVT?HC`RJLhzi@*l3#TdR4NrO{5@C&;E+ZXerI+ggd+{aM$dZyC*#z{JYNYdAHaiXHNMl z&AO!x1rT>!676{arht~a8yYBp1^YqSggr_9QHhe9^ysOB8$fdzm5mH{xIex~?`0wR zarw!8BLN&n_)O_}D&V*R1LpzNb;WN)QCBr)(Nt)npBU~B?yX>U$%I8f#b@%bp9ysd7A?53M-RC`fD8ple%6sCCel5^1OqW6zpg6N-=VPXcyq|(Ea z@;M!ayNB!6)-0c86}RPnGb%LM4y2i{JJnKQ)OnJDRsaJvx=g33L3PE#JYZ-K5P^VxBMT`{Xh*VbrRFJpfh3ddwI{0dLZ5u z7tlNk(cM~zm{1FKs;4~(982*t(<`#D)5rLg18W{}2zikMWc}&XoXV#oxF`WrioPv% z6i-wu*d5K++)1{Bd_vEYQ4ORg3jDnfzwcvCv@tYyWr!4t$5i{rGFg+Kf8A2@&tdZC zSdIM_AJfJn?yp{2e`>NuUnjusK1!{LXI*rud_YK4{8s0y>o}Uu_*^r?A_7is>3kH= zkxTZn1jZ9SL(mdeIt*0h9u`lTRXe>&hF`i;P*}p3u?PlpZGoU&p7SRXI4ngQ$V*gP zc*y&xRoUK>nuEKdef4W8C@oqwmbZ8R6g{8okGw12JKF&-UZG>ZQ>|1>`j|m|u>0x< z`c38y#q&8Yt6{YyFa>+#<#%f}YkXjw@L_7Ek_IS(RuwZ@t4$F+@C$qN>E=}1eV&XK zD|g>J;KmnowKW8E@5A|%Y_}cIH&{3S-mo*mJokAt-Bmd&y2r&7TlL-}#*iyxWhQ{t zx3}dante<28P8^!RjNl%j_Pbt#O)nJ=;^^HM(ja*-+WLh3%Wo0S`9U!r7}H~}@6YJ@ zt$|AaLiqR>!Ue_iomU8VBxm}8%XLHH2u!KFKvPcqMu^Gcis@Y{ZE)mC>Baw6^glAX zJf9rY7ARAl{2s&RyVSY+McvRh7X2`vo`Czuh^~I-R4z16)=w{O3?lWJiDmSc>@9r^4xSOc@h~z`b7rmFrh* zB%W11_v{hi^}s#(b#7kEk9-&oYkZKq`56BP3Z2EX!EyZsf)ww$!_-7$82D7MxKmU1 z?zpyZ_h=q=$;i%ob{^J@ILfdeDQx7g07q&PBSVk|vH*WPIU1r8N&SSQ)UxAR|i_;5k$W1qx{=(slkvM*u2*E zEsGiHSjBeMtrYyx`~hBypX!5HA$6_8@?gdbTBnvQN4@Rgi+0dCQgXgW4%4UpyGjXs0H$mF`50SzxHNBsAlbafpIm#e6&JXn?mY85Ylr*%6$3>{X0_!paz`XnbQQpoRR zI!gkVdMXS*e!Mv~@J^>j4*jk!8^BJ~q@-u)QkCmP!%(t#Gzd-BcoS22dDyD<~88UwKqI1F3pD9FC4jBBKQOY-;E}pw`J9>S|zKba+5b zO`B(ZKwWk;FLcZfZe}uCNi$p~a0RWSdq(OsF`OP*)|(%pPwu(C=>Y4z6;k}(5CH94 znLy;hu8%`8jNMy=Ghny5C1iwY534@rF|rkvgn>cz{_6$c4UeQ9NU3LWLgUzpiUc@U zzaVVGNhmq5G9$#Uh}sB_QVTN8db%;(#H6`_47VhP-^TNaKKFk<-Q2|8*^IEDu(<&2 z#>y?JL_nYK$U^YV4c}2eSM(m>aQ>M`vOk>hz;(SzDp-snIn)^2!nUb*Hna16snM~e zJn!_Nlzq^AQ%hv*Fg3Njx14?mM6IP`NeT!qovAL7k+7p5S#%4_1cDRR{+?c~mPALr z1Y4o5f!EXPe%mWwXVRp@!+Jrqq4yKbc{-}95o>4y1qVwBvo_cCUbpLxVt<9v7CoM) zG|HXNqa%zFDVYR@@@J!OC=k5J;%%?e9dLc9LC2{-1O&o`o-`faP#1h*Cj=mERe#mF z3lLjcqc*G}KUz^u4R|hBaAKlZy#o?R&uIZO_Y}P*>19977)@>gt(VlyD)99 zKmynqwxSP#RqQV;k3<$o7R5qHWJj0gRm4x`W^%1!V0Tf82XUM3eI$0qFp|$k2i&w= z&!H*yHWw3nL`$#7k&q$1Y#=;>7ZA~vIR$R_WF+cX+**9Y$Kryn868P#;0bIgLy8iJ zJa7#Ji*K1Q9@NzhuJ%o{+@eP=$`_wPL*tH~CesSUFFbyI9y!G7Mk185FrwxZ=0b}e zbBW|buYq;*y&pDj8$4Es3;zi|jIA&kuz?deBs4~R*$)1gF&}oe7ZwfMJRL$fK`$|p zp&&;5m(i`Cr$NkUc z=c$4?+$JleFeb0uuVaXRHu4B4#=TRebvdGLnshH8*lMO<(lxE1a<&pof^iIX#3A^A z(RM8&GShx6^cn)zX{PF5`(9GDrsS^NZ+~?6SGcJO%9Z!IaZV)?X9UVVLAiREA|(d2=pCpf*C-6&4?M2E6O4SS z;S(Uv$X;7xUQVdpvlHed$QZ)XM4T?vV72wZ@yF#3vCe7GYP_Mo(69v1VUt?cJIU-zYlDZwB9EdwhfJ850B;sA3}y;1PL&K z+OLy*S_sFsfshPFkNONxeD6KJknu9_DJT|memH?=Co@kd?x8ECtojd6}2)Lll5a8FL|wV_(1QEJdg^J(&O=&>f-eA1L(kJp@_CgAq{ zc5J40Es{;AP|~oS`AMhglCo3si~8rRDD}K2Qwr)+D6)KeQ=xHP;?tTnVJ6f1EX$OZ z(dRQ(V(g;TNv;?Oh>xE*Lw_xs^oqZk5anyja@|%`bSCVG^$E#6r2zC;v4+_3N}9_s zs|nL27M{iN(eWO-XZK&)LOS>$0W(^|AXXcC+c?A4d+R8@ZS(yT$QxuQLW!On!z)lJ zwu%BmUF$JS$!WU_y!>7e9>ktlbZTq;fVBywn-QPpQ|WsHzaLjCP1m9Fcq8NZg@=%9 zH63#?=bim2ttPNmth-Gd?tI*ldz2}SL0G)5VcTBucoWH$O6CwbEyC!|JrOM~0S03i z^+h|2=`-xFuGbz+D|Uqx$m7(jC5G!``g<|LrI9R!#5?b+qQ~*$*sJUlN5H#hy>ONyC@!TR%mv~COZh3c##8B=pRWwS zxS-!xHGLfveML2})bl{Mac_+G_@(B>J5iIK8O<|NQ>6VAFkmFqmAIY%*QNE9=AYi5 z7^9g;-T$q@{%JaWLH+*@E=1{!(0^w93VAIXc7s=psF9}NSuLG7#&3O0zj^(Vf}($o z&`nKd=Su;Hep0>XutODz%(vrf%JD`L+6h%jP!;O@pJmh}l!<025x=X)`TYS%+*C#$?*@PPPk~mub|V<2n<6QPRbvse zL^62ut?)$tBN;P*E?UD+d{M&xQkL+?3&m^CkAy9)>~aegaGx`|!ZMJCiy3;Rnv-$8 zE-mwGf4godWI!z}#q?2$>Il^W!Y>t5PS5laPb?Sy;~@V`cpz@!RHrh{iZ}nJpK|uV zW{imu+Fg%Ol8EeZ;)9)|M%I{(XvyU%ZjS?dQz6vsCeU#i$GX4ffo-nd{D=R2p&Qtr zNGyC>Whs_;3!J^=XdD=nrqlU{?o{Dm=_Urc8YMd;!%lej#t#R37Ytgz--eJ0di;ZF zcr0EMGe@o;vfneBAk^RfM%d>^mn|b!-Jdno)pV^VTb<4u#0$;1>uRQ}>+&sf_sXd> z6pf_*iF-(mF*~Cnu4i=eH`T`_?hB;uXNnkhBBf3*k3lYowQBM#MVIp&Big6V$RX37 zy~V0JJ=ZjKfEBYdap7utmRw$#K+>A+V}12+9wGU9|jBs z@rv0-OIMk&8uJ0-)vl$J$5;r{tDUAbjWm1xwFKXEs|nd`4qB*4soeo?YCkJ-AHrCP zWk-E`P5&G;gH6OhlFbgbA*ehy4uga-L)IT?Ju2)1+lh|Pp3AP}*Q~O~2eYSK#xQ)h zn@hQRV&eXriH=_|H+COGCS<)`on-gUwdB@{SvK2q+qxtYUkbkdj`90td^cI2N5Xjw&QWCM%1pyEJByofrlYfARyRS4xzL%I9m`=ypF~Y$mxw9P2+@`djr4WJi?# zhQ8Pg;tg3@t3589emoX|YGaIW zLZ#rO!K5&P|J_Q3>02%pah^>DPPs-t`Di3YR^1yL>6I7$=r>G7bsk;;J{+f`zx{h$ zs5xr1sfgH7FvUMMdS(aPu6w2vGJiu4KrPwtpnE^HpRUV58RoT4v{}GN{fhb&g|Vt0 zjovuC0B4F9pVb!hld)?dpebQ-P|MwAU}F7nrI>4x=xJ!(g1kiAZMI+03G9TMzsgw$ z@CxW&)r+tYFzi?4Wt;7JwF7Or42Wi)_ME;YnxWRoXO9i^e|bwag;S`J3sttA%B9b` z#tA>_lvtD~j(9Ji5cN@uR<)rSgKAISWO7!Y@IA`5lM!|Np*Ql3jr7xTaG7m;;t{4$ykjQR1M>@GWyZqFDJ$Q-mwHEcavhR(J<@xQk4UiT!^7g>vBq3e; zZ%zu2d5xdng^p$K*>sl!xTDNE2#WE$MN)k{KE@%Bv}cB$a!Yt=L==)=P>E zv=_a9QpY&9e9m;Qi~b+W`vyX+nYo9(k6oA^xs58|rrQH;u2_z24d)430}x}tg}XPf^@6(GXMNa_$MP0utkw^cF?)|pq8q( z^|z+8N%Q+f5SjY0m>-o_2I(tO(%)e&06iPb>{$o&uw*89)XgVd%kTx1vg5D z{u6Q!s9zYd&Nh_Fu}im)u8||2h0i|G{zIIIaqG3T{#(iV?CcWxUuzDrFe~V<1@J!# f+5bVJpr|}C&s>RC9TcyweHdwR1+nU{M#29Nt9WH& diff --git a/_images/userbar-account-settings.png b/_images/userbar-account-settings.png old mode 100755 new mode 100644 diff --git a/_images/wifi1.png b/_images/wifi1.png deleted file mode 100755 index 41fc1bb5835a0997475dc2d09884ba4daccf3c58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34443 zcmb??WmFtZuqaUo5)vGO1b27c;O_43?k*v?yGw8ucbDJ}!CBnhebKiG`R==a-#KqM zb9TCWrn|bjyoz8MDPaUyELbQgCnN_cv{Sw4O5=qh!)Nt)RHbkOd#*7>gO{ZCmfxJUfUaOakdFVh9u4|yx{ctQ1E zN9bn-DXv@7($C8$o*A@u=e?{vXNuZjp(}88``P}E_q}%En7`|}_;b#zH&}lE0$G|X zi|11Z>7Rum7n}aW=!r*Ng`$whHlz0K(F$5M*IP^lX^*qS!m*y8nfFSn2Cx0OjR2Qj zfReITFCRfM_1@nW-*mzM>sEq? zXZ-i5LQnVq^0FhQ(aQYZ!@9Hg&3bcN@lCw;K>fd&pympD=HW*d_Z0MzSc||#L9COXPv5fpXz-90FUE6{-%A5fzAKAaJZf5 z4L;D)K3it_2h}O~3*fxVdmnHw54d3j9GIkjC_XgyO7P+XY^smQ&3wCSvc82o!S+1W z#Cr<9e?H6vavG#Q-ZrmD;ys$xKef!?Q z@)fGb!Qn;PDQ?^OcjH^6zpMI$G)5smfk0bho(J`h=d{lur>X37MuQYNzb=vQ@K-Do zZP^b0!tx8;*~6laTr6Ru-ibaMrFcE-wGRh4 zMbu(^eoWqZzD93f3xB?M_S!KAoWAisS!f%n8EJc>Y+IMqehAnBJe2JLS`DA>qqU!C zu_{OAk`s2>V}v4LWJAGnLvp>wJUU|)5>yVE5)-@Bf7i27`jn#eq|X)dXoieGd9Z^X zy7D|+u=gBfnQ*Qc+oCPkt$*Fuw%6Kv@~D0m56oXVRDasVdnD8*7`)~`ap^nAa9Q`h zztV2M&h$z?k56V=XC*}5?lI1EyYjEscHe>4-gf`2^|X4w?0R%W+j2K`f%iOT?_o{o zy8UM5#-jLYAFh5GJ??S~@o5#VyiQG;`&pT#$>il+Hc1Dph45y2lt~6GlO8LL+wV13 z?)63Q-+3+^8fy{De>=72xd)N1oNmUJyRRpDACIrxcX&V0dL3y$9$P=EU&Uuz{UHR; z2XKKJZR<@6i(-v)n9G(vUeqzsmV_JFv%cTQ^|}XvWh4VUnuGELdjigp z9$l}wD_%s-uSP0Ng<0cYn8s@(xxdd{kqD)Rv0Fow*yne45Ydy=|YFGv>Xy_q@8V{@gV>GUUC^-5K?j=jJ~1 zd9O?Ru5*W9Ty22Gz0bWpfYyTR@w;-IATeT%4fi;oTa=dP8Z?M1HWX_>qS@vs{p5ZX zy63#-rtKWXSp4jK<&muCvCKNWkCLj%m|r)~laK*I_2)?lJSH2igNf67+X``@d%=KPV(Z+o=XK@hY0~Ftmq^e|z1Orq z>D=P*@FWfH3{VL-N%?FxjSpfrB~=sQ?{WSrI9%hS?0tm% zKptI?;YK`{MJzW@iOwcXZCTX&ub&I{m6Y^~BjeIIC2`Q~J9N=-VB} zYzj1N{vG3N|9K|N5FOt;O?K)t-T+&}dr6nQ9qPuy^Ar_X(HT%E9(rGR5Ne_m@;^;> zMd0;bf0HW8v0}vTi;+N#rIDIp`xWi;NP(BK1>^lrJNVhNufCaiZ9+!W z9@v9zBszqHJe*hCt+c9;Lz}FuvgK#%GVLr_u5_;^se<%E2@YDJP$@L-t88j8wfZG9-An)g?)j54UnsD7{XjNVkLe{0I+U6!&!0$tUNY} zXWq3tY2WVvem*@`KSQ)G*g)s}vFrIN;d%P3qrPQLvfTZ+Z~1Jv-V@b3{kxlG4I2vG zWsG5)B`%zadq)jhduIz6Vj%o3x0?AJFp3ifk{XA3%?2Hf9!+dK+zLZuYuBcSd0`Dr ze>xT(Me#|C4&e_*Y~FuLSvGIf9#=Y4{_-|i1<28yZwdNZ&2B+m(1^hqZa1a_C3BKTUY+2zKpib*J`LI+R(wj%nMb5 zAXiBVBW#NXZp?h#(+EhgP8~-CE6GD_&Ce@m)Yjm|J?(G|oo74n)1>yTcyy}v>C^np z;@!prBk3HSyAWHm2c;ley~If@Q3oAiMl7J_u~!)PwKcygy;4`0%Qj}n!dQHTKb2{c zAk1r0qdsz}V2VVd^)N<3RHvyA3duT&e}!|}tGH2R_=t5Xf&`MML+KqmZ&{tY506ph zseG75=y8fpJEJ&`^>hZQp1b}xNt75HGE0ch=Y3v>{D84psVK#Z_3H9AnQQoO3{E_k z>($cZ1P z_loPewAc83-j=nM_ZYm=f8gcHfPi8D-i11sJNv(lkXQd79G(9sujqfhtkK|k&+^!4 zzZ22)pL6m}qs1i}Fg;MJUcc-8zg#l~y(GtgK4;=s+!a+kw^+v!(DtNDtG%I6zeH=c zhaFR2*TcF*_ZMOjv?LDr;3gcg%uPQOUU(8HYTa4B}I0?6?SPxdW6 z-L7Jnz~yA)h21Z)XxGh;B&3E_{jjY+B9+FBVxJ>F;j}NjsnwA`|L_-xQQB~qVL#Ie zYxnAL9p^TUBKIM1nT<=$Jt_taWruB_tTmrtLHn{rk;Fvw2lYH)`+`TT;H1HL&r#@27!ZG{^fq6X?Hf$;iQxK~d7 zR2N3VZ8%?@xPYhbon|K5ZG+mM@L3gHq*MBTz-}Th)u0U-zFjuYdrj&$5;6(EkYVri zI|#L=%l|Y-!yrzpvh4nqs`{<9uFa)R0_!O1$tAuwMYwxwbA28MS%P!{8WQg_ZeP(~ z7<`b3T!za6ZUk!=ma-SJ-DaB&Fu5A~K7NtYN9;|+EmZWpM~soSj(SB7%>fq&{q(E9 zRo)0Hen`MvNC@L?eFB+v8fJy&jN1d*I;Fu-iI?@-`MCa_wu%okX&j%UCD6#=C{SG_ zzqW@`5qxviN{>F~PW#l0f*rbAcT+PwyNLawK)Myv{G>c2vSY&IvqO6xUZIrX5B<#QI zsVZwA;8*D=ewd1N3f`+*QqQ`*-HetAhV53xO&y|QL*}WYEQd14A|uRMI)O7~ZO4l7a9$q(3bpr6su|a%)>1pFO}IT@ski&mzq8iRa2fj9r!Qb zl_j$u@Y>XU5t?>qs8;|613T+P+1vmwgg~lK#%N6~mGNN+j4L=Mh%&_s0271AMCl$& zAWya=%~Am{fv%I{g`ZHPSLs7L&CKFcv)wo!6ADWFG4=`+b1}G%!C`(SM58dwFp?8z z(i1_g#cH_Nda47BL>pn6-q%Zx2y%%kgV8%aam8jI^t!4tDkfMSRY+I@yB`!wzAAy-hUFO(;2~eb0g%CBHxt$l39d}qa|~xbIpY}8Xe%3a zP^BGuv7Soakdl%_N8ZDs!JIJX+-^*+|Lp?Kj+c+Q{rv>0dQxBF&>pH%2Fps%xrAlZ4crKZKP1|FW$Q zv#_dRj|HP}S4o9swgtWC?;mXT{PPqUmGC8;b!BbhB!<*!%@cU^;S|A)V(Y-$;GJD# z0!lqwZCu$(6gU*<^<9zWZ$f&Gpi80y14k4qadW@t7@I)ktG_h;tMOM&AMBiwQWw?s z`Z%e6E{FbMe>LOl8~9|T^A=vms6cNh-L{QhP*o78=jA|%Z63hvi*5f;fp_b}Bx{lr zA5PMpc*UrwO1BE^Ohv@Xcm9N zZ$1~HravsLtuG4F>XN7FE?_FHMb)68^|$ENBtqK+m0QKc)83gR(c#EVkj7@;m7F=z zjOJ%bk6kxuQ$#HWqrzeWVgbr&PwaObS_8ZWmg+xS?}2bb-B|iK{TI9jg9Ms`MHF&p z^TVP&mZq(zOGdq4`v0Mpz$zZK`p<@;?>kb&c{K;qo)`rBizN8 zB+h-Mrussk=U>Qo&W1*-r9U%9mWRHWIN?10MS1qgV|Hv4jv}!0m;F7c9?CM8!lKyTK3hGzLcisOKF(rw|E%b|`fPxAhTEpAwSi}3*u0Sh^)tyN_ z>4?C}IWnM7hx&J1Z2tm+q*p0301s56$O46uLtL3#&@-zdk8 zy{1BhHbeVly$sn+1)6}A;i2SlMDw})&;?qlQ(}O!H47Ft&GZP#S_@TvDyJDNao)-( zVTY7;RsE4UvjQ}tRK&-fx|Jrs&C48@jGJ2n?^zg&DR&=OCnjwY1I=f$>VW=<70_~% z02bEDCwhR3&2z?}bxhp3IPKTQvqI}(+O)h)iRmkvi!|KS&qP^VSk=2tu>85TO%_U& z;5)KOEb$GvL3N2*9gg(n@brVyoFy5jbv~(1b^~h{k5)%Jhe*!X;|c?(+0th#O*_@( zF{y-56dn|RC zA2uxPC&4Oag3sMr%LclI*1ojsN56%X-ZL{Sqh<`=o<3P{V1;*Muj|-c{@Pgh`Js?} z31Nge6u9?v`;7{F{Y#j;2&LXfcaNyXbcJ`~DVWw*W%i-RTGA0gO)Alf^cz-O!*)om zfMwC4Z`K5v&pk{>jYY`9(7>4>lFzX3?3)9ybD|4DAlY69P)9(oo-sYwm&i_6+1Xb^2+{;Q{{Q z4pz}(SbN?PVupfjrtv#ILApZvWZ}QSy)q0O$@z|r{mameSKr&Ke#`I(<_El+M{zdQ zD^tbl?Ka^+&Zjp8x*J6-@iXL-*|NO~%*$`bL1<30!+>SRl0;|F;7a<*xR_7cevfipz7fHmt_>an&Sr8mabS;uWJ?5_eT-&mV}`S@9*Q38Mi_Hsi!E#s ztvgTC#>0i4LM$++jz6t7JfKI+Kv1!ukkKp3VN0Md!iQ_j$Oy4_>Nnr;cQ%=~P5MKd z9v_bga<%<}Iq9ow2gQQZ%x&#+9U@isQsnuQN4VpoQw$9d(^8D<9qbVB-97hkn}(ei z_ax(l9qE~OQg#C0Ov}xCuB_T2Ci}n?e^fHHCO&!idZ95Ewm?$A!CEJB8XD`lr&udA zg?S9L4N`M88Px3cY~U2JI3l043ST41n`oHchM07=YqM_-7y?KfuNXd*zy_+y9D(?? zxx0zIRk`PYT?)Kb5-$5bF>`z4Z#=$fhTIa|vzx(Q71p*u-zZ(v39MpM9|vZvq@0VI zeS+Y1BtsmI4wf6$PGD6W)6Oov92Q>>er!3^)gWZ9$xv$7kivz{qkd&YG2%j;>*T za;M{=jWui^FZcIvdT8&DK0e$iE(VQ)ZE6%qCsn)W;A6A4@}X69qfytJd6}B$P2m03H@7HuD0Mr}&yZ z;Xk;3<(`##)7g38v^9qCaKS^QQS9VPGhBEvZo-ky?Lzb>>DDb-1LxOw!&4B%SLsC2 z6}Q-(?72~<_nQH4VjwJS?ZKmJ^kRy)bT0}~+j2}B9q({gVHotLl&JE8*GqB^!KZ~i zA?@r_XT!1!#G`jYU9X7;J9Q5~ZMv{%x>t~>_df((s3prK`~FaJwJQ8Y2iNkw_J9ma zes7kMvO~bVb3<90C;lUWL{%N>VJW3OE_npW;Ka+xJ_F0!#DnivgayU*!NGOdOsFr3Mb#T$)SXrDmiv17qg_}rxR3R zY(_DIvWF1N$la}ufpQHLcTzb{Mrlm}W|#=}3H|RB7cVOb1M~Oie=slbskEDC@3EW2 z?*xWlCJ|FSwt1dNt4syk#C{O;clg9^$wK#V0;VHLrA|T?IL1FJBtEli>mdLzd(uqN zR`T&v=v-d*#mH&GlpCxMl=LSMEvauaqIH*4bBP%Dj8-|2D+an-4vsachGx3j;j>5& zi+NOwxlhPOdC^=mV zZQkTBFliiOg1*NFK}e7!)398e*HTExs4_c2t?(X64oy~JDi!T<4CES@P-d63OWb#p;A}Zx1`9 z#b|yto-b?yI#CC9tY%+!qCCP!tL2a^psdkac)un&NTw;or3yfED9l6cAWc-PZ<=x7 z-}~9J8S}{NNRDJ7ld~Yk^>`#+NcrcXQQXWfz^!FWwipN{>ob+$Ct@3FNXpw&`WF&P zb+)oxWNpVh=woa$#CA?8T))08hGulw;L52pPa}`RK%f-i#xx&`-2gcpcgz&{H*6fB zlSHo(1}eGY&MkHU4o_Rewl3bkGF!E-Rkk=l7!ofCMHiH=}RE^@GxmF@)%Q8 zKIKUmyD?5mc7!>oh=_PSorabNdZ6i`1In3UD5iE0WgT|z89hY6mS^=2)MBn07|Kzq zq%-dmxoT}>QN3N`3S}Cv7O@DK(pZcQ%Sai)U0vi8BaC2`Q$bDh1=p;W6yMH=xTEjE zfO6(jMTmb>c$U;|^_%AS33n-O${nOD%CSlH%@2Q9VanN#F}N?zgot0iI)llv;eJ-L z=ALA}@o^bBjw{#?fYj+L9faDb&UwMtt~G-fzf>8A_p!3xh|sTLy1NzKLC0gu4d-3L5fM$zEuG;E zg1S1dIv`4|59>*t8z@GTYcE?r#jUH~VRrA0QQrw!|Q`mbUL@K_$_`-Lz&sCcvel{_cD{P6aYh_2n$G3?s^6_0%X0~JzydbSV;P`iO`e|vDsz?zQ{U}Y1zI52vbM3!bED#f)E31o&+gIF zHj6tTnj)&vWU{!RM6eJ)tL#CQzUrCCC@d)Ca3Js6ZC>Z4xt>Gm^JH>JEU*{9es@Aw zswtdCTsgZ93oJNPJAZd)@H`w=w%8Ia5h2S-K58Vyzk{Ucb6>ru?4g7>vpUIZHw(7- zMPolfAzrgNlKr}C7$DlX38Z4Di}PBM7j*lQxt(D1Q-d1}pJuFuxuaTf2cpv#h@Pr2 zOjA9HsXN;U{x|fLrUg2-ykM8<RTTHqN>=dTcB z?esaiAuIlzSduWb9B5@xOq@EN^wh)=+dda0eK)oI)*%8mQ%kXRbGB15Kk#j}ioopy zl#=JwPjZT?hkT1x(0x2ePu8WO#6VKAc&Z^ekGO znzTOS$@0+=_$?~W({q-=kmLWeBy@D~uwH8+eg3lJ8(+YDWp~)^DuHll`U*O z#gFX^7DBqX0Wma8F4qEyF*QCj>$C`8-Pz+xz z4OW^xi7mpwTGddh+R4+HVqq@qcrvUfZAP-dQ4pcjVBTmPGjHI$gbDt7OS_E6s2ps+ z2shA+$$y_42JK;SED%#)XG$5<^J~?DkT1E3NFI6WS0W;9)M_+h)IR&#CUWfo#~rD;<<)g=Rts){)E~BF=fXIKe8oRR`ha zhQUVXnq|=hje%6}GNTHJi)M(3N5ysx@$rp4Di$Zx;^IG#ezNK@rh$mQ~UXM zCz|`}Tb?_Z7ru9ysN)Mv78vymK?K=nDp4dn0NpB-TT{bw35v7FJID~* zGf7-XAt)<>Z(9>=SYU)_dLyS+4HES%9zre4$!g}ZuQ3RTyLV~h?YEN_UZZJ?V8a>E zOl5$>9ODd^F>;OTc6J8Y3OMT-_Z#<#Drh>10(n&3O|floYmwI#{1Lb0jhUw6#3BON z#T3wHjbmRoUF+1=MA2d6476s$AorImT7_sShXnP(`;6dwmgQL~rMlUxY@Z>q-B&@G zzZx^oZdH7{2}wx+f9b#f#!}23v{Qof<|Tut1Pjktk#(9e9_z+^BwE69MUs4S{#i6x zrwQjz*ykr3-S97=iPW%Wz0cQXVc)0oKmLsdjpF|4CwO3se*A?{H9z$dGXuSW1RP&3 zpLf_#|KdwfJ1{Q86x~~nZdML%UFk=qRtw&|89UUtnb^~{E~8!qcX#O-E#%-AfQ(=P zQ2r((-=CBZ9v&j6hb8a(#vyF!fY$B`N57Z3DWL;EDdUZm#N39VvA>&ow(Puu_@P)s zBCIib9x^|3bm?RI(W)O=d3)&8ZtTOnBynb5VHh0*JFBI-m%J7^NYujE1%J3CF^C}V z>c2q4U8$W+#SU^-tZ%_=77?C6o;${qaAWY1#=(Cqb)a&&*Up1$&oQ^$2S#!@5Sz-Z zD;!=R2oLy;XAKJ5GU;t(_*R|k(4xJ5C^Ko+uAK8}l3W$(h#X;D0sk$_Z9p$syc&_Q zfT{F~rdY&nqr{-WceXcvacsCWkz(axk)-P9y0(7v_Q7Zb=ovIP2^r^uiq=jj9LjdC z(pdIKPZ8R;9DFYk?Aoj_T_K924z@Y2Nh{|*=(>$zgiYyT1lI9V)?AFhH#RkS3SJ%4 zrx{-`&4Wc8d2i*8SCch%D3Rr^PEzD3G99e5(FT&U<@c;BI!ENzDVIOzJ@5z)P`iSz zeNPE&mI$UfVetSV7`7IIwKSS*i!-19MduNF5E#9`gz|9PFr^zF=7Z*QGF{-jS3)(^ z4ZqDq$c;Xh(i6yG7H%FVIeAmbUF%OrAEQ=Ba&&QTF~Y{TE`(zu5fPaJdtp&TIoPbC zs<%hjTEn8e&p&!X`%Ih60X;9wPky!^~Ayq3$-`KF`8z0W{4{VDpcpO2gk)vFn^z zha@<(a|`1$5$()E0vxOI<3mRg(d74)xW~mFb@f&#L9vWtviXVW{zNe>aJdsS2~51( z(Q#R{-CJwXVy^s}_}A33ZydD*>CeS8zK{lLjk@a34GQjOeE)Chd@{47JyS6BjmQ1B z&e(G&zirF6+PELBmRlN&SvxPws)6XsSo{ev3pm6^cLQGVoc5PhWdyeSVMg(qCs(2{ z(0%@Vw5{rkh_mRScBRdzFa? z2b_p~yj_g$Ve)wDdoSNN(ab@l4Hry0*EiZyPDu2Z+f?viv5eGGBZ}JY7B2{fPln`g z*J*!E7cjpULLtSggS!Ghvgn~1OuWMVNR-HZzDy0!0d5e5#;S`I^L_zGS0gi!*_lG z?;+U~s{EYEBNGk|(}}Z%@h%3m$<>mlwA^@Z>~i89tYR((LqpM~Eo!vVGPKk)d1Z3J zRicT_SeX)Eg4%8&(1LX^dQ3rga)z|<0O^NCUYLP73i2>wINg#f`F}Dms=Fr?9W0;T za%5?JA5wlBf)waI>}(xj1h)cQiyj29S)4glWcQ=P$(MXD??(RI7hw)blu8?Wky+D! z$vYTByzYSu89Cd#X zc|zTckKt6H!k4aA$W|$MpeN;EX_NSD{E;Hj!?J%p5SMr`M-l2j4k+H*E6DVkOkGy! z)Hm`c-G_6NDk2@gdp(osXfFPjj8@ae!}<YEjbJ5U)9dDEAMX3Mb3l zXx6@~w0aGJ4$weG5$^9Caw{mK_Bi;C+i7_pq1fuvg1Z22vCSJaAoj`B@H7~Z?1}k> zQ4Vj%P3-`D!DHJR(w0shJ0&gnE!#TXjKLy*(~l`I-=ACFaUTMt;Uq`7Bhj~gk1Tdl z;2Wr!@ZOKlKc6e(DqW`-gj)RFEq5iS`u9ij3HK_nMI#NHy+18sY~W7d+x&PjIR*LGguGVbQi zr_9ITt=8n z!{!ay%QueGq#k_1EuBTnP#Wdm#}UgsR- zAyY7sWu@ZE4Km00OT7Cc+yaEaF9tk4Bn%ax4PAx_Y8D_h{+A(VTMuOB$Ku0Cu{`T`@UbbnlzTMt zJgj>J6GQXlsey0u6q6|zmiX_2WZm~Xd%T2&qMY+9 zV0p?zET!UKMr^jZmV<^$bJR&f-MJL8oQ;7t%~gj{#EHoaX|~$LW@sPD<#!5L|AZ>m z#r53PwnFqgtp9PqINBn|4&;g7_(wt!UeQVBW=MMZGq7j&u#Y+VO^q4vLxf;g3ChzD z(WLa0v9s!cVVExpVF2`-QUyp#p<`(#6kgB+2?}pJIyww@)x!8A(Ha?80!f-BBHjP+ z1)Ka#F>VY8c`_OQ!w5dQN-=pnTNRvAZm(qWj;p|)jy7tCTk#gK@5JKWMIX`AsS z{`Z$89;c$6oUPFua^*aZ!c`gTTyI;$KX3SDqkyRfRsRU3U)2!*Ntb4^%`^3BvC&>w z)0${%95!B$o>|JeF6Mf>IQL{dTAkmn!i8oN-BsFgjO;UrP061$BTQXHlVA+FoN_%? zf(Vhq7=;0i4ru~C=zG0NO%n%a^srMvEcaE!9)?%HN6gqOVQdl_$xiNEf=~Q*Uvo#% zu^@L!3(z>rZs!yQIjxT0j5pbHSD7>sv3sdgdPlYNKY_^-9g0qSUck}lc!2hZ7^x~~ zvJ;-f#mK^~XfNUw2R_zeN~i9Rm~dIvyX@_4u?D-8@1&1(BQF&h-=MgR zjQxZ@4!ks>EV$@~_xaOp1Gs-d=oNDL!0RMU_jxF{D7dzw)Bmd*oPokHdKreQ(1X-` zxK_m39ItI6zVs=*`CtF$X1qjq03R8Y;h8(sH=c>ihH7}O z|2x|{q`-3Wi&TW`c_154#wEv2{|Zs)m|?p+7v}Yph? z(xX9iF1~D63F9Is#!d4Pi*~)*k(0BBjg8dIYo~y|s#_0`ne+X|g`5)vzHeFECjPQ0 z;~&IUc{;7;Rfb7T*50v0`NaIM3ex;#8|#C!fv0f~a_QZJOA>c|5W8FTcuM*_soSv^ zC$~P0uE$?vV_(+W5ZEoaYnt1jU7>_@<*&% z=el~D+Q(N57cG5=xQp%Rk?L?PykU0-pJqG~;P>=JBGpBZj1rdQok{Vf#$4VfF9LUp zopD-gBP{r0rakQ(Xw$@F_tk>@F1yoPk2ZHpUROmW2w?+eZpZYr&k)34@29T9)q9Z} z2T%N1nB+iq-0VB}>Z*M$D(cF6H4aq!aLnKLBu!JzID&j8jm9^Fbl3cdOjt*z*f=>#h z^;S#S&pEi&TWdOWKRZNsj3HqM3G{V&_k&)L6SiZ(LiS3565Vga*YRFV7rXqVM4sX`9G${ zQW{fXBl2evlYT@ltXnQdoK@LzzAfc9=36Sc0T?rlOmo z2!&HPm=luRp4@2VgbB_%B)23Hf%}d%pQhirGS(IaM~};TAdsDf`jZ*=GsHT9);r#` zEmDo@;l%TE;jQK2C-!`b&iab78K4qRnky)Jcv%<}{$n2bC_a~qON z+wysk6`glf{q2`UgZ=@>HVVHW`jYt|SJx1ig_#9dZq|}_OV`K;7q;OptF@nq{u)@pg`)dd*xeQuYM#LEQj>!C_O6YCPK9-CxWghH} zKA2|kPr3Q&+2ex`S_N=MBB%H<9&&rpCTJ-?|d+uUuY^^*43RdJ$=;bmZ-P`ip~m5w1@u7NCZPMd7)WUcJ)0yM6y6on$2p z?`6Jwnq2h8g?Iq#>DYagjV9;=E_)7%aL%5?^=dS{&iX;)MC!xkicQ>jp&5TrxGc0Ry7+jg|(+9J9_S?YvM2=h}4s%bN#iZuU9F<;QYV0S2^HLT)iB`=Wh^{=; z+NCJmd454CP$A7txa7A?v#kFGzBlM%*8Plw;1>N!dfybE3)`_TzC?}8Hzp0RKcqJ( zVg1#(HbSgZZ+vpZY+iu^Mo+6jY$ur^s5XAwiwo6$)1&bWMp~ru2=lnh_ElnXdB0y> zDMJ5YRHo!y)Yov{KNw7ozvtW4N`~N0J)eXH5QcEXQ9w|f^ArYkG^Do0pM;s1MD{>%o1gc8L-M})zR|I)x>qIWRueONEf)-co z(OWp>G>#b-BpfCLvvmqPC1%-+2T zAfW|AvgNDbea79x*Hv z?z@J*mCXI5wzhjBdoh5a=pZEuif1kA08Vd3#c;9@w(;#x12+8BWA6*80SAjgkb0F3 zT11I+P;g^PwL?z}TWwzV9$*D|D|G5ASltqbSu8}Cq=bQ9N%UfBUW>v%)kuR^X~U46 zVb*Vtf{bHH4j1^PuxBaQs*0{Hvia7?q@Dbrc92CcG?pL$wZ_qY8c@dZxhBkIbSsc;%9Z5(zvemL1Vz%ZS1LTwVpDYq;s9(Pr$e5^Fvd=L7*Tx$GL;+W8aTA#ic{bP6=T5}XG zLc#}TyfX=y`(-Vzo3w-uttPh#CrBM2Bvk7aboC1F9Z>PPf+8Z1o3QkNUh%N7g8W0FxCzB^OAW=YP}N5nX{QP@xpn?9Tpl594=7dewe70QLn! zUuhLZAOV5?B6-q-{W4EqIM+1Pjk>b>ukszNBclgwUYU`(Wlzwc$b3>1llihcuc?tj=&zj}1?rTHhS&e+Hvg2l#w_LTl zOF@H*V{oC99G@U3D`UQ|hxgr^Ad2lwWSO~Nj9a;Nr!0qUt)pcbt+Pxr~h_W)vW4Svqp_^KhJ0|Q|t~&_OvWfB2x*u^8(Qaux}8M z67j&v5xS9@Xz}9|<{1)X)hL^_$bPm@I1RhgSK5JHQ}wXo7jsD=d>j0eE$T>`UexTT z)%(Sv!cugKyn3djD;PUI^Y2vE{5o3p1kBNX5f{KC2AV5biEp{CPXaxo(F#V_7s#jC zm;1u=vzr9D<*@O1Kn0;8Ma$GPbi)-tE5jb(F+^$i-Qb*7>WeJfH^M>Hs|~KW!SzY$ znF$XGV3C^c-H=t1Dim7 z?R%RKkl7NJ7>a)u_ zPq*w4YrK@c>Jfj&Yb~Zm3N4G#3v09~&3f0vd%|L=AqI#Hb*+9+yVv%~;o(4jjBx*c zau&>}Loz;WCoiR$tmC%^PWt@v^u_QH1 z(6j!^t@mw#z;`WiihU!e9PsG=j}{p`U#*1JJ=y^SS&PbK-_M*-|DN7qqWLsvo}5Fz_U=;+pM*Bz}YiA?aZhQ>APN5G`QHS2CUp0yon zD)vAjAx7L|_~ZI|A`TI~W2dY?cbS%*X)3Bjf!|~j8Iv=p#4T`9Y+A%}UqUwoE!Xe4 z^S*kU5NHO>q9z4}b~7w=YwSL&3)PPmbg1I*HD5r=TmhiYh6YEIy~6 z*bs-ZJTh0DyHCw>(Q3<+I)PV68>eG{9u2NuSc9~@nj*R695a~!;#qc_Ld*=m3ac(@ ze2241Da!0QykrrWX7KY<4$a?jHHkb{5Jo{gLfq zr%p$O>^LoAXZ9{s&xM$)@8FiDQ&COjw8{65?p=ngN}im%RjWWF#llx*+(xt3!S~ga zkDFpw4NF_Ao{_7Fl{`^zcSH(Tj-ye3iufq7?3)K(*eg1iTRANrsd|tM$Geg9Px|>V zUL#_mqb1s~G3{eeFS%b@Q_Q?aEX?ZDeXXK$_VOi?I12?Bn@xmWiBVMm)uGyG+j=f5TlPrB)$n zbB`PJ+$S~tyvH2Eu{jG)N3l!j9i$2hscJv= zEHtMPM51B)-(1_zrWL*s)L}lKi?9)0K3pdEFL~}sw?!^B7dpgU&hTcDx4HY1J?4IF zm$s}I3i1N;Xsn|fHzXd-^k*T5<3_BMuzvy25OS5vjM#Ej&dH<)K!P>Qw zDV6THx6~kOWRLi<_zb=tCe)ikj<>H8b&2FVv=mNH{j?8dU8nhzyTAVMNoDV740acn zi(`?XiO<0dc7c%@JWsIgP#5`Se~SDug8Qo%GwRmDg{Ud^lw$O@%<%(ULmxRck883} zpV%#~WqRRAkNp`1H(1@%=8gKzQ`%|e!yzv`LYEN53R&Ld(N8OMUjBo6NF0xv*2e%9 zy^iBH<&`1DF2>+gDiN)cmvY%9`}Df+Z)hF6L6d0zMM32P{z=%mmf7JyT|_XyL`{=U zq8DXE=`sdsCL-j$8WF-}G2B!FxgJb2{q!FO@<17U(+ASR#xFrNk6)sId`*TIkY7?} z@cGoyKhV5RhB-O*Gk_mRLpfgq0;6}4A;~YoU?#MmSGE4xH;}Npcm|}OY|UG}-H?rZ zU&$*=9iZ;N1{6tN&;F~n^}m87@2F1^Wf*vs>Lv(}?VUgi=b+FRqk4kEd{8_5_#*{q zVnVJdH+A+u=`AY&Jo)bRc_=!-Wu;;u#G`4U#V_%%&k}Dq;QMp{6t|c%MbEcRg>%Q; zJ=(q{PdSEsK>*w!lrl!Ib- zm;_tx3VHa@oRA&8I6Pvb@@OlT-i8P#{H<1M(-K^zT$Nqy!7Lpp$3EkY7i?C-8IztD z4`?3?z#5b<*Ef)do2PEfIEk`cr(JTGIRDS-fK31)Xli9=?|1Oyg6Y7DBtB74ayDIJ z{{?|sEJc&6BmEJhVdJOXc@Q|>?8sQ~Dd9ISft!|s%BMXUM2H`P_Ez0AW$NfHh5NtM2BcCNp!-N9fIXBITzXB65>EM7i*bNP(RaY2^&j=uhSN16M9z zG$0MYp}m)tm~T`OjW!U<8z+ef80L6NDjExnEWUf}T6D%-0*i&Y@II5$likAAM?PtY ztxO70#|>oJLdgQ&=-J6NewWeZpUd3P*v4ki@h3jQGAuGux2_Yb44kMXEXE}}u)Jn@ zF~g+Vl6!iV8-m`eseUkyJ>u|}@c{~uNF3h=k4LXIuW0lvAGvXS!K>E%gR=_NwNh`- za2{K6w^|U%?4;;+9;flpAxP@3p7Kp^aR4A>U-~9&PAYRP>m2{{!17TK5@3s@&wS4?~T4n|h$KvIfK?LU8i8Nurg?PjMrKo-|0vgrU{=!Wh z0?nO#1Db?AOpyG1kmK!-iF4HByX%jEv|C57hZkPA3KA`Hdeog&z8NcJGY_p;QZX&a zKJ2aFkYYKY$0!`J)|~TF&{5^(WyS*U5x>P39t84#VF=3N=VLu{;?$rr{WyQKVidE# z7>4oDn-jK!s5y6z&=eQhOw^juJKsID*`ZE+L>vD-sqcm25T-18tDoV$d-@*Sh8r|~ z|7WJfE*KD^G8VGqEQwNv%v+>8r2bz6F2kzJW|l(CE1Vmb+-Wg(k%cE`mS5#q2G$ujxWbp9<^a$CxK;kmGaQXG$Ra+bjTLwr4;bSN^#R^AHrh z#&h?VBPt=RCA5^KtQuZn&6#ZFZ~Q#XBeY_jSav}c zC|%7CK@jXE_Y`B4D0|@BJ`J0<7AyKV-JpuE>r>?bQ7;P(Oi9p}Reqa>E%{Vdl?#r# z1pP6Z>^W)p+(5q7m$DM}eqYT~b+d;2&~0OU6n4i6BeP`7={Xk#5B?B;6^8#^U#QbjO`ooxH)Lmsds0%s3OW$#*$H6S#kcwhoSJs`>-x)oaKKqG^%z?nKf0x8wwDbKLAZyT2Ic@lqrYbK7Iaclv4h$ z+AtEfp^;T}D;O+PN}(J=*->#y81(^Yag`dXcyc%$+6RSyA1QnP^3M1T7P36sz!Tdg zChzIsy>nff>o~>jizksUwkJ+GSte3pzLTAe%)6?6FQBc$Xo34LmL_iWje);k1&g6J z(SSWU<|p`M14}lM4Qv*>@C50U&>T_9a!oh$R=F`4YOV3PCF0$2uV~W#wolEfuEAl{ zn>InLD6j8nW!nehB&$fZa8RVXKFTg7>Fv^=uO+NC%x?>s#Z) zS$q5QHs<@|c5PjYWz(=kt3SW^(EOd1q2qkH-yWS$M}&m+SvAt1FJegl8%)J<{x7j} z1||1|nWcZQ!}i?zW4P5|rl{dC+a(kUloi~1*1MGq9%Xa>(i}MIMx03J9q(GqT&n0) zU)?f=Y^ZhO72ZoL)pVi#d;L%Cmpbx^l08kygac4j)x_qxv4#oD=g-n#&w0Xaw0^z9 zIP$3TexNtH{Rfeu1Sz;Z;Ts$F1t#1pEjH{1rw@@LEf+{q2Tt^vicYRRLsG8GLc+p0 zPD-mM=M#I#k+b4H@rI}LurFn~vDn#GCDCK4u|wNJ$iqjUpCrZeiG=AB^maxj+b0%5 z@{)MHJh~p3*$5P@ZYWtC-BI^Y0Gxz~d`umva#y?IB_{isU9jTrb;%+iHQJ2vH9Rief>ge{hkOI{=$8#x|xiyC`aCaXn@qS7j@1 zLwLVoXl-kWe9Njk;!LTxa2-7VHK zN7_6?VsJUVVm9DQhtl$KFS524g`X$s)`wt$g&@!UeLv@?(*#(7m(RY#zU(Y<0oaJz zdQ{Ij*c^H#k|K|ta@bR8a(+=xOZ0mB+?f>(Lhu}JAjH}U z+he8_183Wm|3~#al{B-e=K?496@R|+kEPg}^$v2m&Iy|LYJyKQD{Wi8xwj%v9eL)_}a0 z!aYcxC{>G+uVJ*fO>OlVGn8KgfrC5MuV&nfGyo`aw7e85+*nr!B9`sOO9T?S-_D4k z8opWlt{*Q*%Bg5(33{h_DGFi&pVH1FO%`nFZ;kI3ExJ{ba^B_lHD?Iikl3tWO zzux<$G6#0`l<%r%NO3qJzJ}G^5QKPK{6x|j>jF)T9{NlscoS0hh>PcPq^Q=S=nOX@ z6Q+&JAiA8Y0t)<>51PvH&BE;ZS6=&XaM_BXZd-A`4*X;7eS7TdRWE5S(Ppzo<_C_c zyt0RA%g%jZ7f$2&iIz70StSr-z|~K)+4f^Eqi9pV z!pSZ9jD&#FBKCT%mB>N%m~4Pq05<9WoVaYsjP(03k z^ONqB;8j3f%ctG@A0#a7+tKO+v9eYH?E(&Six}|VbTs$nl}MKq(c_PWu=Qqdi_L3T z<3=IQ@&=kMEF)V!Jgp!mDdIcxyvI}ZNACIWHU5mK-+Cu%4+-cLoW7Bw>gOGnnaeHH zUW68NWM9S9ML}U8h$BW%YtP7c)^LguT-Khj2tKdcK$Ts&xt9@>=_SWid0ehjysxTc z)mg8sYsPu@j1{}2UGJTL(&*Y4ZOtJaJ=QDXucpq$o-=-H+}Mwjv{y5ybGlGmc^Dq zYIAsad|jp|9e8JBd~kau$H)CZw;8KzmsZ)Pq-~Mov1s|~JQw736bkL<#z3Jk2@V;r z+JvZcd;@Wf4NZ!i=gDw*d{vS);n4N1{zv}s?h1HnjcgqZ_-^E}LM8f)GIEa;l#&CeOnS-t)ofj^|bF296U>EoqZc z;EEN5q=ilKu4zQ=O=`lJHSf5xwKBnsg+BYD76UchycBFd%PlI(8yFNP*G=5GDk9G( zRb)(UFd1)m=M^0~EO>L&R8Xzv2{v>Gv*^4$2}&rRjyL%NsruZyPN&I9s9XdRy~lC> z$To-7oi_tdvF8qMSu@87PpW0Tcqj1jY>;SEu|;>=hu_z;WRN0yJ%#}(cQ^i5U^IKu z%-WA@QMTVyKbw-~$Cf)B9^}F-DOUX7brEUugV;NKE30c0)H@*%Ik4Cr1?NM)Uo1X) zXyu3Ttjxwh217rbz2X2ShJ?s3IKLpjD(-qO;lo?B!;Xg3kp7#t&cY?gMeFL^GBRCv|*p2n+R9SnA30Jw z?Ka3GZe`*f!Eg8NUaI(3zF8prt$xw+gnLCMc*kfVkeREme@F%!sX6Lroet9jp4n)<1?a$Q_IJv?|_ds{J&Cgq$K}84a)yz#rMbh{5xMBPb`aB z_T??Vyw!WgHpQQQzPpz}@n$0>o44=T_dvG`!!#{Xs%zPj+V}IXiVMYy8HRQ7EOD1J zE>`N4jvqRfX50FCq&}72ynPzx2h|u9uP3vfb>F+R|4O>02XwQBk2DjJDuFYv(%u8t z_=zC*tCkk2j5#Jz3bHcyKmbyII#Q9By`6b3gU9yLDWZS{0-K|z0)U`n`?qwvck!eD z6UdSdJljdR`hoqtPhyev;7HukaQ^7{cJ*273Tx(CWk$~Wpk@;Q)Kf*f0K@qnw)d2h z>s7_O8FQZK8QxlZXLH%))>`f7?r1OL#jNPDBj+Vmr9Xl?Z1?PY(7*nIG^Faav#aU; zv6eek;fbecj`4o1yXO7Q<^0Jx9D~Ko)fHgMwB(9E1YbCNZaLvQcNd$2vPpa+Kgodk zam3>F=mkmkAwV*)!;|9zmO_WVP1ZOIk40HHs$X*J5TuZ%eEq*@hRCZ%G@|R09hq8r zAE+*9F>We7QD>tPw);pn3_#r1Kc)j?PHseaX%_jL@fv%jg6c~ve5>(CfOA`AmGNni zOCVXynocdEd}|2Te_s_%;R>!IRZZ7-DOQNTv{KvV6XK?n7WLw^94lQP-Nzr-DcwAv z^v~1`RFP8WDE)>v8}EjWTSZK4cWu2(QpLxsR6O|3MsqrYAh(fUSH@vRMWh&dY5p7v zxEgUol)j-c?E`|1tq1Tw;3Vly$}lQi)1;N{SEijN+JSrTJ%mLOgcet0He&0Uf#0KUCLrUH|LEG?Ax)w#-w zNjD_#AN-n@R`!T{F0?-<5TPlCF+}%OZQFe$uU0H{14)73b-&|{wxq!`odSi;;b3sTElo9?(7AVZ34AO{?sisUac2l{m=DBoMojan0L zk!8@lC6&FGw|-1qJ-3H0>yK3BTXYF?pm4U0HC)q_ z;JT(wL3hVwq?lDFeV)cUiEg%U15ufN=h3nQUWN~2Q&SrzEHqA3#R#m%I*4C#IktJZ zZw*pdb*87hDGa}-&RSwjdXnG;sYUI-L(`seCE+Pyu7B4k>$7e+v`({d0Ei~}_822y zQdSs_5p$?_zovXF>#3Di;u4^%$+0zQxD@&ViQlHko6?w1FLE?tBYriU9(4aP?FOiH z2gPzy@>f$+xyGp(e<7(XoH;A*utuuj+R@QcXo z7=SOcfMt}zK~x!uDVCnM2!}(xb*<(N2dSf+`4sgWPX9t&zqW^#alij5gB*ETqdKd> zJJe;?JimqG(r=U$Ut3GW=OR`zfO(Vvco?a8`h4$f&esy;0Oo0JWO^Qb#VYlHRI!g6 zdSJhWy|Wtl`yck~@GH*aGk`t2{u2L~vU#%txxW<$3T*AQ;;Mxhws=P2@6JUcgz!c{ z;;7m_F7MQq?|iO~#h)uoeN2??mxamwYNwhHn zxcBGQKIrR5jwcSD-S}SiSc6Fiq-ceXVX26%?YzQl!{fw=O%K}hRW2^Tl;w&{2lI=f z&e;xe*Xl*SHOm`iCQ`6MEML$_ll~pX<;K0w4 zyQbh~H46pe(6}1au0M~9-ZDSaD3jE{#37Uj^^Yq7)2t@&GeDOreOLmUr&wiO{eBO6 zq?+N+L{#faQJYk%uZL7^`~qg>^q~#42}~o@r{_MV05Cdwe+Fy2ihWV4!9O~w9{7!Y zKDw~OsXv)8`V^MR;ip2c@9q#+AN}FNG_1Kt-zZIq{KRhAL%-Zwn-xqQ+RxP7krC_V z$*%3-N)zoJb#pFGX@)j`>kE;o%@|b_eK~W*sXgV73sf}}J+)E1Hezb=GL)|wW# zDWHo?;KtQAR}2x~ortsV#WbW5aDpzR=fgWa4cPUCWi(@n`{cS!DTGM_{%RT}x@}Jg zn)~aeKL{GD`DD-YMyY7q#F2B+J}b2=W9aX=b1gMv$rU|G@3K5pIz^QJnO(u+*+*?I zSG)6fCrE6C3X4*mx;?;^ciwfhtwe;VnWidVptrWYUBy>}MCzF+$^EGh_fY=g6m_+%jeMd@)N_Xhbh|ds zE1TeTK|-&Y4+^|E%AQ11?;CdEkw5CyOR|XE6#jq_)G;eNRUluy+evd{N^49b#9`Nr z>|!g-HyJEhDP(l4Fz_fi=p&wp!ACmxszHo9n8ON*S^xGHh6dXazapY4aVmIH-m!m> z&lVK)Q5B!4pmBsDsc^xUQ-7~MH4bd^zEL`IkRE${qLU%Q^(vfD1e-c+x6A9GFWV(* zd`Le=u0=pCxB#kMnGk{<5oD8K4;o%IO|h{ZY4jT}M`*8fr2i}#-Kt4u{><~e){3Zu zVrdZ<)jR||2CC&yb4l=`-nN^)OLT)*-0s?my9w@@q%WhmZ&lfPI4zcu>x`P1R`bUe zAf!K3a5-B_Qphwf(K_b5{Jw9Ajm3j>Lwe?-4B@J^<>KQF0M+2fjh@*so~H#AuN9S`egYG(`)B=svjN z-_RG((D5a`p$5shxdz0K>@<`{PJ%u2P!wuO=%yBEwIqmvn^hl)W65Qj$xUbLXmCa* z^oLCUNVD%o}>DqT_y3`o?@V7SmQqh3O({-oz{|T2wtW(#DM+NXk|7r7ff; zO+(yzx%?G}kHwvti6%w2G+Vs66%DFJ@kdR%E;6UdE?SuJggi~PUi>45#10{wOW6t} z`YZ^hhKk*6m|PmD{4BU<{}Wz>KE#o%JZx|3OZj~V>C&5& z-(agoC%CuOKyppaG-uwhb(ZiT5xWF&+aHsYn$qk#>;-W33*gkuvO99{Xy$bV#rMJI(w+lkH#&L;c6KrlnDp77E^J> zk+vZ&clqvtPbR$lp{$3OGO-FmUv17@ym>gK(h^r*V&&}1E%Uvq&>?*Z8rR?hl8eon zj%q>e7rBZ{F14I;WG`Jz>86P2QFs*qG`pd%nsd0n%!37Z-l!b0jC*Ts^F?{7-Zuo= zXp|N;-A(>4KEYEmxs73~z)=jUhoDTo2FY5MQ>EdemZM{9SS7NP8S$_F7XM}WhGK1S zzn?r`us*3T1$uE3y4@8ySLC(BkHDYwe!%nt-At@yZ1FP8+^Dyo!qit(h?Fd0PVnT9 z{j|-FqlLP?(M$o)$y#Mh`<6CdacU7WBtLS++zQX~@&{)B3fHya{qpw@-=NEo@~rt| zi8Z2G(b=#KCY-#F6_XEwZlaa!RN}`wgH$@taOfKPyYRuY9+T`AZ;QWm+y|bgY4rWT zozB6Wl)Uy@*Xgz@M*A4`&Nmy2cnx66n%F*u8Iqkr(7VAk8(HAS1Pq1*N*RbVjo}IG z`Cy)pE5Y{6I7TS0;JI_bYjsm4)?9HL)vAP}!^zkVg>$}fB*6B_$bsfFcb0+(8slw4 zHY!I?$z&ws#YwKlelOZXN31RNXlh>6)@Bjlywkl$Tf+P>F3Ftwsv+o;PDc#2SL2cL z5LNvxT^9pqgpi4yONq#m7{6&~%oB(f+GJ9xhafJZ#5_p%J#V}JY@y1UDV$zy-XkFX+go+76m_$Qx>^ z3eY!Wtk~3)Z}~xc$vm!fC+hS2fZ6h!HfZucPDO0dbDj65w7E58)azK-XUoMg-|arp zJ{PsDYt?cl@N8v>di|q2)}rAlQ51r#dp@#pOZYQWn*?Q&UsSRz1X6H^EtVBRHp2~D zzF60>vtGYB)k+}q-s2Ny=qsY!^1E)-1;nqJkD#8dQT`d_h<@40U>KUIRbu<|Q4;H=ZU@t5*0vXXf>k7!byE)QTq+&Ms#x z9)yH-Q6jC7nGa0FwsC$Tah>rd`+dqXv?J73kVZ_a5}v?q!CCTkic4b1OcNE`J9y+( zOurYoaN^79$f@ZuFB|ft)c#keH6Jie^@Vg*=*j8U=3bFSCW3*MScz4dz1Ytgic2YL z(4JCFXh1>_sc+Ngng1Nnn+^Zj6Y-`0ApvNV0%XTVUbpnuGbAyZZR#0m>PcCXKW(85 zYt|jLCH=U-iB^i**R{2LoBRI3!^{Z~@jo4d9tJqN=JQlipxkSci2k?S z`}7TPd{iL+rtBWHFiuBhgmS|1RF}2$ZcpI7Yd3COy+7#rKbRPu0xaE!JU25r@Bj|& zS5Wxi)-N=vZ3isk71pr3x0E5ccOhhdM3gU!iM9S(Y{btabEv9Gi>xC)n2pUDsBDw9st#I)%Zyzf^)s$y4_YzUw;aMIqrVC-Ve3wn1b9%3)@U?qY5y?bXW%aC|QR|L;tsC zM!jEA;X^4Wv*c>f8cyB!DlQYW%D~mRKN{i&y~k64(#Wrlzvv8^|)B8 zqi3a*X$x86bP1)7*#E}W$nMeh{iwp+b{=9!vMb!^Ccjat`bwNPCV;MCWy<;oU^^yZ z7@bAE*}9>V1!ALbxPiDI4{#vMDz~>PT_I)+P|zO2&bPmp!-XOUACjfocH=3%NTjgE zpW)$O9`VWVq%cUSO{Y11@OB3OVRFg4&3Wece!4%WJO0m`Bh0fY7NY2HlbOq?p@U6t zFOMYgy;1gth}|>=}r8b;VNFb z>~>}Pw3DLQC@{jG?sh@@a_^(M6n>MRs(PPj~=!2 z4tt@LOtTsP_E9Qh;`{&Rpwzb%{C~luy~~I(=@FokKDh9d4#^j}mn{Vtz9aYYEXnN< z0WIcGhLd)o@G!nM7OvGx!OJoRVh)^upS9)t{m$X{(>xnc8K=8^!w(4w+by~3plaq{ zVoO%{KQG@wbC*PhX4$RlWX_0@Jsxx;e*jKQibd;iKJ?&*B{txBr|GBwla6VXkR#9H zEyR>ML`f%R6f!6_1zEJCI_!1!y}f?_kvCrc#Ck5>v7a!^5RB`K zmxQmRhaf>k>X4zJj-m9D8pH$p_qMDsCa^3y?^SxMj5ErQHa6X8Hu7~DL}fs?--I?m z5R;?>HK4)%y(;H1NzGhrI>9vOQJf_@Z}b!WlupGqyl^1(aovSKxuqv*sMp`w=O=5_ zFqPMyfeC_i>%eBFi&Nl#4udA}i}EvL+7>6@2=zV0 z9L~xJuzvD1t-bdaNS?|h=(o$@jj4K7T#ue8|5CV(Uza^wpeiM3^!KdszBYKqnN*c% ziMR`){QmiWv3O)rHYqj|rRXb4y>H=_(`9~ccHjCj{U#G_GLU4vg8#JSvc3>kGoN6Y zJZJecQH^St{X*&a9_&c~6tOLi0$MJ7)BHL#ZU%&fr!wz@GWYrCFF!&yME0c~Me2q_ zsP8T3d>qr}U$)NXKWrU}C)}9iD0hMvn&RJ+H4RK+-`DUudTc)fijSPdiiIV2 zSm&WaUKxOM$JF*TJ_n^+{XLZ&@#~afPp=92CIR8sD4GJic+vL;wa`+$b~XpN{o7ZK zJ4`uNZ}vAO_W5tMZMl|B(`=LAe;MugQ@j3}MSXc>D`$|MuTk}L49a5TFDor7zFaai zSor4V<(27*hml+s45`qrDq;YGdXTPse4VaWpiw5j;snv?XBG>}9>+n=>jAwg#rb43 zi75wfL_gB1*%Ulpy9?QV;@`-y?UbTKSU&Ler0GU)*h#H&AK=axOed#B`AtlN{a@FOTl4FsxNYa@g}+2)XQGti>c;s9a4 zUv10bMNYtW-Y1n}gh-wFZa$_PRY*3cymG7Z@1SY^r3q*+7*~-K#+`sN zeu#7SYsOC)k2G8Q(}U!DSpC2JJK3V~XXP)SzvPV}{3p`+F#IxT4tD&%eXm}?{blef z8oskG2>!wMM4T&LWJA`lh1LE#UR9_Xr#O)OZ!E94G5_Dmz3KGD#BvAZc&|pD>=(yq z?1{E0pR_EBF2v6x-&SN5;}?FLk@SMkjS3sfTZGCL|3^E}vh+tgP}OWc{>LLojz9tM zxdD2Kp#mXV9=%sJrFAu6i1p#H3D6My0Ehl9XZrj#@+fEkkhU)_|I-nRKdAGFCs7dy ze}uQi*iJ}DNyBe&Pd)GUE`ucPDOzP%-s69IPEClAiszgg(ia!jWz-eZz4O6yus?%L zf;nF|0qkl(Q8Cu^fUlDF^PK^+Z-PWc$Ka{r zGO&PaDEXoVVW>6*n3dboash}r+kA72NLCg?hX68WBOpK~nuRNK&Cpy@8)nfOQB=__ z*jz?}Vk3eDIdE3u?5;|Jy)dRCEA69^@rrIn;PhvvS`|k>E{q5;!$WXWmRlYuu>!S`SsveZKI}g6sfG zC0}_5*-|-ckmgXKVjsz|I8o3(6j5HIb-LMrEnz@!Yl?RL-kIspDF6b!{Reky`tXDb z27EM`<{dYf)?Ey4vVW{ z7or~d;iZLmO&vnhfhIhn8yoSijspf2E~LqUARU~k`eyf7TGwSQCV$Nav$^;mlT!sZ znq{+TIbqO{r*n$-DBWhve$!JYm|?_{=5F`5HG&jaaVIdIGM@4id+HT=@fgGIbQVr7 z7>a3fX%YC9-k~Wali0D=!)6s-N~&_%K@nleP%ps4cg)0&w!u)K1#kh8bI8<@o%jB= z+CJ7Iw&FPUid`-NT<94fYtlnA5uLbXcmXjlbo@Bs!YdgqMgub}yX51#s4E@CKOidv zNpPW~?H8)Oq^5F;96j1|rjFhe>mK!4Kc7TXvIaPX$&^#(Wg)1tqyR0>H(?6d z%aWEiXBV$2xLeGblh+e9@ip;3s~uNi4l0Z6>%4D5KB>b`x`E?OD3ab6H9>L!v-@RY z5@Q<;Jcz{~_+WqK$XoAgCg|_?HGIf?0q3kT1yN0@?7z%S9J$8>`fKbH@A47j7Vb|C zvd`peol4&U{nkqMFD7FVGj8w(rCfEa@2kq>Tl_Zxxx`}f4M|^j2KtPt%jSm7S<=+4 zeWUb0Kv?6GXz1AwGQfA{0&oW?s#V&vKZ;Gu^Q^kP_bn^bvCKmb^%8L69!QDR9Ly^z z^o#K#+j;yY#QEq)=mrMN-JJaaTpBla1d5qp%(=Hu)RmtF>x)hkxz&Lz^)mfECbd@c^{TBMhkwBqvY0}WQC zYcwxY!GS?C1-%`89NP~k7_R;U%1({!!YEhtYAC-JbAQ_-ZvV8^P!NNJlPK!Ptv$wb zciwf1KinQa!nZYQ5N~6z%M*i&OLk@X#8Xbah}H@8&@1IoFAhz*$SvXXVv}ORvI)k< zwB26WX5J9ulpLTq>h$HCPgoR5Fvt;wxYd|?Vy)20SGdTrtYf5qgB6?(`jH-)=QJ*_ zdw9X0C*Wg)hO~T*kaHsD-Q1V*R+UbQJ`)-Cq{hNcj%wCY2f`ub>n{Pp3uzor5SSdJ5qBK)wm&e&M=f~-sIa(0D z>C2v7PXVgYJ)c&oB?GnJ$pQDvG(MiCO4r?s9_?5*T9#itj+*!f3Ywle;{*-1L@#ux z=~KX*x_jyFthA<74Zgg-n58x4=k`H>m0Kaw=@8?+qVAH5&W_BxbP74I^yHk}v@F%m zJ3|%Iq|9%rzo4TkQX`D0Q{=Qqe#*L)OR~DSu=4>pdi~ne19RqVE_Qz09Mn!RXI)b& zQcsh|8ts2Fdy`#FOAYOIwet@zuhXqJ5ukDANk!WGiNx|5QYVUrlbTU|>FN31@ew?T zBUai!cU~vCX5FgwQPxg#n!&|a42U1q55L}&Iyj%l)QESWA`2bZ;0LS{u(m{2i0d4m zb2&V&i>!1=D`qlXx%PzUcH?o>b#gwqZ}M;*`~lrKpCeVP-m)~*5|ufAKW0B*mwbMp zzGwAzy2n;+^s}aL@u`B$ZFAsfepxCpTt#)?IUN-Ur4pKw3=z?r8_wyb-ZG4ULb5Xj zgHR9Xva7c2DL$%Ir&2FH^^?&QaBFmyP|3A)7YQiNV~({q9iaCVe@f7669@l$^03yyJ0qEyrHyzhY*vaGMXM>6~`vnskU znQ~0=@x4o+V;L0=lby--M~@d9$$Wj8;M$=(_n*hw$r2>pPQt)_Cnk#MeJJBUrwE+x z`1JDV6&!?tKE6L4T$?hTB%7@2FnkMYLKoMZvO%wGzP{JfGRO1$M7aPOhs)~&Bl;oU zq#OsT!>zo&=`Je?U=K%R8N~g%7Z-w?8roFJBVTQ#Si^=wpsd((Cdl$8M)9o&w|5l3 z>rPevuKM|HPpR>Br;?G5b3P8JYLv;0&fHfiIDB&n0{OGhqp=RrcW5o=@klIVGuD-g+==ru~VcYAqQTBH90h&*bmeQY2Ih72=(ic=79cBV5-vl~7* zq-rbtNbkshVHPK|LUec>mtT7Pj}V1`?uM)!>s?<_7djg1Nq8LIS-&7}i%BCvGu(a7 zaR7x%sN@;X_!g8>N_;NT77HX@IWI?o?W5CxIJy1(uv*m)%ufUmlEk1VtMCs6|1!x% zrqAa=m#f}{2mAbWUaP?Wm*xiOS8LmK6ypPI27JDI!3F8~7IhM2B>l&MZG;2xX)|kO zFnkv-@i(XVyP#LGiCD9>EEbhaZ!APyW$#8 Pzmkzq6fYAs{`CI<)P4DJ diff --git a/_images/wifi2.png b/_images/wifi2.png deleted file mode 100755 index 670a8066da6e299a969d5e595d69ceba6acb006d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45607 zcmb4~V{~NO!mhj1v2EM7?R0FXVs_MV(jD9E*tS`*(XnmYy4eT&JKwo`-!bm|Q5lty zEZ3~{ywAHr6uwHpL1ROI`t%7-N>Wtm(f~bRVDib#*3QO+&Jo~XVq)uPZs&CV1<3#D6X7Q*Q6UxAw3BwX zZ#uK-56{hwcIU0%HT8_$;<{3;!)SPXBG0P1o z8m8$MQcg}4{B^^XA)_QIk`?RH?{M|CfcIaAuPT+!>H;iy0G43p_7P3Viq=R2>mNsr z9$}HRv>h>b5^99!KEJ>!;Rxl5b $aSnlwWS5o@mP8@dz_6NChOJYYaE2%VTDp%9 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 Ur4n<9g!sIZkbhGq_QCIe029yBX#fBK diff --git a/_images/wifi3.png b/_images/wifi3.png old mode 100755 new mode 100644 diff --git a/_images/wifi4.png b/_images/wifi4.png deleted file mode 100755 index 576bf06aef59669db8877c0337af3283fedc8b14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35592 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

    u_hYM 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 Xie_b{ozMyb{1Fk96e#_w<@w(LF(~jE diff --git a/_images/xml.png b/_images/xml.png old mode 100755 new mode 100644 diff --git a/_sources/Appendix/GitSetup.md b/_sources/Appendix/GitSetup.md old mode 100755 new mode 100644 index c8f5bb5..da854eb --- a/_sources/Appendix/GitSetup.md +++ b/_sources/Appendix/GitSetup.md @@ -1,99 +1,99 @@ - -# Module 0: Setting Up GIT Repositories - - - -## Purpose -Setup GIT repositories and access on the **Master**. - - -## Create a repo within the GitHub Classroom: - -1. Browse to [github.com](https://www.github.com) and create a GitHub account if you do not already have one. It is useful if your username is something that identifies you (e.g., bneff1013). -1. **One student per group** do the following on your personal computer: - 1. First we are going to setup the repo for the Master. This will allow your instructor to see all of your commits throughout the semester. - 1. Browse to [https://classroom.github.com/a/OoH0u_XW](https://classroom.github.com/a/OoH0u_XW) - 1. Select "Accept this assignment" - 1. You may need to hit refresh, but eventually it will provide you a link to the repository. - 1. Browse to your repository. - 1. Note the url for your repository (save this link, it is the best way to check if your repo is updated). - 1. Go to Settings -> Manage access -> and "Invite teams or people". - 1. Provide access to your team member using their GitHub user name. - 1. Now we need to do the exact same thing to setup the repo for the robot. - 1. Browse to [https://classroom.github.com/a/0MPq4TNE](https://classroom.github.com/a/0MPq4TNE) and repeat steps c-h. - - -## Enable SSH connection to your GitHub account -1. Open a terminal on your **Master** (ctrl+alt+t). -1. The same student as step 1.1.2 do the following: - 1. Generate a new SSH key, substituting your GitHub email address: - ```bash - ssh-keygen -t ed25519 -C "your_email@example.com" - ``` - 1. When you're prompted to "Enter a file in which to save the key," click enter. - 1. At the prompt, type a secure passphrase. - 1. Start the ssh-agent in the background and add your SSH private key to the ssh-agent: - ```bash - eval "$(ssh-agent -s)" - ssh-add ~/.ssh/id_ed25519 - ``` - 1. Open the public key: - ```bash - nano ~/.ssh/id_ed25519.pub - ``` - 1. Select the contents of the file (maximize the window and ensure it has your GIT email at the end), right click, and select copy. - 1. Open a web browser and sign in to your GitHub account. - 1. In the upper-right corner of any page, click your profile photo, then click **Settings**. - ![logo](Figures/userbar-account-settings.png) - 1. In the user settings sidebar, click **SSH and GPG keys**. - ![logo](Figures/settings-sidebar-ssh-keys.png) - 1. Click **New SSH key** - ![logo](Figures/ssh-add-ssh-key.png) - 1. In the "Title" field, add a descriptive label for the new key, such as "MasterX". - 1. Paste your key into the "Key" field (contents of the `.pub` file). - 1. Click **Add SSH key**. - 1. If prompted, confirm your GitHub password. - 1. Create a secure shell connection to your **Robot** (password is dfec3141) - ```bash - ssh pi@robotX - ``` - 1. Repeat steps a-f on your **Robot** and j-n on your **Master**. - - -## Clone repository to your master. -1. On the **Master**, open the GitHub repository and copy your repo address using the SSH mode: - ![logo](Figures/clone.PNG) -1. Open a terminal and browse to your workspace source folder: - ```bash - cd ~/master_ws/src/ - ``` -1. Clone your repo using the username and password used when you generated the SSH key, replacing **USERNAME** with your GitHub username: - ```bash - git clone git@github.com:ECE387/ece387_master_spring202X-USERNAME.git - ``` - -1. Update your git email address and the last name for you and your team mate. - ```bash - git config --global user.email "you@example.com" - git config --global user.name "Lastname1 Lastname2" - ``` - - -## Clone repository to your robot. -1. Create a secure shell connection to your robot: - ```bash - ssh pi@robotX - ``` -1. Ensure you are in the ROS robot workspace src directory. - ```bash - cd robot_ws/src - ``` -1. Clone the robot repository: - ```bash - git clone git@github.com:ECE387/ece387_robot_spring202X-USERNAME.git - ``` -1. Update your git email address and the last name for you and your team mate. - ```bash - git config --global user.email "you@example.com" - git config --global user.name "Lastname1 Lastname2" - ``` + +# Module 0: Setting Up GIT Repositories + + + +## Purpose +Setup GIT repositories and access on the **Master**. + + +## Create a repo within the GitHub Classroom: + +1. Browse to [github.com](https://www.github.com) and create a GitHub account if you do not already have one. It is useful if your username is something that identifies you (e.g., bneff1013). +1. **One student per group** do the following on your personal computer: + 1. First we are going to setup the repo for the Master. This will allow your instructor to see all of your commits throughout the semester. + 1. Browse to [https://classroom.github.com/a/OoH0u_XW](https://classroom.github.com/a/OoH0u_XW) + 1. Select "Accept this assignment" + 1. You may need to hit refresh, but eventually it will provide you a link to the repository. + 1. Browse to your repository. + 1. Note the url for your repository (save this link, it is the best way to check if your repo is updated). + 1. Go to Settings -> Manage access -> and "Invite teams or people". + 1. Provide access to your team member using their GitHub user name. + 1. Now we need to do the exact same thing to setup the repo for the robot. + 1. Browse to [https://classroom.github.com/a/0MPq4TNE](https://classroom.github.com/a/0MPq4TNE) and repeat steps c-h. + + +## Enable SSH connection to your GitHub account +1. Open a terminal on your **Master** (ctrl+alt+t). +1. The same student as step 1.1.2 do the following: + 1. Generate a new SSH key, substituting your GitHub email address: + ```bash + ssh-keygen -t ed25519 -C "your_email@example.com" + ``` + 1. When you're prompted to "Enter a file in which to save the key," click enter. + 1. At the prompt, type a secure passphrase. + 1. Start the ssh-agent in the background and add your SSH private key to the ssh-agent: + ```bash + eval "$(ssh-agent -s)" + ssh-add ~/.ssh/id_ed25519 + ``` + 1. Open the public key: + ```bash + nano ~/.ssh/id_ed25519.pub + ``` + 1. Select the contents of the file (maximize the window and ensure it has your GIT email at the end), right click, and select copy. + 1. Open a web browser and sign in to your GitHub account. + 1. In the upper-right corner of any page, click your profile photo, then click **Settings**. + ![logo](Figures/userbar-account-settings.png) + 1. In the user settings sidebar, click **SSH and GPG keys**. + ![logo](Figures/settings-sidebar-ssh-keys.png) + 1. Click **New SSH key** + ![logo](Figures/ssh-add-ssh-key.png) + 1. In the "Title" field, add a descriptive label for the new key, such as "MasterX". + 1. Paste your key into the "Key" field (contents of the `.pub` file). + 1. Click **Add SSH key**. + 1. If prompted, confirm your GitHub password. + 1. Create a secure shell connection to your **Robot** (password is dfec3141) + ```bash + ssh pi@robotX + ``` + 1. Repeat steps a-f on your **Robot** and j-n on your **Master**. + + +## Clone repository to your master. +1. On the **Master**, open the GitHub repository and copy your repo address using the SSH mode: + ![logo](Figures/clone.PNG) +1. Open a terminal and browse to your workspace source folder: + ```bash + cd ~/master_ws/src/ + ``` +1. Clone your repo using the username and password used when you generated the SSH key, replacing **USERNAME** with your GitHub username: + ```bash + git clone git@github.com:ECE387/ece387_master_spring202X-USERNAME.git + ``` + +1. Update your git email address and the last name for you and your team mate. + ```bash + git config --global user.email "you@example.com" + git config --global user.name "Lastname1 Lastname2" + ``` + + +## Clone repository to your robot. +1. Create a secure shell connection to your robot: + ```bash + ssh pi@robotX + ``` +1. Ensure you are in the ROS robot workspace src directory. + ```bash + cd robot_ws/src + ``` +1. Clone the robot repository: + ```bash + git clone git@github.com:ECE387/ece387_robot_spring202X-USERNAME.git + ``` +1. Update your git email address and the last name for you and your team mate. + ```bash + git config --global user.email "you@example.com" + git config --global user.name "Lastname1 Lastname2" + ``` diff --git a/_sources/Appendix/MasterSetup.md b/_sources/Appendix/MasterSetup.md old mode 100755 new mode 100644 index 5d8329f..85583e3 --- a/_sources/Appendix/MasterSetup.md +++ b/_sources/Appendix/MasterSetup.md @@ -1,215 +1,215 @@ - -# Master Setup -This guide will walk through the steps to install Ubuntu Desktop 20.04 LTS, ROS Noetic, and all dependencies on a desktop computer. This computer system is utilized in the United States Air Force Academy's Electrical and Computer Engineering department in an embedded network with the ground robot, a TurtleBot3 Burger. The master system is used to host *roscore*, utilize ROS GUI tools, and create secure connections with the TurtleBot3. This guide is adapted from the [TurtleBot3 e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/#overview). - ---- - - -## Hardware -For our application, we are using [Intel NUC Kits](https://www.intel.com/content/www/us/en/products/details/nuc/kits.html) but these instructions will work on any AMD64 architecture. - - -## Software -### Download Ubuntu and flash USB -For the desktop machine you will first need to download [Ubuntu Desktop 20.04 LTS](https://releases.ubuntu.com/focal/). - -Once downloaded, follow the instructions to create a [bootable Ubuntu USB stick](https://ubuntu.com/tutorials/create-a-usb-stick-on-ubuntu#1-overview) within Ubuntu. The guide provides links to create USB sticks from Windows and macOS as well. - -Once the bootable USB stick is created, follow the guide to [Install Ubuntu desktop](https://ubuntu.com/tutorials/install-ubuntu-desktop#1-overview) selecting a useful computer name such as `master0`. The NUC requires you to press and hold F10 on startup to boot from a USB stick. - - -#### Setup GitHub SSH Keys -The following assumes you already have a GitHub account. - -Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login: - -```bash -cd -ssh-keygen -t ed25519 -C "github@email.com" -``` - -When prompted to "Enter a file in which to save the key", hit **enter**. - -At the prompt, type a secure password. - -Start the ssh-agent in the background and add your SSH private key to the ssh-agent: - -```bash -eval "$(ssh-agent -s)" -ssh-add ~/.ssh/id_ed25519 -``` - -Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account). - -```bash -nano ~/.ssh/id_ed25519.pub -``` - -Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email). - -Open a web browser and sign in to your GitHub account. - -In the upper-right corner of any page, click your profile photo, then click **Settings**: - -![logo](Figures/ssh1.png) - -In the user settings sidebar, click **SSH and GPG keys**: - -![logo](Figures/ssh2.png) - -Click **New SSH key**: - -![logo](Figures/ssh3.png) - -In the "Title" field, add a descriptive label for the new key, such as "master0". - -Paste your key into the "Key" field (contents of the `.pub` file). - -Click **Add SSH key**. - - - -#### Update Alternatives -Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the "python" command instead of "python3" so we need to create a new executable, "/usr/bin/python" that will call Python3 (basically use the command "python" to call Python3): - -```bash -sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 -``` - -#### Additional Software - -``` -sudo apt install jupyter-notebook -jupyter contrib nbextension install --user -``` - - - -### ROS Noetic -At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the master. All of these instructions are adapted from the [ROS wiki](http://wiki.ros.org/noetic/Installation/Ubuntu). ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal. - -#### Installation -Accept software from packages.ros.org: - -```bash -sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' -``` - -Set up keys: - -```bash -sudo apt install curl # if you haven't already installed curl -curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - -``` - -Install ROS Noetic: - -```bash -sudo apt update -sudo apt -y install ros-noetic-desktop-full -``` - -The full version provides theminimum packaging, build, communications libraries, GUI tools, and 2D/3D simulation and perception packages. - -Install ROS dependencies for building packages: - -```bash -sudo apt -y install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip xterm build-essential -``` - -Initialize rosdep - -```bash -sudo rosdep init -rosdep update -``` - -Source the ROS setup file: -```bash -source /opt/ros/noetic/setup.bash -``` - -Create your ROS workspace: - -```bash -mkdir -p ~/master_ws/src -cd ~/master_ws/ -catkin_make -``` - -Setup ROS environment variables and setup scripts within the `~/.bashrc` file. Open the `~/.bashrc` file with your favorite command line editor and add the following to the bottom: - -```bash -source /opt/ros/noetic/setup.bash -source ~/master_ws/devel/setup.bash -export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share -export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes -export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master -export EDITOR='nano -w' # replace with editor of choice used with rosed command -export TURTLEBOT3_MODEL=burger -export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR -``` - -Any time you make changes to your `~/.bashrc` file you must source it: - -```bash -source ~/.bashrc -``` - -#### Dependencies -There are a number of ROS packages required to operate the TurtleBot3. - -##### ROS Dependencies -```bash -sudo apt-get install ros-noetic-joy ros-noetic-teleop-twist-joy \ - ros-noetic-teleop-twist-keyboard ros-noetic-laser-proc \ - ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \ - ros-noetic-rosserial-python ros-noetic-rosserial-client \ - ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \ - ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \ - ros-noetic-compressed-image-transport ros-noetic-rqt* ros-noetic-rviz \ - ros-noetic-gmapping ros-noetic-navigation ros-noetic-interactive-markers - -``` - -##### TurtleBot3 Dependencies -```bash -sudo apt install ros-noetic-dynamixel-sdk ros-noetic-turtlebot3-msgs ros-noetic-turtlebot3 -``` - -##### [Simulation](https://emanual.robotis.com/docs/en/platform/turtlebot3/simulation/#gazebo-simulation): -```bash -cd ~/master_ws/src -git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git -``` - -##### [ECE387 Curriculum](https://github.com/AF-ROBOTICS/ece387_curriculum) -```bash -git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git -``` - -The **ece387_curriculum** package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool: - -```bash -cd ~/master_ws -rosdep install --from-paths src --ignore-src -r -y -``` - -This may take a while. - -Now we can make and source our workspace: - -```bash -catkin_make -source ~/.bashrc -``` - -The last set of dependencies we need to install are Python dependencies. These are listed within our **ece387_curriculum** package and can be installed using the `pip3` tool: - -```bash -roscd ece387_curriculum -pip3 install -r requirements.txt -``` - -> 📝️ **Note:** the "dlib" package will take quite a while to install. - + +# Master Setup +This guide will walk through the steps to install Ubuntu Desktop 20.04 LTS, ROS Noetic, and all dependencies on a desktop computer. This computer system is utilized in the United States Air Force Academy's Electrical and Computer Engineering department in an embedded network with the ground robot, a TurtleBot3 Burger. The master system is used to host *roscore*, utilize ROS GUI tools, and create secure connections with the TurtleBot3. This guide is adapted from the [TurtleBot3 e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/#overview). + +--- + + +## Hardware +For our application, we are using [Intel NUC Kits](https://www.intel.com/content/www/us/en/products/details/nuc/kits.html) but these instructions will work on any AMD64 architecture. + + +## Software +### Download Ubuntu and flash USB +For the desktop machine you will first need to download [Ubuntu Desktop 20.04 LTS](https://releases.ubuntu.com/focal/). + +Once downloaded, follow the instructions to create a [bootable Ubuntu USB stick](https://ubuntu.com/tutorials/create-a-usb-stick-on-ubuntu#1-overview) within Ubuntu. The guide provides links to create USB sticks from Windows and macOS as well. + +Once the bootable USB stick is created, follow the guide to [Install Ubuntu desktop](https://ubuntu.com/tutorials/install-ubuntu-desktop#1-overview) selecting a useful computer name such as `master0`. The NUC requires you to press and hold F10 on startup to boot from a USB stick. + + +#### Setup GitHub SSH Keys +The following assumes you already have a GitHub account. + +Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login: + +```bash +cd +ssh-keygen -t ed25519 -C "github@email.com" +``` + +When prompted to "Enter a file in which to save the key", hit **enter**. + +At the prompt, type a secure password. + +Start the ssh-agent in the background and add your SSH private key to the ssh-agent: + +```bash +eval "$(ssh-agent -s)" +ssh-add ~/.ssh/id_ed25519 +``` + +Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account). + +```bash +nano ~/.ssh/id_ed25519.pub +``` + +Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email). + +Open a web browser and sign in to your GitHub account. + +In the upper-right corner of any page, click your profile photo, then click **Settings**: + +![logo](Figures/ssh1.png) + +In the user settings sidebar, click **SSH and GPG keys**: + +![logo](Figures/ssh2.png) + +Click **New SSH key**: + +![logo](Figures/ssh3.png) + +In the "Title" field, add a descriptive label for the new key, such as "master0". + +Paste your key into the "Key" field (contents of the `.pub` file). + +Click **Add SSH key**. + + + +#### Update Alternatives +Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the "python" command instead of "python3" so we need to create a new executable, "/usr/bin/python" that will call Python3 (basically use the command "python" to call Python3): + +```bash +sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 +``` + +#### Additional Software + +``` +sudo apt install jupyter-notebook +jupyter contrib nbextension install --user +``` + + + +### ROS Noetic +At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the master. All of these instructions are adapted from the [ROS wiki](http://wiki.ros.org/noetic/Installation/Ubuntu). ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal. + +#### Installation +Accept software from packages.ros.org: + +```bash +sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' +``` + +Set up keys: + +```bash +sudo apt install curl # if you haven't already installed curl +curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - +``` + +Install ROS Noetic: + +```bash +sudo apt update +sudo apt -y install ros-noetic-desktop-full +``` + +The full version provides theminimum packaging, build, communications libraries, GUI tools, and 2D/3D simulation and perception packages. + +Install ROS dependencies for building packages: + +```bash +sudo apt -y install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip xterm build-essential +``` + +Initialize rosdep + +```bash +sudo rosdep init +rosdep update +``` + +Source the ROS setup file: +```bash +source /opt/ros/noetic/setup.bash +``` + +Create your ROS workspace: + +```bash +mkdir -p ~/master_ws/src +cd ~/master_ws/ +catkin_make +``` + +Setup ROS environment variables and setup scripts within the `~/.bashrc` file. Open the `~/.bashrc` file with your favorite command line editor and add the following to the bottom: + +```bash +source /opt/ros/noetic/setup.bash +source ~/master_ws/devel/setup.bash +export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share +export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes +export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master +export EDITOR='nano -w' # replace with editor of choice used with rosed command +export TURTLEBOT3_MODEL=burger +export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR +``` + +Any time you make changes to your `~/.bashrc` file you must source it: + +```bash +source ~/.bashrc +``` + +#### Dependencies +There are a number of ROS packages required to operate the TurtleBot3. + +##### ROS Dependencies +```bash +sudo apt-get install ros-noetic-joy ros-noetic-teleop-twist-joy \ + ros-noetic-teleop-twist-keyboard ros-noetic-laser-proc \ + ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \ + ros-noetic-rosserial-python ros-noetic-rosserial-client \ + ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \ + ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \ + ros-noetic-compressed-image-transport ros-noetic-rqt* ros-noetic-rviz \ + ros-noetic-gmapping ros-noetic-navigation ros-noetic-interactive-markers + +``` + +##### TurtleBot3 Dependencies +```bash +sudo apt install ros-noetic-dynamixel-sdk ros-noetic-turtlebot3-msgs ros-noetic-turtlebot3 +``` + +##### [Simulation](https://emanual.robotis.com/docs/en/platform/turtlebot3/simulation/#gazebo-simulation): +```bash +cd ~/master_ws/src +git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git +``` + +##### [ECE387 Curriculum](https://github.com/AF-ROBOTICS/ece387_curriculum) +```bash +git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git +``` + +The **ece387_curriculum** package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool: + +```bash +cd ~/master_ws +rosdep install --from-paths src --ignore-src -r -y +``` + +This may take a while. + +Now we can make and source our workspace: + +```bash +catkin_make +source ~/.bashrc +``` + +The last set of dependencies we need to install are Python dependencies. These are listed within our **ece387_curriculum** package and can be installed using the `pip3` tool: + +```bash +roscd ece387_curriculum +pip3 install -r requirements.txt +``` + +> 📝️ **Note:** the "dlib" package will take quite a while to install. + diff --git a/_sources/Appendix/RobotSetup.md b/_sources/Appendix/RobotSetup.md old mode 100755 new mode 100644 index 9bcb5e5..75d6747 --- a/_sources/Appendix/RobotSetup.md +++ b/_sources/Appendix/RobotSetup.md @@ -1,691 +1,691 @@ -# Robot Setup - -This guide will walk through the steps to install Ubuntu Server 20.04 LTS, ROS Noetic, and all dependencies on a Raspberry Pi 4 B. This Pi is then embedded within the Robotis TurtleBot3 Burger along with a USB Camera. The robotics system, TurtleBot3, is utilized in the United States Air Force Academy's Electrical and Computer Engineering department to teach undergraduate students robotics. You can follow the below steps or a Raspberry Pi image can be provided by emailing Steven Beyer (sbeyer@beyersbots.com). This guide is adapted from the [TurtleBot3 e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/#overview). - -## Hardware -Below is a list of recommended hardware and links. Other off-the-shelf components can replace the ones below. - -- [TurtleBot3](https://www.robotis.us/turtlebot-3/) -- [USB Camera](https://www.adesso.com/product/cybertrack-h4-1080p-hd-usb-webcam-with-built-in-microphone/) (Any USB Cam will work, this is the one we use) -- 128 GB High Speed MicroSD card -- Monitor, mouse, and keyboard -- If using an older version of the TurtleBot3 with a Jetson Nano or Raspberry Pi 3 B+ you will need to purchase a [Raspberry Pi 4 Model B](https://www.canakit.com/raspberry-pi-4-8gb.html) (preferably with 8 GB of RAM)) - - -### Hardware Assembly -Follow the [Robotis e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/hardware_setup/#hardware-assembly) for hardware assembly stopping after installing the Raspberry Pi. - - -### Raspberry Pi -A Raspberry Pi 4 B with 8 GB of RAM is used throughout this curriculum. Ensure heat sinks are propertly installed on the Pi such as these from [CanaKit](https://www.canakit.com/raspberry-pi-4-heat-sinks.html). - -Also, a small fan can be installed to help with cooling. We used this 3D printed bracket to mount the fan. - -```{image} ./Figures/fan.jpg -:width: 380 -:align: center -``` - - -### Camera -After installing the Raspberry pi level of the TurtleBot3 you need to install the USB Camera Mount prior to finishing the robot build. The mount used in this course can be found in the [curriculum material](../stl/burger_usbcam_mount.stl) and is installed on two of the front standoffs on the TurtleBot3. - -```{image} ./Figures/camera_mount.jpg -:width: 380 -:align: center -``` - - -## Software -### Download Ubuntu and flash MicroSD card -There are multiple ways to download and install Ubuntu 20 to a MicroSD card, but the Raspberry Pi Imager is one of the easiest. Instructions for installing the imager on your operating system can be found on the [Raspberry Pi OS software page](https://www.raspberrypi.com/software/). - -Once installed, start the imager and select the "CHOOSE OS" button. -```{image} ./Figures/installer1.png -:width: 480 -:align: center -``` -
    - -Scroll down the menu and select "Other general purpose OS". -
    - -```{image} ./Figures/installer2.png -:width: 480 -:align: center -``` -
    -Next, select "Ubuntu". - -```{image} ./Figures/installer3.png -:width: 480 -:align: center -``` -
    -Lastly, scroll and select the latest 64-bit version of "Ubuntu Server 22.04 LTS". - -
    - -```{image} ./Figures/installer4.png -:width: 480 -:align: center -``` -
    - -Now that you have the correct image selected, you need to choose the correct storage device that corresponds to the MicroSD card. Select "CHOOSE STORAGE". - -```{warning} -This process will overwrite the drive, so ensure you select the correct device! You can select "CHOOSE STORAGE" before inserting the MicroSD card, then insert it, and the card will be the new drive that pops up. -``` - -Once you are sure the correct drive is selected, click "WRITE". - -Once complete you should have an Ubuntu SD card! Ensure your Raspberry Pi is powered off, connected to a monitor, keyboard, and mouse, and insert the SD card. - - -### Ubuntu Setup -#### Login and changing password -Once Ubuntu boots up you will be prompted to enter the login and password. It may take a few minutes on first boot to configure the default username and password, so if login fails, try again after a few minutes. The default username is **ubuntu** and password is **ubuntu**. - -On first login, you will be prompted to change the password. Enter the current password, **ubuntu**, and then enter a new password twice. - - -#### Changing username (optional) -I like to change the username to "pi" so I remember that this machine is a Raspberry Pi. This is optional and you can change the username to anything you like. - -First, add a *temp* user: -```bash -sudo adduser temp -``` -Enter an easy to remember password, and then hit enter until you are back at the terminal prompt. - -Add the *temp* user to the *sudo* group: -```bash -sudo adduser temp sudo -``` - -Log out of *ubuntu* user: -```bash -exit -``` - -Login to *temp* user account. - -Change the *ubuntu* username to the new username: - -```bash -sudo usermod -l newUsername ubuntu -sudo usermod -d /home/newHomeDir -m newUsername -``` - -For example: -```bash -sudo usermod -l pi ubuntu -sudo suermod -d /home/pi -m pi -``` - -Log out of *temp* user and log in with new username and password (the password is still the same as the password you set for the *ubuntu* user). - -Delete the *temp* user: -```bash -sudo deluser temp -sudo rm -r /home/temp -``` - -Now at the terminal prompt you should see `pi@ubuntu:` and if you type `pwd` you should see `/home/pi` (with `pi` replaced with the username you chose). - -#### Change hostname -If you have multiple robots on your network it is good to give each a unique hostname. We number each robot from 0-n and each robot has a corresponding hostname (e.g., robot0). - -Change the hostname with the command line editor of your choice. -```bash -sudo nano /etc/hostname -``` - -Replace `ubuntu` with the hostname of choice, such as robot0. Save and exit. - -The new hostname will not take effect until reboot. Don't reboot yet, though! We have a couple more things to accomplish before reboot. - - -#### Set up Wi-Fi -Until a desktop GUI is installed we have to work with the command line to set up the Wi-Fi. This is the most reliable method I have found and we will delete these changes once a GUI is installed. - -First, determine the name of your Wi-Fi network adapter by typing `ip link` (for the Raspberry Pi version of Ubuntu Server 20.04 LTS it is typically `wlan0`). - -```bash -pi@ubuntu:~$ ip link -1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 -2: eth0: mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000 - link/ether e4:5f:01:15:5b:30 brd ff:ff:ff:ff:ff:ff -3: wlan0: mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000 - link/ether e4:5f:01:15:5b:31 brd ff:ff:ff:ff:ff:ff -``` - -Open the `/etc/netplan/50-cloud-init.yaml` file in your favorite browser: - -```bash -sudo nano /etc/netplan/50-cloud-init.yaml -``` - -Edit the file so it looks like the below (use spaces and not tabs) replacing **wlan0** with your wireless network interface and using your SSID and password: - -``` -# This file is generated from information provided by the datasource. Changes -# to it will not persist across an instance reboot. To disable cloud-init's -# network configuration capabilities, write a file -# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: -# network: {config: disabled} -network: - ethernets: - eth0: - dhcp4: true - optional: true - version: 2 - wifis: - wlan0: - optional: true - access-points: - "YOUR-SSID": - password: "YOUR-PASSWORD" - dhcp4: true -``` - -Save and exit. - -Apply your changes using the following command: -```bash -sudo netplan apply -``` - -Alternatively, you can reboot your system and the changes will be automatically applied once the system boots. - -**Optional:** It may be beneficial to setup a static IP address. To do this you need to determine your subnet and gateway. - -Determine subnet and gateway addresses: -``` -ubuntu@ubuntu:~$ ip route -default via 192.168.0.1 dev wlan0 proto static -192.168.0.1/24 dev wlan0 proto kernel scope link src 192.168.0.201 -``` - -Set static IP within subnet range: - -``` -# This file is generated from information provided by the datasource. Changes -# to it will not persist across an instance reboot. To disable cloud-init's -# network configuration capabilities, write a file -# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: -# network: {config: disabled} -network: - ethernets: - eth0: - dhcp4: true - optional: true - version: 2 - wifis: - wlan0: - dhcp4: no - access-points: - "robotics_5GHz": - password: "YOUR-PASSWORD" - addresses: - - 192.168.4.208/24 - routes: - - to: default - via: 192.168.4.1 - nameservers: - addresses: [192.168.4.1, 8.8.8.8, 1.1.1.1] - optional: true -``` - - -#### Disable Automatic Updates -Ubuntu will attempt to apply system updates in the background. This has caused issues in the past with ROS dependencies and keys. Disabling automatic updates allows you to control when Ubuntu installs updates. While this is not a good habit for general computer security, it is fine for this application of an embedded robotics system. Ensure you periodically update and upgrade your system. - -Open the auto updater configuration file using sudoedit: -```bash -sudoedit /etc/apt/apt.conf.d/20auto-upgrades -``` - -Change the content from: -``` -APT::Periodic::Update-Package-Lists "1"; -APT::Periodic::Unattended-Upgrade "1"; -``` - -to: -``` -APT::Periodic::Update-Package-Lists "0"; -APT::Periodic::Unattended-Upgrade "0"; -APT::Periodic::AutocleanInterval "0"; -APT::Periodic::Download-Upgradeable-Packages "0"; -``` - -Set the systemd to prevent boot-up delay even if there is no network at startup. Run the command below to set mask the systemd process using the following command. -``` -$ systemctl mask systemd-networkd-wait-online.service -``` - -Disable Suspend and Hibernation -``` -$ sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target -``` - -Reboot the Raspberry Pi. -``` -$ reboot -``` - - -#### Enable SSH and generate new keys -```bash -sudo ssh-keygen -A -sudo systemctl start ssh -``` - - -#### Add Swap Space (optional) -The Raspberry Pi 4 B used in our course has 8 GB of RAM. Swap Space might not be necessary, but with a larger SD card it is beneficial. - -You can check that there is no active swap using the free utility: -```bash -pi@ubuntu:~$ free -h - total used free shared buff/cache available -Mem: 7.6Gi 201Mi 7.1Gi 3.0Mi 328Mi 7.3Gi -Swap: 0B 0B 0B -``` - - -The **fallocate** program can be used to create a swap: - -```bash -sudo fallocate -l 2G /swapfile -``` - -If it was created correctly, you should see the below: -```bash -pi@ubuntu:~$ ls -lh /swapfile --rw------- 1 root root 2.0G Aug 19 17:30 /swapfile -``` - -Make the file only accessible to root by typing: - -```bash -sudo chmod 600 /swapfile -``` - - -Verify the permissions by typing the following: - -```bash -pi@ubuntu:~$ ls -lh /swapfile --rw------- 1 root root 2.0G Aug 19 17:28 /swapfile -Now only root user has read and write flags enabled. -``` - -You can set the file as swap space by typing the following: - -```bash -pi@ubuntu:~$ sudo mkswap /swapfile -Setting up swapspace version 1, size = 2 GiB (2147479552 bytes) -no label, UUID=b5bc4abf-2bce-419e-870d-4d44a7a05778 -``` - -Then turn on the swap file: - -```bash -sudo swapon /swapfile -``` - -To verify that this worked you can type the following: - -```bash -pi@ubuntu:~$ sudo swapon --show -NAME TYPE SIZE USED PRIO -/swapfile file 2G 0B -2 -``` - -This swap will only last until reboot, so to make it permanent at it to the `fstab` file: -```bash -echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab -``` - -Now it is time to reboot by typing - -```shell -sudo reboot -``` - - -#### Verify changes -After reboot and you log in your new hostname should be listed at the terminal (e.g., `pi@robot0`). Additionally, you should be connected to Wi-Fi and have an IP Address. You can confirm by typing the following and observing the IP address in the output: - -![logo](Figures/wifi3.png) - -You can now use this IP address to create a remote secure shell into the TurtleBot3 using either the IP address or hostname if your network provides Dynamic DNS. From another machine connected to your network type one of the following: - -```bash -ssh username@IP_ADDRESS -``` -or -```bash -ssh username@HOSTNAME -``` - -Lastly, ensure your swap space is still active by typing the following and observing the output: - -```bash -pi@robot99:~$ free -h - total used free shared buff/cache available -Mem: 7.6Gi 224Mi 6.5Gi 3.0Mi 903Mi 7.3Gi -Swap: 2.0Gi 0B 2.0Gi -``` - - -#### Update and Upgrade -Since we turned off automatic updates, you should periodically update and upgrade. You can use this single command to accomplish both while accepting all upgrades: - -```bash -sudo apt update && sudo apt -y upgrade -``` - -#### Install Ubuntu Desktop (optional) -A desktop GUI is not necessary for a remote machine like the USAFABot and will take up about 1.4 GB of RAM to run. I include directions for installing the Ubuntu GNOME 3 desktop environment for completeness and flexibility. The following will install the environment while confirming the installation: - -```bash -sudo apt -y install ubuntu-desktop -``` - - -#### Network Settings -If you do install the Ubuntu Desktop and want to use the GUI to setup the Wi-Fi network then you need to remove the settings included in the `/etc/netplan/50-cloud-init.yaml` file. It should look like the original file when complete: - -``` -# This file is generated from information provided by the datasource. Changes -# to it will not persist across an instance reboot. To disable cloud-init's -# network configuration capabilities, write a file -# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: -# network: {config: disabled} -network: - ethernets: - eth0: - dhcp4: true - optional: true - version: 2 - wifis: - wlan0: - dhcp4: true - optional: true -``` - -You can now use the GUI interface in the top right of the screen to set up a Wi-Fi connection. - - -#### Setup GitHub SSH Keys -The following assumes you already have a GitHub account. - -Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login: - -```bash -cd -ssh-keygen -t ed25519 -C "github@email.com" -``` - -When prompted to "Enter a file in which to save the key", hit **enter**. - -At the prompt, type a secure password. - -Start the ssh-agent in the background and add your SSH private key to the ssh-agent: - -```bash -eval "$(ssh-agent -s)" -ssh-add ~/.ssh/id_ed25519 -``` - -Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account). - -```bash -nano ~/.ssh/id_ed25519.pub -``` - -Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email). - -Open a web browser and sign in to your GitHub account. - -In the upper-right corner of any page, click your profile photo, then click **Settings**: - -```{image} ./Figures/ssh1.png -:width: 240 -:align: center -``` -
    - -In the user settings sidebar, click **SSH and GPG keys**: - -
    - -```{image} ./Figures/ssh2.png -:width: 240 -:align: center -``` -
    - -Click **New SSH key**: -
    - -```{image} ./Figures/ssh3.png -:width: 640 -:align: center -``` -
    - - -In the `Title` field, add a descriptive label for the new key, such as "robot0". - -Paste your key into the `Key` field (contents of the `.pub` file). - -Click **Add SSH key**. - - -#### Update Alternatives -Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the "python" command instead of "python3" so we need to create a new executable, "/usr/bin/python" that will call the Python3 (basically use the command "python" to call Python3): - -```bash -sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 -``` - - -### ROS Noetic -At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the TurtleBot3. All of these instructions are adapted from the [ROS wiki](http://wiki.ros.org/noetic/Installation/Ubuntu). ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal. - -#### Installation -Accept software from packages.ros.org: - -```bash -sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' -``` - -Set up keys: - -```bash -sudo apt install curl # if you haven't already installed curl -curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - -``` - -Install ROS Noetic: - -```bash -sudo apt update -sudo apt -y install ros-noetic-ros-base -``` - -The base version provides the Bare Bones of ROS to include minimum packaging, build, and communications libraries. No GUI tools are installed. As the Raspberry Pi is embedded into the USAFABot it is ideal to keep overhead as low as possible. Many of the GUI tools will be ran on the main machine. - -Install ROS dependencies for building packages: - -```bash -sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip build-essential -``` - -Initialize rosdep - -```bash -sudo rosdep init -rosdep update -``` - -Source the ROS setup file: -```bash -source /opt/ros/noetic/setup.bash -``` - -Create your ROS workspace: - -```bash -mkdir -p ~/robot_ws/src -cd ~/robot_ws/ -catkin_make -``` - -Setup ROS environment variables and setup scripts within the `~/.bashrc` file. Open the `~/.bashrc` file with your favorite command line editor and add the following to the bottom: - -```bash -source /opt/ros/noetic/setup.bash -source ~/robot_ws/devel/setup.bash -export ROS_PACKAGE_PATH=~/robot_ws/src:/opt/ros/noetic/share -export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes -export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master -export EDITOR='nano -w' # replace with editor of choice used with rosed command -export TURTLEBOT3_MODEL=burger -export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR -``` - -Any time you make changes to your `~/.bashrc` file you must source it: - -```bash -source ~/.bashrc -``` - - -#### Dependencies -There are a number of ROS packages required to operate the TurtleBot3. - -##### ROS Dependencies - -```bash -sudo apt-get install ros-noetic-laser-proc ros-noetic-hls-lfcd-lds-driver \ - ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \ - ros-noetic-rosserial-python ros-noetic-rosserial-client \ - ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \ - ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \ - ros-noetic-compressed-image-transport ros-noetic-gmapping \ - ros-noetic-navigation ros-noetic-interactive-markers -``` - -NOTE: We may not need the following packages on the robot. -```bash -sudo apt-get install ros-noetic-rqt* ros-noetic-rviz -``` - - -##### TurtleBot3 Dependencies -```bash -sudo apt install libudev-dev ros-noetic-turtlebot3-msgs -cd ~/robot_ws/src -git clone -b develop https://github.com/ROBOTIS-GIT/ld08_driver.git -git clone https://github.com/ROBOTIS-GIT/turtlebot3.git -git clone https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git -``` - -##### [ECE387 Curriculum](https://github.com/AF-ROBOTICS/ece387_curriculum) -```bash -git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git -``` - -The **ece387_curriculum** package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool: - -```bash -cd ~/robot_ws -rosdep install --from-paths src --ignore-src -r -y -``` - -This will take a while. - -Now we can make and source our workspace: - -```bash -cd ~/robot_ws -catkin_make -source ~/.bashrc -``` - -The last set of dependencies we need to install are Python dependencies. These are listed within our **ece387_curriculum** package and can be installed using the `pip3` tool: - -```bash -roscd ece387_curriculum -pip3 install -r requirements.txt -``` - -> 📝️ **Note:** the "dlib" package will take quite a while to install. - - - -### [Updating OpenCR firmware](https://emanual.robotis.com/docs/en/platform/turtlebot3/opencr_setup/#opencr-setup) -The last step is updating the firmware for the OpenCR controller board. - -Install required packages on the Raspberry Pi -```bash -sudo dpkg --add-architecture armhf -sudo apt-get update -sudo apt-get install libc6:armhf -``` - -Setup the OpenCR model name: -```bash -export OPENCR_PORT=/dev/ttyACM0 -export OPENCR_MODEL=burger_noetic -``` - -Download the firmware and loader, then extract the file: -```bash -wget https://github.com/ROBOTIS-GIT/OpenCR-Binaries/raw/master/turtlebot3/ROS1/latest/opencr_update.tar.bz2 -tar -xvf opencr_update.tar.bz2 -rm -rf ./opencr_update.tar.bz2 -``` - -Upload firmware to the OpenCR: -```bash -cd ./opencr_update -./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr -``` - -A successful firmware upload for TurtleBot3 Burger will look like: -```bash -pi@robot8: ~ -$ cd opencr_update/ -pi@robot8: ~/opencr_update -$ ./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr -aarch64 -arm -OpenCR Update Start.. -opencr_ld_shell ver 1.0.0 -opencr_ld_main -[ ] file name : burger_noetic.opencr -[ ] file size : 183 KB -[ ] fw_name : burger_noetic -[ ] fw_ver : 1.2.6 -[OK] Open port : /dev/ttyACM0 -[ ] -[ ] Board Name : OpenCR R1.0 -[ ] Board Ver : 0x17020800 -[ ] Board Rev : 0x00000000 -[OK] flash_erase : 0.99s -[OK] flash_write : 1.60s -[OK] CRC Check : 12A5C20 12A5C20 , 0.005000 sec -[OK] Download -[OK] jump_to_fw -``` - -If not successful, attempt the debug methods in the [OpenCR Setup](https://emanual.robotis.com/docs/en/platform/turtlebot3/opencr_setup/#opencr-setup) guide. - +# Robot Setup + +This guide will walk through the steps to install Ubuntu Server 20.04 LTS, ROS Noetic, and all dependencies on a Raspberry Pi 4 B. This Pi is then embedded within the Robotis TurtleBot3 Burger along with a USB Camera. The robotics system, TurtleBot3, is utilized in the United States Air Force Academy's Electrical and Computer Engineering department to teach undergraduate students robotics. You can follow the below steps or a Raspberry Pi image can be provided by emailing Steven Beyer (sbeyer@beyersbots.com). This guide is adapted from the [TurtleBot3 e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/#overview). + +## Hardware +Below is a list of recommended hardware and links. Other off-the-shelf components can replace the ones below. + +- [TurtleBot3](https://www.robotis.us/turtlebot-3/) +- [USB Camera](https://www.adesso.com/product/cybertrack-h4-1080p-hd-usb-webcam-with-built-in-microphone/) (Any USB Cam will work, this is the one we use) +- 128 GB High Speed MicroSD card +- Monitor, mouse, and keyboard +- If using an older version of the TurtleBot3 with a Jetson Nano or Raspberry Pi 3 B+ you will need to purchase a [Raspberry Pi 4 Model B](https://www.canakit.com/raspberry-pi-4-8gb.html) (preferably with 8 GB of RAM)) + + +### Hardware Assembly +Follow the [Robotis e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/hardware_setup/#hardware-assembly) for hardware assembly stopping after installing the Raspberry Pi. + + +### Raspberry Pi +A Raspberry Pi 4 B with 8 GB of RAM is used throughout this curriculum. Ensure heat sinks are propertly installed on the Pi such as these from [CanaKit](https://www.canakit.com/raspberry-pi-4-heat-sinks.html). + +Also, a small fan can be installed to help with cooling. We used this 3D printed bracket to mount the fan. + +```{image} ./Figures/fan.jpg +:width: 380 +:align: center +``` + + +### Camera +After installing the Raspberry pi level of the TurtleBot3 you need to install the USB Camera Mount prior to finishing the robot build. The mount used in this course can be found in the [curriculum material](../stl/burger_usbcam_mount.stl) and is installed on two of the front standoffs on the TurtleBot3. + +```{image} ./Figures/camera_mount.jpg +:width: 380 +:align: center +``` + + +## Software +### Download Ubuntu and flash MicroSD card +There are multiple ways to download and install Ubuntu 20 to a MicroSD card, but the Raspberry Pi Imager is one of the easiest. Instructions for installing the imager on your operating system can be found on the [Raspberry Pi OS software page](https://www.raspberrypi.com/software/). + +Once installed, start the imager and select the "CHOOSE OS" button. +```{image} ./Figures/installer1.png +:width: 480 +:align: center +``` +
    + +Scroll down the menu and select "Other general purpose OS". +
    + +```{image} ./Figures/installer2.png +:width: 480 +:align: center +``` +
    +Next, select "Ubuntu". + +```{image} ./Figures/installer3.png +:width: 480 +:align: center +``` +
    +Lastly, scroll and select the latest 64-bit version of "Ubuntu Server 22.04 LTS". + +
    + +```{image} ./Figures/installer4.png +:width: 480 +:align: center +``` +
    + +Now that you have the correct image selected, you need to choose the correct storage device that corresponds to the MicroSD card. Select "CHOOSE STORAGE". + +```{warning} +This process will overwrite the drive, so ensure you select the correct device! You can select "CHOOSE STORAGE" before inserting the MicroSD card, then insert it, and the card will be the new drive that pops up. +``` + +Once you are sure the correct drive is selected, click "WRITE". + +Once complete you should have an Ubuntu SD card! Ensure your Raspberry Pi is powered off, connected to a monitor, keyboard, and mouse, and insert the SD card. + + +### Ubuntu Setup +#### Login and changing password +Once Ubuntu boots up you will be prompted to enter the login and password. It may take a few minutes on first boot to configure the default username and password, so if login fails, try again after a few minutes. The default username is **ubuntu** and password is **ubuntu**. + +On first login, you will be prompted to change the password. Enter the current password, **ubuntu**, and then enter a new password twice. + + +#### Changing username (optional) +I like to change the username to "pi" so I remember that this machine is a Raspberry Pi. This is optional and you can change the username to anything you like. + +First, add a *temp* user: +```bash +sudo adduser temp +``` +Enter an easy to remember password, and then hit enter until you are back at the terminal prompt. + +Add the *temp* user to the *sudo* group: +```bash +sudo adduser temp sudo +``` + +Log out of *ubuntu* user: +```bash +exit +``` + +Login to *temp* user account. + +Change the *ubuntu* username to the new username: + +```bash +sudo usermod -l newUsername ubuntu +sudo usermod -d /home/newHomeDir -m newUsername +``` + +For example: +```bash +sudo usermod -l pi ubuntu +sudo suermod -d /home/pi -m pi +``` + +Log out of *temp* user and log in with new username and password (the password is still the same as the password you set for the *ubuntu* user). + +Delete the *temp* user: +```bash +sudo deluser temp +sudo rm -r /home/temp +``` + +Now at the terminal prompt you should see `pi@ubuntu:` and if you type `pwd` you should see `/home/pi` (with `pi` replaced with the username you chose). + +#### Change hostname +If you have multiple robots on your network it is good to give each a unique hostname. We number each robot from 0-n and each robot has a corresponding hostname (e.g., robot0). + +Change the hostname with the command line editor of your choice. +```bash +sudo nano /etc/hostname +``` + +Replace `ubuntu` with the hostname of choice, such as robot0. Save and exit. + +The new hostname will not take effect until reboot. Don't reboot yet, though! We have a couple more things to accomplish before reboot. + + +#### Set up Wi-Fi +Until a desktop GUI is installed we have to work with the command line to set up the Wi-Fi. This is the most reliable method I have found and we will delete these changes once a GUI is installed. + +First, determine the name of your Wi-Fi network adapter by typing `ip link` (for the Raspberry Pi version of Ubuntu Server 20.04 LTS it is typically `wlan0`). + +```bash +pi@ubuntu:~$ ip link +1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 +2: eth0: mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000 + link/ether e4:5f:01:15:5b:30 brd ff:ff:ff:ff:ff:ff +3: wlan0: mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000 + link/ether e4:5f:01:15:5b:31 brd ff:ff:ff:ff:ff:ff +``` + +Open the `/etc/netplan/50-cloud-init.yaml` file in your favorite browser: + +```bash +sudo nano /etc/netplan/50-cloud-init.yaml +``` + +Edit the file so it looks like the below (use spaces and not tabs) replacing **wlan0** with your wireless network interface and using your SSID and password: + +``` +# This file is generated from information provided by the datasource. Changes +# to it will not persist across an instance reboot. To disable cloud-init's +# network configuration capabilities, write a file +# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: +# network: {config: disabled} +network: + ethernets: + eth0: + dhcp4: true + optional: true + version: 2 + wifis: + wlan0: + optional: true + access-points: + "YOUR-SSID": + password: "YOUR-PASSWORD" + dhcp4: true +``` + +Save and exit. + +Apply your changes using the following command: +```bash +sudo netplan apply +``` + +Alternatively, you can reboot your system and the changes will be automatically applied once the system boots. + +**Optional:** It may be beneficial to setup a static IP address. To do this you need to determine your subnet and gateway. + +Determine subnet and gateway addresses: +``` +ubuntu@ubuntu:~$ ip route +default via 192.168.0.1 dev wlan0 proto static +192.168.0.1/24 dev wlan0 proto kernel scope link src 192.168.0.201 +``` + +Set static IP within subnet range: + +``` +# This file is generated from information provided by the datasource. Changes +# to it will not persist across an instance reboot. To disable cloud-init's +# network configuration capabilities, write a file +# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: +# network: {config: disabled} +network: + ethernets: + eth0: + dhcp4: true + optional: true + version: 2 + wifis: + wlan0: + dhcp4: no + access-points: + "robotics_5GHz": + password: "YOUR-PASSWORD" + addresses: + - 192.168.4.208/24 + routes: + - to: default + via: 192.168.4.1 + nameservers: + addresses: [192.168.4.1, 8.8.8.8, 1.1.1.1] + optional: true +``` + + +#### Disable Automatic Updates +Ubuntu will attempt to apply system updates in the background. This has caused issues in the past with ROS dependencies and keys. Disabling automatic updates allows you to control when Ubuntu installs updates. While this is not a good habit for general computer security, it is fine for this application of an embedded robotics system. Ensure you periodically update and upgrade your system. + +Open the auto updater configuration file using sudoedit: +```bash +sudoedit /etc/apt/apt.conf.d/20auto-upgrades +``` + +Change the content from: +``` +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; +``` + +to: +``` +APT::Periodic::Update-Package-Lists "0"; +APT::Periodic::Unattended-Upgrade "0"; +APT::Periodic::AutocleanInterval "0"; +APT::Periodic::Download-Upgradeable-Packages "0"; +``` + +Set the systemd to prevent boot-up delay even if there is no network at startup. Run the command below to set mask the systemd process using the following command. +``` +$ systemctl mask systemd-networkd-wait-online.service +``` + +Disable Suspend and Hibernation +``` +$ sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target +``` + +Reboot the Raspberry Pi. +``` +$ reboot +``` + + +#### Enable SSH and generate new keys +```bash +sudo ssh-keygen -A +sudo systemctl start ssh +``` + + +#### Add Swap Space (optional) +The Raspberry Pi 4 B used in our course has 8 GB of RAM. Swap Space might not be necessary, but with a larger SD card it is beneficial. + +You can check that there is no active swap using the free utility: +```bash +pi@ubuntu:~$ free -h + total used free shared buff/cache available +Mem: 7.6Gi 201Mi 7.1Gi 3.0Mi 328Mi 7.3Gi +Swap: 0B 0B 0B +``` + + +The **fallocate** program can be used to create a swap: + +```bash +sudo fallocate -l 2G /swapfile +``` + +If it was created correctly, you should see the below: +```bash +pi@ubuntu:~$ ls -lh /swapfile +-rw------- 1 root root 2.0G Aug 19 17:30 /swapfile +``` + +Make the file only accessible to root by typing: + +```bash +sudo chmod 600 /swapfile +``` + + +Verify the permissions by typing the following: + +```bash +pi@ubuntu:~$ ls -lh /swapfile +-rw------- 1 root root 2.0G Aug 19 17:28 /swapfile +Now only root user has read and write flags enabled. +``` + +You can set the file as swap space by typing the following: + +```bash +pi@ubuntu:~$ sudo mkswap /swapfile +Setting up swapspace version 1, size = 2 GiB (2147479552 bytes) +no label, UUID=b5bc4abf-2bce-419e-870d-4d44a7a05778 +``` + +Then turn on the swap file: + +```bash +sudo swapon /swapfile +``` + +To verify that this worked you can type the following: + +```bash +pi@ubuntu:~$ sudo swapon --show +NAME TYPE SIZE USED PRIO +/swapfile file 2G 0B -2 +``` + +This swap will only last until reboot, so to make it permanent at it to the `fstab` file: +```bash +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab +``` + +Now it is time to reboot by typing + +```shell +sudo reboot +``` + + +#### Verify changes +After reboot and you log in your new hostname should be listed at the terminal (e.g., `pi@robot0`). Additionally, you should be connected to Wi-Fi and have an IP Address. You can confirm by typing the following and observing the IP address in the output: + +![logo](Figures/wifi3.png) + +You can now use this IP address to create a remote secure shell into the TurtleBot3 using either the IP address or hostname if your network provides Dynamic DNS. From another machine connected to your network type one of the following: + +```bash +ssh username@IP_ADDRESS +``` +or +```bash +ssh username@HOSTNAME +``` + +Lastly, ensure your swap space is still active by typing the following and observing the output: + +```bash +pi@robot99:~$ free -h + total used free shared buff/cache available +Mem: 7.6Gi 224Mi 6.5Gi 3.0Mi 903Mi 7.3Gi +Swap: 2.0Gi 0B 2.0Gi +``` + + +#### Update and Upgrade +Since we turned off automatic updates, you should periodically update and upgrade. You can use this single command to accomplish both while accepting all upgrades: + +```bash +sudo apt update && sudo apt -y upgrade +``` + +#### Install Ubuntu Desktop (optional) +A desktop GUI is not necessary for a remote machine like the USAFABot and will take up about 1.4 GB of RAM to run. I include directions for installing the Ubuntu GNOME 3 desktop environment for completeness and flexibility. The following will install the environment while confirming the installation: + +```bash +sudo apt -y install ubuntu-desktop +``` + + +#### Network Settings +If you do install the Ubuntu Desktop and want to use the GUI to setup the Wi-Fi network then you need to remove the settings included in the `/etc/netplan/50-cloud-init.yaml` file. It should look like the original file when complete: + +``` +# This file is generated from information provided by the datasource. Changes +# to it will not persist across an instance reboot. To disable cloud-init's +# network configuration capabilities, write a file +# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: +# network: {config: disabled} +network: + ethernets: + eth0: + dhcp4: true + optional: true + version: 2 + wifis: + wlan0: + dhcp4: true + optional: true +``` + +You can now use the GUI interface in the top right of the screen to set up a Wi-Fi connection. + + +#### Setup GitHub SSH Keys +The following assumes you already have a GitHub account. + +Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login: + +```bash +cd +ssh-keygen -t ed25519 -C "github@email.com" +``` + +When prompted to "Enter a file in which to save the key", hit **enter**. + +At the prompt, type a secure password. + +Start the ssh-agent in the background and add your SSH private key to the ssh-agent: + +```bash +eval "$(ssh-agent -s)" +ssh-add ~/.ssh/id_ed25519 +``` + +Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account). + +```bash +nano ~/.ssh/id_ed25519.pub +``` + +Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email). + +Open a web browser and sign in to your GitHub account. + +In the upper-right corner of any page, click your profile photo, then click **Settings**: + +```{image} ./Figures/ssh1.png +:width: 240 +:align: center +``` +
    + +In the user settings sidebar, click **SSH and GPG keys**: + +
    + +```{image} ./Figures/ssh2.png +:width: 240 +:align: center +``` +
    + +Click **New SSH key**: +
    + +```{image} ./Figures/ssh3.png +:width: 640 +:align: center +``` +
    + + +In the `Title` field, add a descriptive label for the new key, such as "robot0". + +Paste your key into the `Key` field (contents of the `.pub` file). + +Click **Add SSH key**. + + +#### Update Alternatives +Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the "python" command instead of "python3" so we need to create a new executable, "/usr/bin/python" that will call the Python3 (basically use the command "python" to call Python3): + +```bash +sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 +``` + + +### ROS Noetic +At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the TurtleBot3. All of these instructions are adapted from the [ROS wiki](http://wiki.ros.org/noetic/Installation/Ubuntu). ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal. + +#### Installation +Accept software from packages.ros.org: + +```bash +sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' +``` + +Set up keys: + +```bash +sudo apt install curl # if you haven't already installed curl +curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - +``` + +Install ROS Noetic: + +```bash +sudo apt update +sudo apt -y install ros-noetic-ros-base +``` + +The base version provides the Bare Bones of ROS to include minimum packaging, build, and communications libraries. No GUI tools are installed. As the Raspberry Pi is embedded into the USAFABot it is ideal to keep overhead as low as possible. Many of the GUI tools will be ran on the main machine. + +Install ROS dependencies for building packages: + +```bash +sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip build-essential +``` + +Initialize rosdep + +```bash +sudo rosdep init +rosdep update +``` + +Source the ROS setup file: +```bash +source /opt/ros/noetic/setup.bash +``` + +Create your ROS workspace: + +```bash +mkdir -p ~/robot_ws/src +cd ~/robot_ws/ +catkin_make +``` + +Setup ROS environment variables and setup scripts within the `~/.bashrc` file. Open the `~/.bashrc` file with your favorite command line editor and add the following to the bottom: + +```bash +source /opt/ros/noetic/setup.bash +source ~/robot_ws/devel/setup.bash +export ROS_PACKAGE_PATH=~/robot_ws/src:/opt/ros/noetic/share +export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes +export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master +export EDITOR='nano -w' # replace with editor of choice used with rosed command +export TURTLEBOT3_MODEL=burger +export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR +``` + +Any time you make changes to your `~/.bashrc` file you must source it: + +```bash +source ~/.bashrc +``` + + +#### Dependencies +There are a number of ROS packages required to operate the TurtleBot3. + +##### ROS Dependencies + +```bash +sudo apt-get install ros-noetic-laser-proc ros-noetic-hls-lfcd-lds-driver \ + ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \ + ros-noetic-rosserial-python ros-noetic-rosserial-client \ + ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \ + ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \ + ros-noetic-compressed-image-transport ros-noetic-gmapping \ + ros-noetic-navigation ros-noetic-interactive-markers +``` + +NOTE: We may not need the following packages on the robot. +```bash +sudo apt-get install ros-noetic-rqt* ros-noetic-rviz +``` + + +##### TurtleBot3 Dependencies +```bash +sudo apt install libudev-dev ros-noetic-turtlebot3-msgs +cd ~/robot_ws/src +git clone -b develop https://github.com/ROBOTIS-GIT/ld08_driver.git +git clone https://github.com/ROBOTIS-GIT/turtlebot3.git +git clone https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git +``` + +##### [ECE387 Curriculum](https://github.com/AF-ROBOTICS/ece387_curriculum) +```bash +git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git +``` + +The **ece387_curriculum** package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool: + +```bash +cd ~/robot_ws +rosdep install --from-paths src --ignore-src -r -y +``` + +This will take a while. + +Now we can make and source our workspace: + +```bash +cd ~/robot_ws +catkin_make +source ~/.bashrc +``` + +The last set of dependencies we need to install are Python dependencies. These are listed within our **ece387_curriculum** package and can be installed using the `pip3` tool: + +```bash +roscd ece387_curriculum +pip3 install -r requirements.txt +``` + +> 📝️ **Note:** the "dlib" package will take quite a while to install. + + + +### [Updating OpenCR firmware](https://emanual.robotis.com/docs/en/platform/turtlebot3/opencr_setup/#opencr-setup) +The last step is updating the firmware for the OpenCR controller board. + +Install required packages on the Raspberry Pi +```bash +sudo dpkg --add-architecture armhf +sudo apt-get update +sudo apt-get install libc6:armhf +``` + +Setup the OpenCR model name: +```bash +export OPENCR_PORT=/dev/ttyACM0 +export OPENCR_MODEL=burger_noetic +``` + +Download the firmware and loader, then extract the file: +```bash +wget https://github.com/ROBOTIS-GIT/OpenCR-Binaries/raw/master/turtlebot3/ROS1/latest/opencr_update.tar.bz2 +tar -xvf opencr_update.tar.bz2 +rm -rf ./opencr_update.tar.bz2 +``` + +Upload firmware to the OpenCR: +```bash +cd ./opencr_update +./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr +``` + +A successful firmware upload for TurtleBot3 Burger will look like: +```bash +pi@robot8: ~ +$ cd opencr_update/ +pi@robot8: ~/opencr_update +$ ./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr +aarch64 +arm +OpenCR Update Start.. +opencr_ld_shell ver 1.0.0 +opencr_ld_main +[ ] file name : burger_noetic.opencr +[ ] file size : 183 KB +[ ] fw_name : burger_noetic +[ ] fw_ver : 1.2.6 +[OK] Open port : /dev/ttyACM0 +[ ] +[ ] Board Name : OpenCR R1.0 +[ ] Board Ver : 0x17020800 +[ ] Board Rev : 0x00000000 +[OK] flash_erase : 0.99s +[OK] flash_write : 1.60s +[OK] CRC Check : 12A5C20 12A5C20 , 0.005000 sec +[OK] Download +[OK] jump_to_fw +``` + +If not successful, attempt the debug methods in the [OpenCR Setup](https://emanual.robotis.com/docs/en/platform/turtlebot3/opencr_setup/#opencr-setup) guide. + diff --git a/_sources/Module00.0_IntroductionGIT/IntroGit.md b/_sources/Module00.0_IntroductionGIT/IntroGit.md deleted file mode 100755 index c8f5bb5..0000000 --- a/_sources/Module00.0_IntroductionGIT/IntroGit.md +++ /dev/null @@ -1,99 +0,0 @@ - -# Module 0: Setting Up GIT Repositories - - - -## Purpose -Setup GIT repositories and access on the **Master**. - - -## Create a repo within the GitHub Classroom: - -1. Browse to [github.com](https://www.github.com) and create a GitHub account if you do not already have one. It is useful if your username is something that identifies you (e.g., bneff1013). -1. **One student per group** do the following on your personal computer: - 1. First we are going to setup the repo for the Master. This will allow your instructor to see all of your commits throughout the semester. - 1. Browse to [https://classroom.github.com/a/OoH0u_XW](https://classroom.github.com/a/OoH0u_XW) - 1. Select "Accept this assignment" - 1. You may need to hit refresh, but eventually it will provide you a link to the repository. - 1. Browse to your repository. - 1. Note the url for your repository (save this link, it is the best way to check if your repo is updated). - 1. Go to Settings -> Manage access -> and "Invite teams or people". - 1. Provide access to your team member using their GitHub user name. - 1. Now we need to do the exact same thing to setup the repo for the robot. - 1. Browse to [https://classroom.github.com/a/0MPq4TNE](https://classroom.github.com/a/0MPq4TNE) and repeat steps c-h. - - -## Enable SSH connection to your GitHub account -1. Open a terminal on your **Master** (ctrl+alt+t). -1. The same student as step 1.1.2 do the following: - 1. Generate a new SSH key, substituting your GitHub email address: - ```bash - ssh-keygen -t ed25519 -C "your_email@example.com" - ``` - 1. When you're prompted to "Enter a file in which to save the key," click enter. - 1. At the prompt, type a secure passphrase. - 1. Start the ssh-agent in the background and add your SSH private key to the ssh-agent: - ```bash - eval "$(ssh-agent -s)" - ssh-add ~/.ssh/id_ed25519 - ``` - 1. Open the public key: - ```bash - nano ~/.ssh/id_ed25519.pub - ``` - 1. Select the contents of the file (maximize the window and ensure it has your GIT email at the end), right click, and select copy. - 1. Open a web browser and sign in to your GitHub account. - 1. In the upper-right corner of any page, click your profile photo, then click **Settings**. - ![logo](Figures/userbar-account-settings.png) - 1. In the user settings sidebar, click **SSH and GPG keys**. - ![logo](Figures/settings-sidebar-ssh-keys.png) - 1. Click **New SSH key** - ![logo](Figures/ssh-add-ssh-key.png) - 1. In the "Title" field, add a descriptive label for the new key, such as "MasterX". - 1. Paste your key into the "Key" field (contents of the `.pub` file). - 1. Click **Add SSH key**. - 1. If prompted, confirm your GitHub password. - 1. Create a secure shell connection to your **Robot** (password is dfec3141) - ```bash - ssh pi@robotX - ``` - 1. Repeat steps a-f on your **Robot** and j-n on your **Master**. - - -## Clone repository to your master. -1. On the **Master**, open the GitHub repository and copy your repo address using the SSH mode: - ![logo](Figures/clone.PNG) -1. Open a terminal and browse to your workspace source folder: - ```bash - cd ~/master_ws/src/ - ``` -1. Clone your repo using the username and password used when you generated the SSH key, replacing **USERNAME** with your GitHub username: - ```bash - git clone git@github.com:ECE387/ece387_master_spring202X-USERNAME.git - ``` - -1. Update your git email address and the last name for you and your team mate. - ```bash - git config --global user.email "you@example.com" - git config --global user.name "Lastname1 Lastname2" - ``` - - -## Clone repository to your robot. -1. Create a secure shell connection to your robot: - ```bash - ssh pi@robotX - ``` -1. Ensure you are in the ROS robot workspace src directory. - ```bash - cd robot_ws/src - ``` -1. Clone the robot repository: - ```bash - git clone git@github.com:ECE387/ece387_robot_spring202X-USERNAME.git - ``` -1. Update your git email address and the last name for you and your team mate. - ```bash - git config --global user.email "you@example.com" - git config --global user.name "Lastname1 Lastname2" - ``` diff --git a/_sources/Module00.1_RobotSetup/RobotSetup.md b/_sources/Module00.1_RobotSetup/RobotSetup.md deleted file mode 100755 index 9d75fab..0000000 --- a/_sources/Module00.1_RobotSetup/RobotSetup.md +++ /dev/null @@ -1,656 +0,0 @@ -# Robot Setup - - - -This guide will walk through the steps to install Ubuntu Server 22.04 LTS, ROS2 Humble, and all dependencies on a Raspberry Pi 4B. This Pi is then embedded within the Robotis TurtleBot3 Burger along with a USB camera. The robotics system, TurtleBot3, is utilized in the United States Air Force Academy's Electrical and Computer Engineering department to teach undergraduate students robotics. - -This guide is adapted from the [TurtleBot3 e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/#overview). - ---- - - -## Hardware -Below is a list of recommended hardware and links. Other off-the-shelf components can replace the ones below. - -- [TurtleBot3](https://www.robotis.us/turtlebot-3/) -- [USB Camera](https://www.adesso.com/product/cybertrack-h4-1080p-hd-usb-webcam-with-built-in-microphone/) (Any USB Cam will work, this is the one we use) -- 128 GB High Speed MicroSD card -- Monitor, mouse, and keyboard -- If using an older version of the TurtleBot3 with a Jetson Nano or Raspberry Pi 3B+ you will need ot purchase a [Raspberry Pi 4 Model B](https://www.canakit.com/raspberry-pi-4-8gb.html) (preferably with 8 GB of RAM)) - - -### Hardware Assembly -Follow the [Robotis e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/hardware_setup/#hardware-assembly) for hardware assembly stopping after installing the Raspberry Pi. - - -### Raspberry Pi -A Raspberry Pi 4 B with 8 GB of RAM is used throughout this curriculum. Ensure heat sinks are propertly installed on the Pi such as these from [CanaKit](https://www.canakit.com/raspberry-pi-4-heat-sinks.html). - -Also, a small fan can be installed to help with cooling. We used this 3D printed bracket to mount the fan. - -```{image} ./Figures/fan.jpg -:width: 520 -:align: center -``` - - -### Camera -After installing the Raspberry pi level of the TurtleBot3 you need to install the USB Camera Mount prior to finishing the robot build. The mount used in this course can be found in the [curriculum material](../stl/burger_usbcam_mount.stl) and is installed on two of the front standoffs on the TurtleBot3. - -```{image} ./Figures/camera_mount.jpg -:width: 380 -:align: center -``` - -## Software - -### Download Ubuntu and flash MicroSD card -There are multiple ways to download and install Ubuntu 22.04 to a MicroSD card, but the Raspberry Pi Imager is one of the easiest. Instructions for installing the imager on your operating system can be found on the [Raspberry Pi OS software page](https://www.raspberrypi.com/software/). - -Once installed, start the imager and select the "CHOOSE OS" button. - -```{image} ./Figures/installer1.png -:width: 540 -:align: center -``` -
    -Scroll down the menu and select "Other general purpose OS". - -
    - -```{image} ./Figures/installer2.png -:width: 540 -:align: center -``` - -
    - -Next, select "Ubuntu". - -```{image} ./Figures/installer3.png -:width: 540 -:align: center -``` -
    -Lastly, scroll and select the latest 64-bit version of "Ubuntu Server 22.04 LTS". - -
    - -```{image} ./Figures/installer4.png -:width: 540 -:align: center -``` -
    - -Now that you have the correct image selected, you need to choose the correct storage device that corresponds to the MicroSD card. Select "CHOOSE STORAGE". - -```{warning} -This process will overwrite the drive, so ensure you select the correct device! You can select "CHOOSE STORAGE" before inserting the MicroSD card, then insert it, and the card will be the new drive that pops up. -``` - -Once you are sure the correct drive is selected, click "WRITE". - -Once complete you should have an Ubuntu SD card! Ensure your Raspberry Pi is powered off, connected to a monitor, keyboard, and mouse, and insert the SD card. - - -### Ubuntu Setup - -#### Login and changing password -Once Ubuntu boots up you will be prompted to enter the login and password. It may take a few minutes on first boot to configure the default username and password, so if login fails, try again after a few minutes. The default username is **ubuntu** and password is **ubuntu**. - -On first login, you will be prompted to change the password. Enter the current password, **ubuntu**, and then enter a new password twice. - -#### Changing username (optional) -I like to change the username to "pi" so I remember that this machine is a Raspberry Pi. This is optional and you can change the username to anything you like. - -First, add a *temp* user: -```bash -sudo adduser temp -``` -Enter an easy to remember password, and then hit enter until you are back at the terminal prompt. - -Add the *temp* user to the *sudo* group: -```bash -sudo adduser temp sudo -``` - -Log out of *ubuntu* user: -```bash -exit -``` - -Login to *temp* user account. - -Change the *ubuntu* username to the new username: - -```console -sudo usermod -l newUsername ubuntu -sudo usermod -d /home/newHomeDir -m newUsername -``` - -For example: -```shell -sudo usermod -l pi ubuntu -sudo usermod -d /home/pi -m pi -``` - -Log out of *temp* user and log in with new username and password (the password is still the same as the password you set for the *ubuntu* user). - -Delete the *temp* user: -```shell -sudo deluser temp -sudo rm -r /home/temp -``` - -Now at the terminal prompt you should see `pi@ubuntu:` and if you type `pwd` you should see `/home/pi` (with `pi` replaced with the username you chose). - - -#### Change hostname -If you have multiple robots on your network it is good to give each a unique hostname. We number each robot from 0-n and each robot has a corresponding hostname (e.g., robot0). - -Change the hostname with the command line editor of your choice. -```bash -sudo hostnamectl set-hostname robot0 -``` - -Replace `ubuntu` with the hostname of choice, such as robot0. Save and exit. - -The new hostname will not take effect until reboot. Don't reboot yet, though! We have a couple more things to accomplish before reboot. - -#### Set up Wi-Fi -Until a desktop GUI is installed we have to work with the command line to set up the Wi-Fi. This is the most reliable method I have found and we will delete these changes once a GUI is installed. - -First, determine the name of your Wi-Fi network adapter by typing `ip link` (for the Raspberry Pi version of Ubuntu Server 20.04 LTS it is typically **`wlan0`**). - -```bash -pi@ubuntu:~$ ip link -1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 -2: eth0: mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000 - link/ether e4:5f:01:15:5b:30 brd ff:ff:ff:ff:ff:ff -3: wlan0: mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000 - link/ether e4:5f:01:15:5b:31 brd ff:ff:ff:ff:ff:ff -``` - -Open the `/etc/netplan/50-cloud-init.yaml` file in your favorite browser: - -```bash -sudo nano /etc/netplan/50-cloud-init.yaml -``` - -Edit the file so it looks like the below (use spaces and not tabs) replacing **wlan0** with your wireless network interface and using your SSID and password: - -Save and exit. - -``` -# This file is generated from information provided by the datasource. Changes -# to it will not persist across an instance reboot. To disable cloud-init's -# network configuration capabilities, write a file -# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: -# network: {config: disabled} -network: - ethernets: - eth0: - dhcp4: true - optional: true - version: 2 - wifis: - wlan0: - optional: true - access-points: - "YOUR-SSID": - password: "YOUR-PASSWORD" - dhcp4: true -``` - - -Apply your changes using the following command: -```bash -sudo netplan apply -``` - -Alternatively, you can reboot your system and the changes will be automatically applied once the system boots. - - - -**Optional:** It may be beneficial to setup a static IP address. To do this you need to determine your subnet and gateway. - -Determine subnet and gateway addresses: -``` -ubuntu@ubuntu:~$ ip route -default via 192.168.0.1 dev wlan0 proto static -192.168.0.0/24 dev wlan0 proto kernel scope link src 192.168.0.201 -``` - -Set static IP within subnet range: -``` -# This file is generated from information provided by the datasource. Changes -# to it will not persist across an instance reboot. To disable cloud-init's -# network configuration capabilities, write a file -# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: -# network: {config: disabled} -network: - ethernets: - eth0: - dhcp4: true - optional: true - version: 2 - wifis: - wlan0: - dhcp4: no - access-points: - "YOUR-SSID": - password: "YOUR-PASSWORD" - addresses: - - 192.168.0.201/24 - routes: - - to: default - via: 192.168.0.1 - nameservers: - addresses: [192.168.0.1, 8.8.8.8, 1.1.1.1] - optional: true -``` - -#### Disable Automatic Updates -Ubuntu will attempt to apply system updates in the background. This has caused issues in the past with ROS dependencies and keys. Disabling automatic updates allows you to control when Ubuntu installs updates. While this is not a good habit for general computer security, it is fine for this application of an embedded robotics system. Ensure you periodically update and upgrade your system. - -Open the auto updater configuration file using sudoedit: -```bash -sudoedit /etc/apt/apt.conf.d/20auto-upgrades -``` - -Change the content from: -``` -APT::Periodic::Update-Package-Lists "1"; -APT::Periodic::Unattended-Upgrade "1"; -``` - -to: -``` -APT::Periodic::Update-Package-Lists "0"; -APT::Periodic::Unattended-Upgrade "0"; -APT::Periodic::AutocleanInterval "0"; -APT::Periodic::Download-Upgradeable-Packages "0"; -``` - -#### Enable SSH and generate new keys -```bash -sudo ssh-keygen -A -sudo systemctl start ssh -``` - -#### Add Swap Space (optional) -The Raspberry Pi 4 B used in our course has 8 GB of RAM. Swap Space might not be necessary, but with a larger SD card it is beneficial. - -You can check that there is no active swap using the free utility: - -```bash -pi@ubuntu:~$ free -h - total used free shared buff/cache available -Mem: 7.6Gi 201Mi 7.1Gi 3.0Mi 328Mi 7.3Gi -Swap: 0B 0B 0B -``` - -The **fallocate** program can be used to create a swap: - -```bash -sudo fallocate -l 2G /swapfile -``` - -If it was created correctly, you should see the below: - -```bash -pi@ubuntu:~$ ls -lh /swapfile --rw------- 1 root root 2.0G Aug 19 17:30 /swapfile -``` - -Make the file only accessible to root by typing: - -```bash -sudo chmod 600 /swapfile -``` - -Verify the permissions by typing the following: - -```bash -pi@ubuntu:~$ ls -lh /swapfile --rw------- 1 root root 2.0G Aug 19 17:28 /swapfile -Now only root user has read and write flags enabled. -``` - -You can set the file as swap space by typing the following: - -```bash -pi@ubuntu:~$ sudo mkswap /swapfile -Setting up swapspace version 1, size = 2 GiB (2147479552 bytes) -no label, UUID=b5bc4abf-2bce-419e-870d-4d44a7a05778 -``` - -Then turn on the swap file: - -```bash -sudo swapon /swapfile -``` - -To verify that this worked you can type the following: - -```bash -pi@ubuntu:~$ sudo swapon --show -NAME TYPE SIZE USED PRIO -/swapfile file 2G 0B -2 -``` - -This swap will only last until reboot, so to make it permanent at it to the `fstab` file: - -```shell -echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab -``` - -Now it is time to reboot by typing - -```shell -sudo reboot -``` - -#### Verify changes -After reboot and you log in your new hostname should be listed at the terminal (e.g., `pi@robot0`). Additionally, you should be connected to Wi-Fi and have an IP Address. You can confirm by typing the following and observing the IP address in the output: - -![logo](Figures/wifi3.png) - -You can now use this IP address to create a remote secure shell into the TurtleBot3 using either the IP address or hostname if your network provides Dynamic DNS. From another machine connected to your network type one of the following: - -```bash -ssh username@IP_ADDRESS -``` -or -```bash -ssh username@HOSTNAME -``` - -Lastly, ensure your swap space is still active by typing the following and observing the output: - -```bash -pi@robot99:~$ free -h - total used free shared buff/cache available -Mem: 7.6Gi 224Mi 6.5Gi 3.0Mi 903Mi 7.3Gi -Swap: 2.0Gi 0B 2.0Gi -``` - -#### Update and Upgrade -Since we turned off automatic updates, you should periodically update and upgrade. You can use this single command to accomplish both while accepting all upgrades: - -```bash -sudo apt update && sudo apt -y upgrade -``` - -#### Install Ubuntu Desktop (optional) -A desktop GUI is **not** necessary for a remote machine like TurtleBot3 and will take up about 1.4 GB of RAM to run. If GUI is neccessary, the following will install the environment while confirming the installation: - -```bash -sudo apt -y install ubuntu-desktop -``` - -If you do install the Ubuntu Desktop and want to use the GUI to setup the Wi-Fi network then you need to remove the settings included in the `/etc/netplan/50-cloud-init.yaml` file. It should look like the original file when complete: - -``` -# This file is generated from information provided by the datasource. Changes -# to it will not persist across an instance reboot. To disable cloud-init's -# network configuration capabilities, write a file -# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: -# network: {config: disabled} -network: - ethernets: - eth0: - dhcp4: true - optional: true - version: 2 - wifis: - wlan0: - dhcp4: true - optional: true -``` - - - -You can now use the GUI interface in the top right of the screen to set up a Wi-Fi connection. - -### Setup GitHub SSH Keys -The following assumes you already have a GitHub account. - -Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login: - -```bash -cd -ssh-keygen -t ed25519 -C "github@email.com" -``` - -When prompted to "Enter a file in which to save the key", hit **enter**. - -At the prompt, type a secure password. - -Start the ssh-agent in the background and add your SSH private key to the ssh-agent: - -```bash -eval "$(ssh-agent -s)" -ssh-add ~/.ssh/id_ed25519 -``` - -Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account). - -```bash -nano ~/.ssh/id_ed25519.pub -``` - -Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email). - -Open a web browser and sign in to your GitHub account. - -In the upper-right corner of any page, click your profile photo, then click **Settings**: - -
    - -```{image} ./Figures/ssh1.png -:width: 240 -:align: center -``` -
    - -In the user settings sidebar, click **SSH and GPG keys**: - -
    - -```{image} ./Figures/ssh2.png -:width: 240 -:align: center -``` -
    - -Click **New SSH key**: -
    - -```{image} ./Figures/ssh3.png -:width: 640 -:align: center -``` -
    - -In the `Title` field, add a descriptive label for the new key, such as "robot0". - -Paste your key into the `Key` field (contents of the `.pub` file). - -Click **Add SSH key**. - - -#### Update Alternatives -Python3 is installed in Ubuntu 22.04 by default. Some ROS packages utilize the "python" command instead of "python3" so we need to create a new executable, "/usr/bin/python" that will call the Python3 (basically use the command "python" to call Python3): - -```bash -sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 -``` - -## ROS2 Humble -At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the TurtleBot3. All of these instructions are adapted from the [ROS2 Documentation](https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debians.html) and [The Robotics Back-End](https://roboticsbackend.com/install-ros2-on-raspberry-pi/) -. ROS 2 Humble is the latest version of ROS 2 that supports Ubuntu 22.04. - -### Installation - -Make sure you have a locale which supports UTF-8. - -```shell -locale # check for UTF-8 - -sudo apt update && sudo apt install locales -sudo locale-gen en_US en_US.UTF-8 -sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 -export LANG=en_US.UTF-8 - -locale # verify settings -``` - -First ensure that the Ubuntu Universe repository is enabled. -```shell -sudo apt install software-properties-common -sudo add-apt-repository universe -``` - -Set up keys: - -```shell -sudo apt update && sudo apt install curl -y -sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg -``` - -Then add the repository to your sources list. - -```bash -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ro -``` - -Install ROS 2 Humble: - -```bash -sudo apt update -sudo apt upgrade -sudo apt install ros-humble-ros-base -``` - -The base version provides the Bare Bones of ROS 2 to include minimum packaging, build, and communications libraries. No GUI tools are installed. As the Raspberry Pi is embedded into TurtleBot it is ideal to keep overhead as low as possible. Many of the GUI tools will be ran on the main machine. - -Install colcon (build tool) - -ROS2 uses colcon as a build tool (and ament as the build system). When you only install the ROS2 core packages, colcon is not here, so install it manually. - -```bash -sudo apt install python3-colcon-common-extensions -``` - - -Setup ROS environment variables and setup scripts within the `~/.bashrc` file. If you don’t want to have to source the setup file every time you open a new shell, then add the command to your shell startup script: - -```bash -echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc -``` - -Any time you make changes to your `~/.bashrc` file you must source it: - -```bash -source ~/.bashrc -``` - -Sourcing ROS 2 setup files will set several environment variables necessary for operating ROS 2. If you ever have problems finding or using your ROS 2 packages, make sure that your environment is properly set up using the following command: - -```shell -printenv | grep -i ROS -``` - -Check that variables like ROS_DISTRO and ROS_VERSION are set. - -```shell -ROS_VERSION=2 -ROS_PYTHON_VERSION=3 -ROS_DISTRO=humble -``` - -## TurtleBot3 - -ToDo: need to update this section for ROS2 - -```bash -sudo apt install libudev-dev ros-noetic-turtlebot3-msgs -cd ~/robot_ws/src -git clone -b develop https://github.com/ROBOTIS-GIT/ld08_driver.git -git clone https://github.com/ROBOTIS-GIT/turtlebot3.git -git clone https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git -``` - -### [ECE387 Curriculum](https://github.com/AF-ROBOTICS/ece387_curriculum) -```bash -git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git -``` - -The **ece387_curriculum** package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool: - -```bash -cd ~/robot_ws -rosdep install --from-paths src --ignore-src -r -y -``` - -This will take a while. - -Now we can make and source our workspace: - -```bash -cd ~/robot_ws -catkin_make -source ~/.bashrc -``` - -The last set of dependencies we need to install are Python dependencies. These are listed within our **ece387_curriculum** package and can be installed using the `pip3` tool: - -```bash -roscd ece387_curriculum -pip3 install -r requirements.txt -``` - -> 📝️ **Note:** the "dlib" package will take quite a while to install. - - - -### [Updating OpenCR firmware](https://emanual.robotis.com/docs/en/platform/turtlebot3/opencr_setup/#opencr-setup) -The last step is updating the firmware for the OpenCR controller board. - -Install required packages on the Raspberry Pi -```bash -sudo dpkg --add-architecture armhf -sudo apt-get update -sudo apt-get install libc6:armhf -``` - -Setup the OpenCR model name: -```bash -export OPENCR_PORT=/dev/ttyACM0 -export OPENCR_MODEL=burger_noetic -rm -rf ./opencr_update.tar.bz2 -``` - -Download the firmware and loader, then extract the file: -```bash -wget https://github.com/ROBOTIS-GIT/OpenCR-Binaries/raw/master/turtlebot3/ROS1/latest/opencr_update.tar.bz2 -tar -xvf opencr_update.tar.bz2 -``` - -Upload firmware to the OpenCR: -```bash -cd ./opencr_update -./update.sh $OPENCR_PORT $OPENCR_MODEL.opencr -``` - -A successful firmware upload for TurtleBot3 Burger will look like: -![lobo](Figures/firmware.png) - -If not successful, attempt the debug methods in the [OpenCR Setup](https://emanual.robotis.com/docs/en/platform/turtlebot3/opencr_setup/#opencr-setup) guide. - - - -Created by Steve Beyer, 2022 -Updated by Stan Baek, 2023 \ No newline at end of file diff --git a/_sources/Module00.2_MasterSetup/MasterSetup.md b/_sources/Module00.2_MasterSetup/MasterSetup.md deleted file mode 100755 index 5d8329f..0000000 --- a/_sources/Module00.2_MasterSetup/MasterSetup.md +++ /dev/null @@ -1,215 +0,0 @@ - -# Master Setup -This guide will walk through the steps to install Ubuntu Desktop 20.04 LTS, ROS Noetic, and all dependencies on a desktop computer. This computer system is utilized in the United States Air Force Academy's Electrical and Computer Engineering department in an embedded network with the ground robot, a TurtleBot3 Burger. The master system is used to host *roscore*, utilize ROS GUI tools, and create secure connections with the TurtleBot3. This guide is adapted from the [TurtleBot3 e-Manual](https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/#overview). - ---- - - -## Hardware -For our application, we are using [Intel NUC Kits](https://www.intel.com/content/www/us/en/products/details/nuc/kits.html) but these instructions will work on any AMD64 architecture. - - -## Software -### Download Ubuntu and flash USB -For the desktop machine you will first need to download [Ubuntu Desktop 20.04 LTS](https://releases.ubuntu.com/focal/). - -Once downloaded, follow the instructions to create a [bootable Ubuntu USB stick](https://ubuntu.com/tutorials/create-a-usb-stick-on-ubuntu#1-overview) within Ubuntu. The guide provides links to create USB sticks from Windows and macOS as well. - -Once the bootable USB stick is created, follow the guide to [Install Ubuntu desktop](https://ubuntu.com/tutorials/install-ubuntu-desktop#1-overview) selecting a useful computer name such as `master0`. The NUC requires you to press and hold F10 on startup to boot from a USB stick. - - -#### Setup GitHub SSH Keys -The following assumes you already have a GitHub account. - -Create SSH keys to use with your GitHub account by typing the following using the same email as you GitHub login: - -```bash -cd -ssh-keygen -t ed25519 -C "github@email.com" -``` - -When prompted to "Enter a file in which to save the key", hit **enter**. - -At the prompt, type a secure password. - -Start the ssh-agent in the background and add your SSH private key to the ssh-agent: - -```bash -eval "$(ssh-agent -s)" -ssh-add ~/.ssh/id_ed25519 -``` - -Open the public key with your favorite command line editor (this is easier to accomplish via an SSH connection from a desktop machine with a GUI so you can copy the public key to your GitHub account). - -```bash -nano ~/.ssh/id_ed25519.pub -``` - -Copy the contents of the file (maximize the window and ensure you copy the entire contents up to the GitHub email). - -Open a web browser and sign in to your GitHub account. - -In the upper-right corner of any page, click your profile photo, then click **Settings**: - -![logo](Figures/ssh1.png) - -In the user settings sidebar, click **SSH and GPG keys**: - -![logo](Figures/ssh2.png) - -Click **New SSH key**: - -![logo](Figures/ssh3.png) - -In the "Title" field, add a descriptive label for the new key, such as "master0". - -Paste your key into the "Key" field (contents of the `.pub` file). - -Click **Add SSH key**. - - - -#### Update Alternatives -Python3 is installed in Ubuntu20 by default. Some ROS packages utilize the "python" command instead of "python3" so we need to create a new executable, "/usr/bin/python" that will call Python3 (basically use the command "python" to call Python3): - -```bash -sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 -``` - -#### Additional Software - -``` -sudo apt install jupyter-notebook -jupyter contrib nbextension install --user -``` - - - -### ROS Noetic -At this point, the Ubuntu environment is setup. Now we will setup the ROS requirements for the master. All of these instructions are adapted from the [ROS wiki](http://wiki.ros.org/noetic/Installation/Ubuntu). ROS Noetic is the latest version of ROS 1 that supports Ubuntu Focal. - -#### Installation -Accept software from packages.ros.org: - -```bash -sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' -``` - -Set up keys: - -```bash -sudo apt install curl # if you haven't already installed curl -curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - -``` - -Install ROS Noetic: - -```bash -sudo apt update -sudo apt -y install ros-noetic-desktop-full -``` - -The full version provides theminimum packaging, build, communications libraries, GUI tools, and 2D/3D simulation and perception packages. - -Install ROS dependencies for building packages: - -```bash -sudo apt -y install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool python3-pip xterm build-essential -``` - -Initialize rosdep - -```bash -sudo rosdep init -rosdep update -``` - -Source the ROS setup file: -```bash -source /opt/ros/noetic/setup.bash -``` - -Create your ROS workspace: - -```bash -mkdir -p ~/master_ws/src -cd ~/master_ws/ -catkin_make -``` - -Setup ROS environment variables and setup scripts within the `~/.bashrc` file. Open the `~/.bashrc` file with your favorite command line editor and add the following to the bottom: - -```bash -source /opt/ros/noetic/setup.bash -source ~/master_ws/devel/setup.bash -export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share -export ROS_HOSTNAME=`hostname` # note these are backticks, not apostrophes -export ROS_MASTER_URI=http://MASTER_IP:11311 # replace "MASTER_IP" with IP address/hostname of your master -export EDITOR='nano -w' # replace with editor of choice used with rosed command -export TURTLEBOT3_MODEL=burger -export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR -``` - -Any time you make changes to your `~/.bashrc` file you must source it: - -```bash -source ~/.bashrc -``` - -#### Dependencies -There are a number of ROS packages required to operate the TurtleBot3. - -##### ROS Dependencies -```bash -sudo apt-get install ros-noetic-joy ros-noetic-teleop-twist-joy \ - ros-noetic-teleop-twist-keyboard ros-noetic-laser-proc \ - ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \ - ros-noetic-rosserial-python ros-noetic-rosserial-client \ - ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \ - ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \ - ros-noetic-compressed-image-transport ros-noetic-rqt* ros-noetic-rviz \ - ros-noetic-gmapping ros-noetic-navigation ros-noetic-interactive-markers - -``` - -##### TurtleBot3 Dependencies -```bash -sudo apt install ros-noetic-dynamixel-sdk ros-noetic-turtlebot3-msgs ros-noetic-turtlebot3 -``` - -##### [Simulation](https://emanual.robotis.com/docs/en/platform/turtlebot3/simulation/#gazebo-simulation): -```bash -cd ~/master_ws/src -git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git -``` - -##### [ECE387 Curriculum](https://github.com/AF-ROBOTICS/ece387_curriculum) -```bash -git clone git@github.com:AF-ROBOTICS/ece387_curriculum.git -``` - -The **ece387_curriculum** package includes all dependencies needed to run the TurtleBot3 nodes. We can automatically install these dependencies using the ROSDEP tool: - -```bash -cd ~/master_ws -rosdep install --from-paths src --ignore-src -r -y -``` - -This may take a while. - -Now we can make and source our workspace: - -```bash -catkin_make -source ~/.bashrc -``` - -The last set of dependencies we need to install are Python dependencies. These are listed within our **ece387_curriculum** package and can be installed using the `pip3` tool: - -```bash -roscd ece387_curriculum -pip3 install -r requirements.txt -``` - -> 📝️ **Note:** the "dlib" package will take quite a while to install. - diff --git a/_sources/Module10_FinalProject/FinalProject.md b/_sources/Module10_FinalProject/FinalProject.md old mode 100755 new mode 100644 index 064bb0c..43d331e --- a/_sources/Module10_FinalProject/FinalProject.md +++ b/_sources/Module10_FinalProject/FinalProject.md @@ -1,56 +1,56 @@ -# Module 10: Final Project - -## Purpose -Students will utilize all previous modules to drive their robot in the DFEC halls. The LIDAR will be used to drive the robot centered between two walls, the IMU will be used to make 90/180/360 deg turns, OpenCV will be used to identify randomly placed stopped signs and act accordingly, and apriltag_ros will be used to navigate the robot throughout the halls (when the robot is a certain distance from the apriltag then turn left, right, around, or the robot has reached the goal depending on the tag ID). - -## Design -The first step to a complex task such as this is to plan. You will not be successful if you just start trying to code a solution. Therefore, **you should not write ANY code until you have completed the design phase of the project**. There are a number of tools to help with design, but we will focus on two: graphs and finite state machines. - -**Graphs:** -Build a notional ROS graph for your application using PowerPoint or another digital tool. You have a lot of practice up to this point using the *rqt_graph* tool, but this time you will design the graph before the project. Ensure your graph includes all nodes and topics you foresee using and which machine these will operate on. - -**Finite State Machine:** -A finite state machine is a great way to plan out the logic flow of a complex problem. It allows you to visualize how your robot will transition states (current condition of the robot: e.g., driving forward) based on the inputs received from all of the sensors. Your "base state" will be driving forward. The rest is up to your design. For simplicity, we will use a Moore Machine, so the output only relies on the current state. - -A quick intro on finite state machines from ECE382 is here: [ECE382: Module 7 Part 1](https://youtu.be/A8m0qo2MKlE?list=PLLvuo5HBf25HKbK18J28lJNyPkltAxfln&t=26) - -> 📝️ **Note:** Make sure you include both the graph and FSM in your final report in the design section under System level design (3.3) - -## Design Presentation -Your team will present both the ROS graph and Finite State Machine in class in a presentation taking no more than 10 minutes. - -## Implementation -The robot will start at an undisclosed location within the large halls of DFEC. When traveling between two walls (the halls are approximately 4 meters wide), the robot should stay in the middle of the hall. There will be an AprilTag at the end of each hall providing turn commands: - -- ID 0: turn left -- ID 1: turn right -- ID 2: turn around - -You should stop halfway in the intersection (approximately 2 meters from the tag) and turn accordingly. Stop signs will be randomly placed throughout the halls. The robot should stop approximately 1 meter from the stop sign, wait five seconds, and then continue driving forward. - -If at any time there is an open door in the hall, the robot should ignore the door and continue driving in the middle of the hall. - -When the goal is discovered (AprilTag ID 3), the robot should stop within 0.5 meters from the goal and complete a 360 deg turn. - -## Demonstration -Demonstrations will be accomplished on lesson 40 using a randomized course. Points will be deducted for failed checkpoints (e.g., does not stop and turn within approximately 2 meters of AprilTag 0). The final rubric is below and each item is worth **6 points** for a total of **60 points** assigned to the demonstration: - -- Wall following -- Ignore doors -- AprilTag0: Turn left -- AprilTag1: Turn right -- AprilTag2: Turn around -- AprilTag3: Turn 360 -- Stop halfway in intersectiom -- Stop 1.0 meters from stop sign -- Wait 5 seconds after stop sign detection -- Stop within 0.5 meters from AprilTag3 - -## Turn-in Requirements -**[10 points]** Design Presentation - -**[60 points]** Demonstration - -**[30 points]** Report and code - -> ⚠️ **Warning:** The final project is 25% of your final grade! +# Module 10: Final Project + +## Purpose +Students will utilize all previous modules to drive their robot in the DFEC halls. The LIDAR will be used to drive the robot centered between two walls, the IMU will be used to make 90/180/360 deg turns, OpenCV will be used to identify randomly placed stopped signs and act accordingly, and apriltag_ros will be used to navigate the robot throughout the halls (when the robot is a certain distance from the apriltag then turn left, right, around, or the robot has reached the goal depending on the tag ID). + +## Design +The first step to a complex task such as this is to plan. You will not be successful if you just start trying to code a solution. Therefore, **you should not write ANY code until you have completed the design phase of the project**. There are a number of tools to help with design, but we will focus on two: graphs and finite state machines. + +**Graphs:** +Build a notional ROS graph for your application using PowerPoint or another digital tool. You have a lot of practice up to this point using the *rqt_graph* tool, but this time you will design the graph before the project. Ensure your graph includes all nodes and topics you foresee using and which machine these will operate on. + +**Finite State Machine:** +A finite state machine is a great way to plan out the logic flow of a complex problem. It allows you to visualize how your robot will transition states (current condition of the robot: e.g., driving forward) based on the inputs received from all of the sensors. Your "base state" will be driving forward. The rest is up to your design. For simplicity, we will use a Moore Machine, so the output only relies on the current state. + +A quick intro on finite state machines from ECE382 is here: [ECE382: Module 7 Part 1](https://youtu.be/A8m0qo2MKlE?list=PLLvuo5HBf25HKbK18J28lJNyPkltAxfln&t=26) + +> 📝️ **Note:** Make sure you include both the graph and FSM in your final report in the design section under System level design (3.3) + +## Design Presentation +Your team will present both the ROS graph and Finite State Machine in class in a presentation taking no more than 10 minutes. + +## Implementation +The robot will start at an undisclosed location within the large halls of DFEC. When traveling between two walls (the halls are approximately 4 meters wide), the robot should stay in the middle of the hall. There will be an AprilTag at the end of each hall providing turn commands: + +- ID 0: turn left +- ID 1: turn right +- ID 2: turn around + +You should stop halfway in the intersection (approximately 2 meters from the tag) and turn accordingly. Stop signs will be randomly placed throughout the halls. The robot should stop approximately 1 meter from the stop sign, wait five seconds, and then continue driving forward. + +If at any time there is an open door in the hall, the robot should ignore the door and continue driving in the middle of the hall. + +When the goal is discovered (AprilTag ID 3), the robot should stop within 0.5 meters from the goal and complete a 360 deg turn. + +## Demonstration +Demonstrations will be accomplished on lesson 40 using a randomized course. Points will be deducted for failed checkpoints (e.g., does not stop and turn within approximately 2 meters of AprilTag 0). The final rubric is below and each item is worth **6 points** for a total of **60 points** assigned to the demonstration: + +- Wall following +- Ignore doors +- AprilTag0: Turn left +- AprilTag1: Turn right +- AprilTag2: Turn around +- AprilTag3: Turn 360 +- Stop halfway in intersectiom +- Stop 1.0 meters from stop sign +- Wait 5 seconds after stop sign detection +- Stop within 0.5 meters from AprilTag3 + +## Turn-in Requirements +**[10 points]** Design Presentation + +**[60 points]** Demonstration + +**[30 points]** Report and code + +> ⚠️ **Warning:** The final project is 25% of your final grade! diff --git a/_sources/Module1_ROS/ICE1_ListenerTalker.md b/_sources/Module1_ROS/ICE1_ListenerTalker.md old mode 100755 new mode 100644 index 782f90d..d816a56 --- a/_sources/Module1_ROS/ICE1_ListenerTalker.md +++ b/_sources/Module1_ROS/ICE1_ListenerTalker.md @@ -1,242 +1,242 @@ -# ICE1: Talker and Listener - -## Implementing the chat subscriber - -### Import modules - - -```python -# import required modules -import rospy -from std_msgs.msg import String -``` - -## Listener -This function will create the subscriber ("listener") used to receive chat messages from the publisher ("talker"). - - -```python -def listener(): - rospy.Subscriber('chat', String, callback_chat) -``` - -The above function creates the subscriber to the **/chat** topic. Every time a *String* message is sent over the topic the `callback_chat()` function is called. This is an interrupt that spins a new thread to call that function. - -### Callback function -The callback function will log and display what the chat listener sent. - - -```python -def callback_chat(message): - rospy.loginfo(rospy.get_caller_id() + " I heard %s", message.data) -``` - -The callback function receives the *String* message as an input (you can name this parameter anything, but it is helpful if it is a meaningful variable name). To access the actual message, we need to utilize the data attribute of the *String* message. If you browse to the documentation for the [String Mesage](http://docs.ros.org/en/noetic/api/std_msgs/html/msg/String.html), you will note that the message attribute is called *data* and it is of type *string*. This is why we use the command `message.data`. - -### Main - - -```python -def main(): - rospy.init_node('listener') - try: - listener() - rospy.spin() - except rospy.ROSInterruptException: - pass -``` - -The above is similar to the talker, but adds the `rospy.spin()` function call to create an infinite loop to allow the subscriber to operate in the background. - -In an actual Python script we will replace -```python -def main() -``` -with -```python -if __name__ == "__main__": -``` -This allows our python files to be imported into other python files that might also have a main() function. - -### Run the listener - - -```python -main() -``` - - [INFO] [1666756858.595079, 308.564000]: /listener I heard hello - - -At this point, the subscriber is waiting for the publisher to send a message. Browse back to your talker and type a message! You should see that message show up above after hitting `enter` in the talker Notebook. - -## Talker - -### A note on this document -This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. - -### Purpose -This Jupyter Notebook will delve a little deeper into ROS implementation. You will create a basic one way chat server that allows a user to send messages to another (both users will be on the same computer at this point in time). - -### Initialize ROS: -The first step when utilizing ROS is to initialize *roscore*. There are two methods to accomplish this: first, by explicitly running *roscore* and second, by running a launch file (which will initialize *roscore* if it is not already running). During the first portion of this course we will explicitly run *roscore* and then take advantage of launch files later in the course. - -Copy the following code and run it in a new terminal (use the shortcut `ctrl+alt+t` to open a new terminal window or select an open terminal and hit `ctrl+shift+t` to open a new terminal tab): - -`roscore` - -### Implementing the chat publisher -> 📝️ **Note:** The following is Python code that will be implemented within the Jupyter Notebook. Again, the Notebook is just a way to allow for code and text to coexist to help guide you through the ICE. You could take all of the code in this Notebook and put it within a Python file and it would work the same as it does here. The focus of this ICE is the ROS implementation. It is assumed you have a working knowledge of Python at this time so this Notebook will not go into a lot of background regarding the Python code. Module 3 will provide a Python refresher. - -### Import modules - -> ⚠️ **Important:** Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a `*` character. Do not move to the next step until the `*` is gone. - - -```python -# import required modules -import rospy -from std_msgs.msg import String -``` - -Here, we have two modules, rospy and a message. The rospy module provides all ROS implementation to create nodes and publish messages on topics. The next line imports the String message from the std_msgs package which we will use to send our chat messages. The Standard ROS Messages include a number of common message types. You can find more information about these messages on the [ROS wiki](http://wiki.ros.org/std_msgs). - -### Talker Function -One method to communicate in ROS is using a publisher/subscriber model. One node publishes messages over a topic and any other nodes can subscribe to this topic to receive the messages. - -This function will create the publisher ("talker") used to send chat messages to the subscriber ("listener"). It will read user input and publish the message. - - -```python -def talker(): - chat_pub = rospy.Publisher('chat', String, queue_size = 1) - rate = rospy.Rate(10) # 10 Hz - while not rospy.is_shutdown(): - chat_str = input("Message: ") - rospy.loginfo("I sent %s", chat_str) - chat_pub.publish(chat_str) - rate.sleep() -``` - -Line 2 creates the publisher. The publisher will publish *String* messages over the **/chat** topic. The `queue_size` parameter determines how many messages the ROS network will hold before dropping old messages. If the publisher publishes faster than the network can handle, then messages will start getting dropped. - -Line 3 determines the rate at which the following loop should run. With an input of 10, the loop should run 10 times per second (10 Hz). Line 4 creates a loop that runs until `ctrl+c` is pressed in the terminal. Line 5 gets user input while line 6 logs the message into a log file (and prints it to the screen). Line 7 publishes the chat message using the previously created publisher. Lastly, line 8 sleeps so the loop runs at the desired rate. - -> 📝️ **Note:** Waiting for user input will cause the loop to not run at 10 Hz as line 5 will block until the user hits enter. - -### Main -The main function calls our talker function. - - -```python -def main(): - rospy.init_node('talker') - try: - talker() - except rospy.ROSInterruptException: - pass -``` - -Line 2 of the above code initializes our talker node. The rest creates a try-except statement that calls our talker function. All the try/except really does is ensures we exit cleanly when ctrl+c is pressed in a terminal. - -In an actual Python script we will replace -```python -def main() -``` -with -```python -if __name__ == "__main__": -``` -This allows our python files to be imported into other python files that might also have a main() function. - -### Run the talker - - -```python -main() -``` - -### Create the listener -At this point the talker is waiting for user input. Don't start typing yet, though! We need to implement and run our listener. - -### ROS commands - -Note that the Jupyter code block for the `main()` function call on both the talker and listener has an `*` on the left side. That is due to the infinite loops in the talker and main functions. This means that those functions are blocking and no other Jupyter code blocks will run in these two notebooks. We have to open a new notebook to run the ROS commands we would use to investigate the state of our ROS system. This would be equivalent to opening a new terminal on the Linux computer. Open the [ROS](ROS.md) notebook and follow the instructions. - - - -## ROS -Below you will see the ROS commands you will use throughout this course to investigate your ROS system and write your lab reports. The "!" character in the front allows us to run bash commands from Jupyter and would **NOT** be used in the command line. - -With the talker and listener nodes running execute the below commands. - -List all running nodes: - - -``` -$ rosnode list -``` - -You should see the listener and talker nodes. The */rosout* node is created when running `roscore` and facilitates communication in the network. You can ignore this node in your lab reports. - -Get more information about the */listener* node: - - -``` -$ rosnode info /listener -``` - -You can see what topics the node is publishing and subscribing to. It publishes to the ROS log file (for debugging) and subscribes to the **/chat** topic. - -List the active topics: - - -``` -$ rostopic list -``` - -The first topic is the one we created. The last two are created by `roscore` and can be ignored. - -Show information about the **/chat** topic such as what type of messages are sent over the topic and publishing and subscribing nodes. - - - - -``` -$ rostopic info /chat -``` - -As expected, the *talker* node is publishing to the **/chat** topic while the *listener* node subscribes. - -Display running nodes and communication between them: - - -``` -$ rqt_graph -``` - -Close the rqt_graph. - -Display information about the message that is sent over the **/chat** topic. - - -``` -$ rostopic type /chat | rosmsg show -``` - -The output of the command is the same as the information we saw from the ROS documentation. Again, to access the message we have to use the `data` attribute. - -Display messages sent over the **/chat** topic: - - -``` -$ rostopic echo /chat -``` - -In the ICE1_Talker notebook send a message to the listener. You should see that message show up both here and at the listener. This echo tool is useful to ensure your nodes are sending the messages as expected. - -## Checkpoint -Once complete, get checked off by an instructor showing the output of each of the above. - -## Cleanup -In each of the notebooks reset the Jupyter kernel and clear output (at the top menu bar select "Kernel" and "Restart & Clear Output"). Close each notebook. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. +# ICE1: Talker and Listener + +## Implementing the chat subscriber + +### Import modules + + +```python +# import required modules +import rospy +from std_msgs.msg import String +``` + +## Listener +This function will create the subscriber ("listener") used to receive chat messages from the publisher ("talker"). + + +```python +def listener(): + rospy.Subscriber('chat', String, callback_chat) +``` + +The above function creates the subscriber to the **/chat** topic. Every time a *String* message is sent over the topic the `callback_chat()` function is called. This is an interrupt that spins a new thread to call that function. + +### Callback function +The callback function will log and display what the chat listener sent. + + +```python +def callback_chat(message): + rospy.loginfo(rospy.get_caller_id() + " I heard %s", message.data) +``` + +The callback function receives the *String* message as an input (you can name this parameter anything, but it is helpful if it is a meaningful variable name). To access the actual message, we need to utilize the data attribute of the *String* message. If you browse to the documentation for the [String Mesage](http://docs.ros.org/en/noetic/api/std_msgs/html/msg/String.html), you will note that the message attribute is called *data* and it is of type *string*. This is why we use the command `message.data`. + +### Main + + +```python +def main(): + rospy.init_node('listener') + try: + listener() + rospy.spin() + except rospy.ROSInterruptException: + pass +``` + +The above is similar to the talker, but adds the `rospy.spin()` function call to create an infinite loop to allow the subscriber to operate in the background. + +In an actual Python script we will replace +```python +def main() +``` +with +```python +if __name__ == "__main__": +``` +This allows our python files to be imported into other python files that might also have a main() function. + +### Run the listener + + +```python +main() +``` + + [INFO] [1666756858.595079, 308.564000]: /listener I heard hello + + +At this point, the subscriber is waiting for the publisher to send a message. Browse back to your talker and type a message! You should see that message show up above after hitting `enter` in the talker Notebook. + +## Talker + +### A note on this document +This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. + +### Purpose +This Jupyter Notebook will delve a little deeper into ROS implementation. You will create a basic one way chat server that allows a user to send messages to another (both users will be on the same computer at this point in time). + +### Initialize ROS: +The first step when utilizing ROS is to initialize *roscore*. There are two methods to accomplish this: first, by explicitly running *roscore* and second, by running a launch file (which will initialize *roscore* if it is not already running). During the first portion of this course we will explicitly run *roscore* and then take advantage of launch files later in the course. + +Copy the following code and run it in a new terminal (use the shortcut `ctrl+alt+t` to open a new terminal window or select an open terminal and hit `ctrl+shift+t` to open a new terminal tab): + +`roscore` + +### Implementing the chat publisher +> 📝️ **Note:** The following is Python code that will be implemented within the Jupyter Notebook. Again, the Notebook is just a way to allow for code and text to coexist to help guide you through the ICE. You could take all of the code in this Notebook and put it within a Python file and it would work the same as it does here. The focus of this ICE is the ROS implementation. It is assumed you have a working knowledge of Python at this time so this Notebook will not go into a lot of background regarding the Python code. Module 3 will provide a Python refresher. + +### Import modules + +> ⚠️ **Important:** Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a `*` character. Do not move to the next step until the `*` is gone. + + +```python +# import required modules +import rospy +from std_msgs.msg import String +``` + +Here, we have two modules, rospy and a message. The rospy module provides all ROS implementation to create nodes and publish messages on topics. The next line imports the String message from the std_msgs package which we will use to send our chat messages. The Standard ROS Messages include a number of common message types. You can find more information about these messages on the [ROS wiki](http://wiki.ros.org/std_msgs). + +### Talker Function +One method to communicate in ROS is using a publisher/subscriber model. One node publishes messages over a topic and any other nodes can subscribe to this topic to receive the messages. + +This function will create the publisher ("talker") used to send chat messages to the subscriber ("listener"). It will read user input and publish the message. + + +```python +def talker(): + chat_pub = rospy.Publisher('chat', String, queue_size = 1) + rate = rospy.Rate(10) # 10 Hz + while not rospy.is_shutdown(): + chat_str = input("Message: ") + rospy.loginfo("I sent %s", chat_str) + chat_pub.publish(chat_str) + rate.sleep() +``` + +Line 2 creates the publisher. The publisher will publish *String* messages over the **/chat** topic. The `queue_size` parameter determines how many messages the ROS network will hold before dropping old messages. If the publisher publishes faster than the network can handle, then messages will start getting dropped. + +Line 3 determines the rate at which the following loop should run. With an input of 10, the loop should run 10 times per second (10 Hz). Line 4 creates a loop that runs until `ctrl+c` is pressed in the terminal. Line 5 gets user input while line 6 logs the message into a log file (and prints it to the screen). Line 7 publishes the chat message using the previously created publisher. Lastly, line 8 sleeps so the loop runs at the desired rate. + +> 📝️ **Note:** Waiting for user input will cause the loop to not run at 10 Hz as line 5 will block until the user hits enter. + +### Main +The main function calls our talker function. + + +```python +def main(): + rospy.init_node('talker') + try: + talker() + except rospy.ROSInterruptException: + pass +``` + +Line 2 of the above code initializes our talker node. The rest creates a try-except statement that calls our talker function. All the try/except really does is ensures we exit cleanly when ctrl+c is pressed in a terminal. + +In an actual Python script we will replace +```python +def main() +``` +with +```python +if __name__ == "__main__": +``` +This allows our python files to be imported into other python files that might also have a main() function. + +### Run the talker + + +```python +main() +``` + +### Create the listener +At this point the talker is waiting for user input. Don't start typing yet, though! We need to implement and run our listener. + +### ROS commands + +Note that the Jupyter code block for the `main()` function call on both the talker and listener has an `*` on the left side. That is due to the infinite loops in the talker and main functions. This means that those functions are blocking and no other Jupyter code blocks will run in these two notebooks. We have to open a new notebook to run the ROS commands we would use to investigate the state of our ROS system. This would be equivalent to opening a new terminal on the Linux computer. Open the [ROS](ROS.md) notebook and follow the instructions. + + + +## ROS +Below you will see the ROS commands you will use throughout this course to investigate your ROS system and write your lab reports. The "!" character in the front allows us to run bash commands from Jupyter and would **NOT** be used in the command line. + +With the talker and listener nodes running execute the below commands. + +List all running nodes: + + +``` +$ rosnode list +``` + +You should see the listener and talker nodes. The */rosout* node is created when running `roscore` and facilitates communication in the network. You can ignore this node in your lab reports. + +Get more information about the */listener* node: + + +``` +$ rosnode info /listener +``` + +You can see what topics the node is publishing and subscribing to. It publishes to the ROS log file (for debugging) and subscribes to the **/chat** topic. + +List the active topics: + + +``` +$ rostopic list +``` + +The first topic is the one we created. The last two are created by `roscore` and can be ignored. + +Show information about the **/chat** topic such as what type of messages are sent over the topic and publishing and subscribing nodes. + + + + +``` +$ rostopic info /chat +``` + +As expected, the *talker* node is publishing to the **/chat** topic while the *listener* node subscribes. + +Display running nodes and communication between them: + + +``` +$ rqt_graph +``` + +Close the rqt_graph. + +Display information about the message that is sent over the **/chat** topic. + + +``` +$ rostopic type /chat | rosmsg show +``` + +The output of the command is the same as the information we saw from the ROS documentation. Again, to access the message we have to use the `data` attribute. + +Display messages sent over the **/chat** topic: + + +``` +$ rostopic echo /chat +``` + +In the ICE1_Talker notebook send a message to the listener. You should see that message show up both here and at the listener. This echo tool is useful to ensure your nodes are sending the messages as expected. + +## Checkpoint +Once complete, get checked off by an instructor showing the output of each of the above. + +## Cleanup +In each of the notebooks reset the Jupyter kernel and clear output (at the top menu bar select "Kernel" and "Restart & Clear Output"). Close each notebook. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. diff --git a/_sources/Module1_ROS/ROS.md b/_sources/Module1_ROS/ROS.md old mode 100755 new mode 100644 index ec8e886..429d866 --- a/_sources/Module1_ROS/ROS.md +++ b/_sources/Module1_ROS/ROS.md @@ -1,226 +1,226 @@ -# Robotics Operating System (ROS) - -## Purpose -This lecture accompanies the introduction to ROS notetaker used in class. We will apply the knowledge you learned by interacting with a simulated TurlteBot3 Burger. - -## ROS Introduction. -Robotics Operating System (https://www.ros.org/about-ros/): - -The Robot Operating System (ROS) is a flexible framework for writing robot software. It is a collection of tools, libraries, and conventions that aim to simplify the task of creating complex and robust robot behavior across a wide variety of robotic platforms. ROS is sometimes called a meta operating system because it performs many functions of an operating system, but it requires a computer's operating system such as Linux. - -Why? Because creating truly robust, general-purpose robot software is hard. From the robot's perspective, problems that seem trivial to humans often vary wildly between instances of tasks and environments. Dealing with these variations is so hard that no single individual, laboratory, or institution can hope to do it on their own. - -As a result, ROS was built from the ground up to encourage collaborative robotics software development. For example, one laboratory might have experts in mapping indoor environments, and could contribute a world-class system for producing maps. Another group might have experts at using maps to navigate, and yet another group might have discovered a computer vision approach that works well for recognizing small objects in clutter. ROS was designed specifically for groups like these to collaborate and build upon each other's work, as is described throughout this site. - -Noetic Ninjemys (http://wiki.ros.org/noetic): - - -## Setting up the terminal for ROS -1. Open a new Linux terminal by pressing: - - `ctrl+alt+t` -
    -1. Change to your root directory: - - `cd` - - When the `cd` command has no arguments, it changes to the user's home directory. The command `cd ..` will move up one level. -
    -1. Print the working directory: - - `pwd` -
    -1. List the contents of the current directory: - - `ls` -
    -1. Change directory to your master_ws folder: - - `cd master_ws` -
    -1. List the contents of the current directory: - - `ls` -
    -1. Change directory to your devel folder: - - `cd devel` -
    -1. You should notice a setup.bash file in the devel folder. Source this file allowing you to call your ROS packages: - - `source setup.bash` -
    -1. Open your .bashrc file (a script that is ran every time a new terminal instance is opened): - - `nano ~/.bashrc` - - This command is ran with sudo priveleges as the .bashrc is a system level file. The '~' character indicates the .bashrc file is in the user's home directory and allows us to access it from anywhere in the file system (would be the same as using the absolute path `nano /dfec/home/.bashrc` ). - -1. Scroll to the bottom of the file. You should see a few lines of code such as the following: - - ``` - source /opt/ros/noetic/setup.bash - source ~/master_ws/devel/setup.bash - export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share - export ROS_MASTER_URI=http://master:11311 - export EDITOR='nano -w' - ``` -
    - The first three lines set up the ROS environment allowing access to built-in packages and packages within your master workspace. Line 4 establishes which system hosts the ROS network. You can replace master with your robot, and the entire ROS network would run on your robot. - Hit ctrl+s to save, then ctrl+x to exit. -
    - You need to source (execute) the .bashrc file any time the file is changed (the .bashrc file is ran every time a new terminal is opened, but since we haven't opened a new terminal yet, we have to run it manually). -
    -
    -1. Execute the .bashrc file - - `source ~/.bashrc` - -## Running the TurtleBot3 simulation -The best way to learn about ROS is through implementation, however, let's start off by playing around with a virtual TurtleBot3! The TurtleBot3 is a tool we will utilize throughout this course to apply and integrate robotics systems. Ultimately you will create a complex embedded robotics system to perform a specific dedicated task, such as navigating the halls of DFEC. But let's see if we can get the robot to drive around first. - -1. In a terminal (**Pro tip:** ctrl+alt+t opens a new terminal window, while ctrl+shift+t opens a new terminal tab in an existing window) and initialize the ROS network: - - `roscore` - -1. That terminal is now occupied. Open a new terminal tab/window and launch the TurtleBot3 gazebo launch file (A launch file is a way to run one or more nodes at once, we will learn about launch files later): - - `roslaunch turtlebot3_gazebo turtlebot3_world.launch` - -> ⌨️ **Syntax:** `roslaunch ` - - -3. Open another terminal tab/window and launch the following: - - `roslaunch turtlebot3_gazebo turtlebot3_gazebo_rviz.launch` - -The launch files will run the nodes necessary to simulate our robot and opens two windows: Gazebo and rviz. Gazebo is a simulation tool that provides physics for our robot and enables the testing of algorithms prior to real-world implementation. You should see a TurtleBot3 sitting at (-2.0, -0.5) facing the positive x direction surrounded by a maze. Using the mouse, left-click and holding will pan, holding the scroll wheel will change the orientation of the camera, and scrolling will zoom in and out. - -The second window is rviz, a tool that visualizes topics interacting in our system. You should see a number of red dots outlining the location of the obstacles in our simulation. These are objects detected by our LIDAR which is communicating over a scan topic. Using the mouse, left-click will change the orientation of the camera, holding the scroll wheel will pan, and scrolling will zoom in and out (would be nice if they were the same). - -Don't worry! We will learn more about both of these tools at a later time. - -Let's go ahead and take a look at what nodes and topics currently running in our system. - -The "!" character in front of the following commands allows us to run bash commands from the Jupyter NB and would **NOT** be used in the command line. - - -```python -rosnode list -``` - -There are five nodes running, the first two enable the Gazebo simulation, the third allows for the visualization of the simulated robot, the fourth is created every time *roscore* is ran and helps with communication in our network, and the last enables rviz. - -Not too exciting yet, so lets see what topics are active. - - -```python -rostopic list -``` - -There are a lot of topics that are utilized to simulate our robot. Our real-world robot will eventually have most of these, such as **/cmd_vel**, **/imu**, and **/scan**. These are topics that allow us to communicate with some of the simulated hardware, such as the orientation sensor (/imu), LIDAR (/scan), and driving the robot (/cmd_vel). The rest of the topics enable visualization and movement within the simulation and can be ignored for now. - -Another useful tool for visualizing nodes and topics is called *rqt_graph*: - - -```python -rqt_graph -``` - -All that is going on right now is Gazebo is publishing the position and scan data which rviz uses to visualize the robot. **Close your rqt_graph** and let's add another node to make things a bit more interesting. - -Earlier we saw the topic **/cmd_vel**. This is a topic used to send *Twist* messages to a robot. A *Twist* message includes linear and angular x, y, z values to move a robot in a 3-dimensional space (google `ROS twist message` for more information). Our robot only drives in 2-dimensions and the wheels can only move forward and backward, so we will only use the linear x (forward and backward) and angular z (turning) attributes of the *Twist* message. To drive our simulated robot, we need a node that can publish *Twist* messages to the **/cmd_vel** topic. Luckily, there is a pre-built node called Teleop_Twist_Keyboard that sends *Twist* messages using a keyboard! - -Open a new terminal tab (select the terminal and press ctrl+shift+t) and run the following (**Pro tip**: in a Linux terminal if you type the first couple letters of a command and then hit tab it will autocomplete the command for you): - -`rosrun teleop_twist_keyboard teleop_twist_keyboard.py` - -> ⌨️ **Syntax:** `rosrun ` - -To drive the robot use the 'x' key to decrease linear x speed to about .25 m/s and use the 'c' key to decrease angular z speed to about .5 rad/s. Now follow the directions to utilize the keyboard to drive the robot! You should see the robot move in the simulation. - -Let's take a look at our rqt_graph to see if anything has changed. - -```python -rqt_graph -``` - -You should now see the teleop_twist_keyboard node which sends messages over **/cmd_vel** topic to Gazebo. **Close the rqt_graph window**. Let's run through a number of commands that will provide you more information about your ROS network. You will use these throughout the course to determine what is going on in your ROS network. - -## Common ROS commands - -The `rosnode` command allows us to interact with nodes. Typing any ROS command followd by `--help` will provide information about that command: - - -```python -rosnode --help -``` - -Let's get some information about our new node, teleop_twist_keyboard: - - -```python -rosnode info /teleop_twist_keyboard -``` - -The output of the command lists the topics the node is publishing and subscribing to (here is where we can see it publishes on **/cmd_vel**). - -The `rostopic` command interacts with topics. - - -```python -rostopic --help -``` - -Some of the common rostopic commands we will use in this course are `echo`, `hz`, `info`, `type`, and `list` - - -```python -rostopic list -``` - -Let's get some information about the **/cmd_vel** topic. - - -```python -rostopic info /cmd_vel -``` - -From the output we can see what nodes are publishing and subscribing to the **/cmd_vel** topic. - -Echoing the topic will allow us to see what messages are sent over the topic. After running the below command, browse back to your teleop_twist_keyboard node and drive the robot. You should see the twist messages sent to Gazebo. - - -```python -rostopic echo /cmd_vel -``` - -```{note} -When moving forward and backward ('i' and ',' keys) only a linear x value is sent, when turning left or right ('j' and 'l' keys) only an angular z value is sent, and when arcing ('u', 'o', 'm', and '.' keys) both a linear x and angular z value are sent. -``` - -```{note} -The previous command still has an `*` character to the left. This means this command is waiting for inputs and will block all future commands. To kill the command and restart the kernel in the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". This will allow future commands to run. -``` - -To learn more about the messages sent over the **/cmd_vel** topic we can use the `type` command and the `rosmsg` tool. - - -```python -rostopic type /cmd_vel -``` - - -```python -rosmsg show geometry_msgs/Twist -``` - -Or in one combined command: - - -```python -rostopic type /cmd_vel | rosmsg show -``` - -## Cleanup -Before moving on to the in-class exercise, close all running nodes. In the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". In each terminal window, close the node by typing `ctrl+c`. Ensure roscore is terminated before moving on to the ICE. +# Robotics Operating System (ROS) + +## Purpose +This lecture accompanies the introduction to ROS notetaker used in class. We will apply the knowledge you learned by interacting with a simulated TurlteBot3 Burger. + +## ROS Introduction. +Robotics Operating System (https://www.ros.org/about-ros/): + +The Robot Operating System (ROS) is a flexible framework for writing robot software. It is a collection of tools, libraries, and conventions that aim to simplify the task of creating complex and robust robot behavior across a wide variety of robotic platforms. ROS is sometimes called a meta operating system because it performs many functions of an operating system, but it requires a computer's operating system such as Linux. + +Why? Because creating truly robust, general-purpose robot software is hard. From the robot's perspective, problems that seem trivial to humans often vary wildly between instances of tasks and environments. Dealing with these variations is so hard that no single individual, laboratory, or institution can hope to do it on their own. + +As a result, ROS was built from the ground up to encourage collaborative robotics software development. For example, one laboratory might have experts in mapping indoor environments, and could contribute a world-class system for producing maps. Another group might have experts at using maps to navigate, and yet another group might have discovered a computer vision approach that works well for recognizing small objects in clutter. ROS was designed specifically for groups like these to collaborate and build upon each other's work, as is described throughout this site. + +Noetic Ninjemys (http://wiki.ros.org/noetic): + + +## Setting up the terminal for ROS +1. Open a new Linux terminal by pressing: + + `ctrl+alt+t` +
    +1. Change to your root directory: + + `cd` + + When the `cd` command has no arguments, it changes to the user's home directory. The command `cd ..` will move up one level. +
    +1. Print the working directory: + + `pwd` +
    +1. List the contents of the current directory: + + `ls` +
    +1. Change directory to your master_ws folder: + + `cd master_ws` +
    +1. List the contents of the current directory: + + `ls` +
    +1. Change directory to your devel folder: + + `cd devel` +
    +1. You should notice a setup.bash file in the devel folder. Source this file allowing you to call your ROS packages: + + `source setup.bash` +
    +1. Open your .bashrc file (a script that is ran every time a new terminal instance is opened): + + `nano ~/.bashrc` + + This command is ran with sudo priveleges as the .bashrc is a system level file. The '~' character indicates the .bashrc file is in the user's home directory and allows us to access it from anywhere in the file system (would be the same as using the absolute path `nano /dfec/home/.bashrc` ). + +1. Scroll to the bottom of the file. You should see a few lines of code such as the following: + + ``` + source /opt/ros/noetic/setup.bash + source ~/master_ws/devel/setup.bash + export ROS_PACKAGE_PATH=~/master_ws/src:/opt/ros/noetic/share + export ROS_MASTER_URI=http://master:11311 + export EDITOR='nano -w' + ``` +
    + The first three lines set up the ROS environment allowing access to built-in packages and packages within your master workspace. Line 4 establishes which system hosts the ROS network. You can replace master with your robot, and the entire ROS network would run on your robot. + Hit ctrl+s to save, then ctrl+x to exit. +
    + You need to source (execute) the .bashrc file any time the file is changed (the .bashrc file is ran every time a new terminal is opened, but since we haven't opened a new terminal yet, we have to run it manually). +
    +
    +1. Execute the .bashrc file + + `source ~/.bashrc` + +## Running the TurtleBot3 simulation +The best way to learn about ROS is through implementation, however, let's start off by playing around with a virtual TurtleBot3! The TurtleBot3 is a tool we will utilize throughout this course to apply and integrate robotics systems. Ultimately you will create a complex embedded robotics system to perform a specific dedicated task, such as navigating the halls of DFEC. But let's see if we can get the robot to drive around first. + +1. In a terminal (**Pro tip:** ctrl+alt+t opens a new terminal window, while ctrl+shift+t opens a new terminal tab in an existing window) and initialize the ROS network: + + `roscore` + +1. That terminal is now occupied. Open a new terminal tab/window and launch the TurtleBot3 gazebo launch file (A launch file is a way to run one or more nodes at once, we will learn about launch files later): + + `roslaunch turtlebot3_gazebo turtlebot3_world.launch` + +> ⌨️ **Syntax:** `roslaunch ` + + +3. Open another terminal tab/window and launch the following: + + `roslaunch turtlebot3_gazebo turtlebot3_gazebo_rviz.launch` + +The launch files will run the nodes necessary to simulate our robot and opens two windows: Gazebo and rviz. Gazebo is a simulation tool that provides physics for our robot and enables the testing of algorithms prior to real-world implementation. You should see a TurtleBot3 sitting at (-2.0, -0.5) facing the positive x direction surrounded by a maze. Using the mouse, left-click and holding will pan, holding the scroll wheel will change the orientation of the camera, and scrolling will zoom in and out. + +The second window is rviz, a tool that visualizes topics interacting in our system. You should see a number of red dots outlining the location of the obstacles in our simulation. These are objects detected by our LIDAR which is communicating over a scan topic. Using the mouse, left-click will change the orientation of the camera, holding the scroll wheel will pan, and scrolling will zoom in and out (would be nice if they were the same). + +Don't worry! We will learn more about both of these tools at a later time. + +Let's go ahead and take a look at what nodes and topics currently running in our system. + +The "!" character in front of the following commands allows us to run bash commands from the Jupyter NB and would **NOT** be used in the command line. + + +```python +rosnode list +``` + +There are five nodes running, the first two enable the Gazebo simulation, the third allows for the visualization of the simulated robot, the fourth is created every time *roscore* is ran and helps with communication in our network, and the last enables rviz. + +Not too exciting yet, so lets see what topics are active. + + +```python +rostopic list +``` + +There are a lot of topics that are utilized to simulate our robot. Our real-world robot will eventually have most of these, such as **/cmd_vel**, **/imu**, and **/scan**. These are topics that allow us to communicate with some of the simulated hardware, such as the orientation sensor (/imu), LIDAR (/scan), and driving the robot (/cmd_vel). The rest of the topics enable visualization and movement within the simulation and can be ignored for now. + +Another useful tool for visualizing nodes and topics is called *rqt_graph*: + + +```python +rqt_graph +``` + +All that is going on right now is Gazebo is publishing the position and scan data which rviz uses to visualize the robot. **Close your rqt_graph** and let's add another node to make things a bit more interesting. + +Earlier we saw the topic **/cmd_vel**. This is a topic used to send *Twist* messages to a robot. A *Twist* message includes linear and angular x, y, z values to move a robot in a 3-dimensional space (google `ROS twist message` for more information). Our robot only drives in 2-dimensions and the wheels can only move forward and backward, so we will only use the linear x (forward and backward) and angular z (turning) attributes of the *Twist* message. To drive our simulated robot, we need a node that can publish *Twist* messages to the **/cmd_vel** topic. Luckily, there is a pre-built node called Teleop_Twist_Keyboard that sends *Twist* messages using a keyboard! + +Open a new terminal tab (select the terminal and press ctrl+shift+t) and run the following (**Pro tip**: in a Linux terminal if you type the first couple letters of a command and then hit tab it will autocomplete the command for you): + +`rosrun teleop_twist_keyboard teleop_twist_keyboard.py` + +> ⌨️ **Syntax:** `rosrun ` + +To drive the robot use the 'x' key to decrease linear x speed to about .25 m/s and use the 'c' key to decrease angular z speed to about .5 rad/s. Now follow the directions to utilize the keyboard to drive the robot! You should see the robot move in the simulation. + +Let's take a look at our rqt_graph to see if anything has changed. + +```python +rqt_graph +``` + +You should now see the teleop_twist_keyboard node which sends messages over **/cmd_vel** topic to Gazebo. **Close the rqt_graph window**. Let's run through a number of commands that will provide you more information about your ROS network. You will use these throughout the course to determine what is going on in your ROS network. + +## Common ROS commands + +The `rosnode` command allows us to interact with nodes. Typing any ROS command followd by `--help` will provide information about that command: + + +```python +rosnode --help +``` + +Let's get some information about our new node, teleop_twist_keyboard: + + +```python +rosnode info /teleop_twist_keyboard +``` + +The output of the command lists the topics the node is publishing and subscribing to (here is where we can see it publishes on **/cmd_vel**). + +The `rostopic` command interacts with topics. + + +```python +rostopic --help +``` + +Some of the common rostopic commands we will use in this course are `echo`, `hz`, `info`, `type`, and `list` + + +```python +rostopic list +``` + +Let's get some information about the **/cmd_vel** topic. + + +```python +rostopic info /cmd_vel +``` + +From the output we can see what nodes are publishing and subscribing to the **/cmd_vel** topic. + +Echoing the topic will allow us to see what messages are sent over the topic. After running the below command, browse back to your teleop_twist_keyboard node and drive the robot. You should see the twist messages sent to Gazebo. + + +```python +rostopic echo /cmd_vel +``` + +```{note} +When moving forward and backward ('i' and ',' keys) only a linear x value is sent, when turning left or right ('j' and 'l' keys) only an angular z value is sent, and when arcing ('u', 'o', 'm', and '.' keys) both a linear x and angular z value are sent. +``` + +```{note} +The previous command still has an `*` character to the left. This means this command is waiting for inputs and will block all future commands. To kill the command and restart the kernel in the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". This will allow future commands to run. +``` + +To learn more about the messages sent over the **/cmd_vel** topic we can use the `type` command and the `rosmsg` tool. + + +```python +rostopic type /cmd_vel +``` + + +```python +rosmsg show geometry_msgs/Twist +``` + +Or in one combined command: + + +```python +rostopic type /cmd_vel | rosmsg show +``` + +## Cleanup +Before moving on to the in-class exercise, close all running nodes. In the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". In each terminal window, close the node by typing `ctrl+c`. Ensure roscore is terminated before moving on to the ICE. diff --git a/_sources/Module2_Linux/Linux.md b/_sources/Module2_Linux/Linux.md old mode 100755 new mode 100644 index 711beca..91949fb --- a/_sources/Module2_Linux/Linux.md +++ b/_sources/Module2_Linux/Linux.md @@ -1,391 +1,391 @@ -# Module 2: Linux for Robotics - -```{note} -This document is known as a Jupyter notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. -``` - -## Purpose -This Jupyter Notebook accompanies the introduction to Linux notetaker used in class. We will apply the knowledge you learned by interacting with the Ubuntu Operating System (OS) on the Master. - -## Linux Commands -During class we went over a number of basic Linux commands. Open a terminal on the Master and let's practice using those commands (use the shortcut `ctrl+alt+t` to open a new terminal window or select an open terminal and hit `ctrl+shift+t` to open a new terminal tab). - -When observing the terminal (or Shell) you will note the syntax `username@hostname:`(i.e., on the master: `dfec@masterX:`, on the robot: `pi@robotX`), the current working directory (i.e., `~`, which represents the home folder of the user), and lastly the '$' character and a blinking cursor. This line is the prompt and the blinking cursor indicates the terminal is active and ready for commands. - -Run the commands to move to the home folder and make a new directory: - -`cd` - -`mkdir my_folder` - -> 💡️ **Tip:** In Linux, if you highlight a command you can paste it into the terminal by clicking the scroll wheel. - -Change directories into your new folder: - -`cd my_folder` - -Create a new bash script we can use to drive the TurtleBot3: - -`touch move_turtlebot.sh` - -A bash script is a regular text file that allows you to run any command you would run within the terminal. We will use it to run a few ROS command line tools to move our TurtleBot. - -We can use the Nano text editor to edit a file within the terminal. There are a number of text editors within the terminal and WWIII might be fought over which is best; some other options include Vim and Emacs. Nano is one of the more simple editors for quick edits. Feel free to use whichever works best for you, but all guidance within this course will be based on Nano. - -Edit the new bash script: - -`nano move_turtlebot.sh` - -Copy the following into the script: - -```bash -#!/bin/bash - -ARG1=$1 - -if [ $ARG1 == 'forward' ]; then - rostopic pub /cmd_vel geometry_msgs/Twist "linear: - x: 0.15 - y: 0.0 - z: 0.0 -angular: - x: 0.0 - y: 0.0 - z: 0.0" - -elif [ $ARG1 == 'rotate' ]; then - rostopic pub /cmd_vel geometry_msgs/Twist "linear: - x: 0.0 - y: 0.0 - z: 0.0 -angular: - x: 0.0 - y: 0.0 - z: 0.5" - -elif [ $ARG1 == 'stop' ]; then - rostopic pub /cmd_vel geometry_msgs/Twist "linear: - x: 0.0 - y: 0.0 - z: 0.0 -angular: - x: 0.0 - y: 0.0 - z: 0.0" -else -echo "Please enter one of the following: - forward - rotate - stop" -fi -``` - -Typing `ctrl+s` saves the file and then typing `ctrl+x` exits Nano. - -Again, a bash script just runs commands exactly as you would within a terminal. The above code takes an argument and publishes a *Twist* message over the **/cmd_vel** topic to drive the robot accordingly. - -Before running this script, let's get our ROS environment setup: - -1. Open a new terminal and type `roscore`. -2. Open a new terminal tab and run our TurtleBot3 simulation: - - `roslaunch turtlebot3_gazebo turtlebot3_empty_world.launch` - -Now, in a new terminal, run the script you created: - -`./move_turtlebot.sh` - -Did you get an error? That is because the permissions have not been properly set and you do not have execute permissions. You can observe the permissions of a file by typing `ls -la`. - -For the `move_turtlebot.sh` file you should see `-rw-rw-r--`. The first position indicates file type ('d' for directory, '-' for regular file), the next three positions indicate read (r), write (w), and execute (x) permissions for the file owner (u), the next three indicate permisions for the group owner of the file (g), and the last three characters indicate permissions for all other users (o). - -You can change the permissions of a file or directory using the command `chmod`: - -> ⌨️ **Syntax:** `chmod ` - -For example, if we wanted to give the Owner execute permissions, you can enter the command: - -`chmod u+x move_turtlebot.sh` - -Typically we will give all users executable permissions (`chmod +x move_turtlebot.sh`). This isn't the most secure thing to do, but in our controlled environment, it isn't an issue. If you type `ls -la` now you should see the 'x' permission added for each permission group (`-rwxrwxr-x`). - -Try running your script again: - -`./move_turtlebot.sh rotate` - -> 📝️ **Note:** You can remove permissions by utilizing the '-' character instead of '+'. - -Move to your github repo that you previously created: - -`cd ~/master_ws/src/ece387_master_sp23-USERNAME/master/` - -Now we are going to create a new ROS package. You learned this in the previous homework assignment, but we will continue practicing: - -`catkin_create_pkg module02 std_msgs rospy roscpp` - -Before we go any further, it is always a good idea to compile your workspace with the new package. To do that, we will use a command `catkin_make` from within the top-level of the workspace: - -`cd ~/master_ws` - -`catkin_make` - -Now go back to the `my_folder` that you created at the beginning of the lesson and create a new bash script, `bash_script.sh`, to accomplish the following: - -1. Moves into the package you just created (you will need to figure out the complete path to this folder) -1. Creates a directory called `my_scripts` -1. Moves into that directory -1. Creates a file called **move_turtlebot_square.py** -1. Lists all files showing the permissions -1. Modifies the permissions of the **move_turtlebot_square.py** file so all groups have executable permissions -1. Lists all files again showing the updated permissions - -Now run the script. If successful you should see the file listed without and then with execute permissions. - -We are done with this script so let's remove it (you will need to either use the complete path to the file you want to remove, or be in the directory of the file you want to remove). The `rm` command can remove folders or files. If you want to learn more about a command there are two helpful tools: **Help**: `rm --help`; **Manual**: `man rm`. - -Type the following to remove our bash script: - -`rm bash_script.sh`. - -```{note} -To delete a whole folder add the `-r` tag to remove directories and thier contents recursively (e.g., `rm -r my_folder`, but don't remove your folder just yet). -``` - -We can copy (`cp`, just like ctrl+c in a GUI) and move (`mv`, just like ctrl+x in a GUI) files and folders as well. Let's copy the `move_turtlebot.sh` to the `my_scripts` folder you created earlier: - -`cp move_turtlebot.sh ~/master_ws/src/ece387_master_sp23-USERNAME/master/module02/my_scripts` - -> ⌨️ **Syntax:** `cp ` - -```{note} -For the above to work, you must already be in the same folder as the `move_turtlebot.sh` file. Otherwise you have to use the absolute file path, such as `~/my_folder/move_turtlebot.sh`. -``` - -You can now delete your `my_folder` folder. - -`cd ..` - -`rm -r my_folder` - -Change directories to your `my_scripts` folder. We can use a ROS tool, `roscd`, to change directories to ROS packages without using the absolute file path: - -`roscd module02/my_scripts` - -> ⌨️ **Syntax:** `roscd ` - -Edit the **move_turtlebot_square.py** file and paste the following contents: - -```python -#!/usr/bin/env python3 -import rospy, time, math -from geometry_msgs.msg import Twist - -class MoveTurtleBot(): - def __init__(self): - self.pub = rospy.Publisher('cmd_vel', Twist, queue_size=1) - self.cmd = Twist() - self.ctrl_c = False - rospy.on_shutdown(self.shutdownhook) - self.rate = rospy.Rate(10) # 10 Hz - - def publish_cmd_vel_once(self): - """ - In case publishing fails on first attempt - """ - while not self.ctrl_c: - connections = self.pub.get_num_connections() - if connections > 0: - self.pub.publish(self.cmd) - rospy.loginfo("Cmd Published") - break - else: - self.rate.sleep() - - def shutdownhook(self): - rospy.loginfo("Shutting down. Stopping TurtleBot!") - self.stop_turtlebot() - self.ctrl_c = True - - def stop_turtlebot(self): - self.cmd.linear.x = 0.0 - self.cmd.angular.z = 0.0 - self.publish_cmd_vel_once() - - def move_time(self, moving_time = 10.0, lin_spd = 0.2, ang_spd = 0.2): - self.cmd.linear.x = lin_spd - self.cmd.angular.z = ang_spd - - self.publish_cmd_vel_once() - time.sleep(moving_time) - - def move_square(self): - i = 0 - - while not self.ctrl_c: - # Move Forward - self.move_time(moving_time = 2.0, lin_spd = 0.2, ang_spd = 0) - # Turn - ang_spd = 0.5 # rad/sec - moving_time = math.radians(90)/ang_spd - self.move_time(moving_time = moving_time, lin_spd = 0.0, ang_spd = ang_spd) - - -if __name__ == '__main__': - rospy.init_node('move_turtlebot') - move_object = MoveTurtleBot() - try: - move_object.move_square() - except rospy.ROSInterruptException: - pass -``` - -There is a lot going on in this script, but hopefully after the previous lesson some of it makes sense. To summarize, the script creates a node, `move_turtlebot`, that publishes **Twist** messages to the **/cmd_vel** topic to drive the robot in a square: drive forward, turn 90 degrees, drive forward, repeat. This script will run until killed using `ctrl+c`. - -The script is already executable, so you can run it using ROS! - -`rosrun module02 move_turtlebot_square.py` - -```{note} Note:** It won't be a perfect square as the robot doesn't turn perfectly, but it will be close! -``` - -The robot is now driving in a square until the script is killed. If you select the terminal and hit `ctrl+c`, it will kill the script. - -Run the script again and this time hit `ctrl+z`. You can see that the robot is still running, but the commands are not updating. This is because `ctrl+z` suspends the current process, but does not kill it. We can observe all running processes on Linux by typing `ps -faux`. As you can see, there are a lot! The `grep` command allows us to filter these processes. Try the following: - -`ps -faux | grep move_turtlebot_square.py` - -The first entry should be our process and the leftmost number is the process ID (PID). We can kill any process using this number and the `kill` command: - -`kill PID` replacing PID with the number listed. - -If you hit enter again, you should see that the process was killed. Unfortunately, the TurtleBot will just continue to execute the last command sent, so you need to kill the simulation as well. Just select that terminal and hit `ctrl+c`. - -The `grep` tool is very powerful and can be used with any Linux command. For example, if you wanted to see all turtlebot packages available to us, we could type the following: - -`rospack list` - -There are a lot, so this isn't very helpful, but we can filter this command! - -`rospack list | grep turtlebot` - -The vertical line, '|', pipes the results of the first command into the second command, so we can filter all packages looking only for turtlebot packages. - -## Working with a remote machine -Later in this course you will drive your TurtleBot around unplugged from a monitor and keyboard, but you will still need access to the Raspberry Pi on the robot to run ROS nodes. One of the easiest ways to remotely access a Linux machine is through a secure shell. To create a secure shell, you need the uesrname and hostname of the computer you want to remote into. For the Raspberry Pis, all usernames are `pi` and the hostname is your robot number (e.g., `robot0`). - -1. Open a terminal on your master -1. Check connectivity to the robot: - - `ping robotX` -
    -1. Create a secure shell to the robot: - - `ssh pi@robotX` -> ⌨️ **Syntax:** `ssh @` - -1. Enter the robot's password - - You should see that the terminal lists the `pi` username and your robot's hostname, `robotX`. Any command you run in this shell will execute on the robot. -
    -1. Create a new package on the robot we can use to store scripts for this module. This package should be called 'module02' and should be in your student repo. From the terminal ssh'd into the robot, type: - -`cd ece387_robot_sp23-USERNAME/robot/` - -`catkin_create_pkg module02` - -1. Change directories to your new folder. - -1. Print the working directory. You should get "/home/pi/robot_ws/src/ece387_robot_sp23-USERNAME/robot/" - -1. You can type `exit` to close an SSH connection (but don't close the connection yet!). - -> 📝️ **Note:** At times, there may be network issues and name resolution will fail. What this means is if you try to ping the robot from the master or vice versa using the hostname (e.g., `ping master0`) it will not work. However, it will work if you use the IP address (e.g., `ping 192.168.2.120`). To do this, you need the IP address of the machine you want to access. This IP address will change periodically. The easiest way to determine the current IP Address is, with the machine plugged into a monitor and keyboard, type `ip addr` in a terminal. This will list all of the network interfaces on the machine (such as *eth0* for ethernet and *wlan0* for wireless). You are looking for the `inet` field under `wlan0` (may be called `wlo1`). Now you can unplug the roobt from the monitor, ping the IP address to check connectivity, and then SSH into the robot. - -The other remote tool you may want to take advantage of is SCP, which securely copies a file to a remote machine. - -> ⌨️ **Syntax:** `scp @:` - -For example, copy the `move_turtlebot_square.py` file to your robot by typing the following in a new terminal on the master: - -`roscd module02/my_scripts` - -`scp move_turtlebot_square.py pi@robot0:/home/pi/robot_ws/src/ece387_robot_sp23-USERNAME/robot/module02/src/` - -> 📝️ **Note:** The destination uses the absolute path you printed earlier. - -In your secure shell list the contents of your 'my_scripts' folder. You should see the `move_turtlebot_square.py` file. Check that it is executable. If it is not, make it executable. - -Now let's move our TurtleBot using the script on the robot: - -1. Run roscore on the master. - -1. Run the simulated robot on the master. - -1. SSH into your robot Raspberry Pi. - -1. Run the move_turtlebot_square.py using the `rosrun` command on the robot. - -You are now controlling your simulation (which is running on the master) remotely from the robot. In future lessons you will have nodes running on your robot to drive the robot and will control the robot remotely from the master. - -## ROS -Below you will run ROS commands. The "!" character in the front allows us to run bash commands from Jupter and would **NOT** be used in the command line. - -Accomplish the following on the master by adding the commands necessary below: - -List all running nodes: - - -```python - -``` - -List the active topics: - - -``` - -``` - -Display running nodes and communication between them: - - -```python - -``` - -Exit the rqt_graph. - -Show information about a the **/cmd_vel** topic such as what type of messages are sent over the topic and publishing and subscribing nodes. - - -```python - -``` - -Display information about the message that is sent over the **/cmd_vel** topic. - - -```python - -``` - -Display messages sent over the **/cmd_vel** topic: - - -```python - -``` - -## Checkpoint -Once complete, get checked off by an instructor showing the output of each of the above. - -```{note} You will use all of the above ROS commands for each lab to write your lab reports. You could create a bash script to run these commands automatically ;) -``` - -Additionally, place screen captures showing each of the commands running or the windows they bring up into a folder within ../module02/Pictures on your master repo. Then push the repo to get credit for this work. - -## Summary -You have seen a lot of different Linux/ROS commands during this lesson but it only scratches the surface. There are tons of online resources available if you want to learn more. I recommend working through a few of these tutorials: http://www.ee.surrey.ac.uk/Teaching/Unix/. They are fairly quick and willl give you more insight into the tools we discussed. - -## Cleanup -In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. In each of the notebooks reset the Jupter kernel and clear output. Now it is safe to exit out of this window. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. +# Module 2: Linux for Robotics + +```{note} +This document is known as a Jupyter notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. +``` + +## Purpose +This Jupyter Notebook accompanies the introduction to Linux notetaker used in class. We will apply the knowledge you learned by interacting with the Ubuntu Operating System (OS) on the Master. + +## Linux Commands +During class we went over a number of basic Linux commands. Open a terminal on the Master and let's practice using those commands (use the shortcut `ctrl+alt+t` to open a new terminal window or select an open terminal and hit `ctrl+shift+t` to open a new terminal tab). + +When observing the terminal (or Shell) you will note the syntax `username@hostname:`(i.e., on the master: `dfec@masterX:`, on the robot: `pi@robotX`), the current working directory (i.e., `~`, which represents the home folder of the user), and lastly the '$' character and a blinking cursor. This line is the prompt and the blinking cursor indicates the terminal is active and ready for commands. + +Run the commands to move to the home folder and make a new directory: + +`cd` + +`mkdir my_folder` + +> 💡️ **Tip:** In Linux, if you highlight a command you can paste it into the terminal by clicking the scroll wheel. + +Change directories into your new folder: + +`cd my_folder` + +Create a new bash script we can use to drive the TurtleBot3: + +`touch move_turtlebot.sh` + +A bash script is a regular text file that allows you to run any command you would run within the terminal. We will use it to run a few ROS command line tools to move our TurtleBot. + +We can use the Nano text editor to edit a file within the terminal. There are a number of text editors within the terminal and WWIII might be fought over which is best; some other options include Vim and Emacs. Nano is one of the more simple editors for quick edits. Feel free to use whichever works best for you, but all guidance within this course will be based on Nano. + +Edit the new bash script: + +`nano move_turtlebot.sh` + +Copy the following into the script: + +```bash +#!/bin/bash + +ARG1=$1 + +if [ $ARG1 == 'forward' ]; then + rostopic pub /cmd_vel geometry_msgs/Twist "linear: + x: 0.15 + y: 0.0 + z: 0.0 +angular: + x: 0.0 + y: 0.0 + z: 0.0" + +elif [ $ARG1 == 'rotate' ]; then + rostopic pub /cmd_vel geometry_msgs/Twist "linear: + x: 0.0 + y: 0.0 + z: 0.0 +angular: + x: 0.0 + y: 0.0 + z: 0.5" + +elif [ $ARG1 == 'stop' ]; then + rostopic pub /cmd_vel geometry_msgs/Twist "linear: + x: 0.0 + y: 0.0 + z: 0.0 +angular: + x: 0.0 + y: 0.0 + z: 0.0" +else +echo "Please enter one of the following: + forward + rotate + stop" +fi +``` + +Typing `ctrl+s` saves the file and then typing `ctrl+x` exits Nano. + +Again, a bash script just runs commands exactly as you would within a terminal. The above code takes an argument and publishes a *Twist* message over the **/cmd_vel** topic to drive the robot accordingly. + +Before running this script, let's get our ROS environment setup: + +1. Open a new terminal and type `roscore`. +2. Open a new terminal tab and run our TurtleBot3 simulation: + + `roslaunch turtlebot3_gazebo turtlebot3_empty_world.launch` + +Now, in a new terminal, run the script you created: + +`./move_turtlebot.sh` + +Did you get an error? That is because the permissions have not been properly set and you do not have execute permissions. You can observe the permissions of a file by typing `ls -la`. + +For the `move_turtlebot.sh` file you should see `-rw-rw-r--`. The first position indicates file type ('d' for directory, '-' for regular file), the next three positions indicate read (r), write (w), and execute (x) permissions for the file owner (u), the next three indicate permisions for the group owner of the file (g), and the last three characters indicate permissions for all other users (o). + +You can change the permissions of a file or directory using the command `chmod`: + +> ⌨️ **Syntax:** `chmod ` + +For example, if we wanted to give the Owner execute permissions, you can enter the command: + +`chmod u+x move_turtlebot.sh` + +Typically we will give all users executable permissions (`chmod +x move_turtlebot.sh`). This isn't the most secure thing to do, but in our controlled environment, it isn't an issue. If you type `ls -la` now you should see the 'x' permission added for each permission group (`-rwxrwxr-x`). + +Try running your script again: + +`./move_turtlebot.sh rotate` + +> 📝️ **Note:** You can remove permissions by utilizing the '-' character instead of '+'. + +Move to your github repo that you previously created: + +`cd ~/master_ws/src/ece387_master_sp23-USERNAME/master/` + +Now we are going to create a new ROS package. You learned this in the previous homework assignment, but we will continue practicing: + +`catkin_create_pkg module02 std_msgs rospy roscpp` + +Before we go any further, it is always a good idea to compile your workspace with the new package. To do that, we will use a command `catkin_make` from within the top-level of the workspace: + +`cd ~/master_ws` + +`catkin_make` + +Now go back to the `my_folder` that you created at the beginning of the lesson and create a new bash script, `bash_script.sh`, to accomplish the following: + +1. Moves into the package you just created (you will need to figure out the complete path to this folder) +1. Creates a directory called `my_scripts` +1. Moves into that directory +1. Creates a file called **move_turtlebot_square.py** +1. Lists all files showing the permissions +1. Modifies the permissions of the **move_turtlebot_square.py** file so all groups have executable permissions +1. Lists all files again showing the updated permissions + +Now run the script. If successful you should see the file listed without and then with execute permissions. + +We are done with this script so let's remove it (you will need to either use the complete path to the file you want to remove, or be in the directory of the file you want to remove). The `rm` command can remove folders or files. If you want to learn more about a command there are two helpful tools: **Help**: `rm --help`; **Manual**: `man rm`. + +Type the following to remove our bash script: + +`rm bash_script.sh`. + +```{note} +To delete a whole folder add the `-r` tag to remove directories and thier contents recursively (e.g., `rm -r my_folder`, but don't remove your folder just yet). +``` + +We can copy (`cp`, just like ctrl+c in a GUI) and move (`mv`, just like ctrl+x in a GUI) files and folders as well. Let's copy the `move_turtlebot.sh` to the `my_scripts` folder you created earlier: + +`cp move_turtlebot.sh ~/master_ws/src/ece387_master_sp23-USERNAME/master/module02/my_scripts` + +> ⌨️ **Syntax:** `cp ` + +```{note} +For the above to work, you must already be in the same folder as the `move_turtlebot.sh` file. Otherwise you have to use the absolute file path, such as `~/my_folder/move_turtlebot.sh`. +``` + +You can now delete your `my_folder` folder. + +`cd ..` + +`rm -r my_folder` + +Change directories to your `my_scripts` folder. We can use a ROS tool, `roscd`, to change directories to ROS packages without using the absolute file path: + +`roscd module02/my_scripts` + +> ⌨️ **Syntax:** `roscd ` + +Edit the **move_turtlebot_square.py** file and paste the following contents: + +```python +#!/usr/bin/env python3 +import rospy, time, math +from geometry_msgs.msg import Twist + +class MoveTurtleBot(): + def __init__(self): + self.pub = rospy.Publisher('cmd_vel', Twist, queue_size=1) + self.cmd = Twist() + self.ctrl_c = False + rospy.on_shutdown(self.shutdownhook) + self.rate = rospy.Rate(10) # 10 Hz + + def publish_cmd_vel_once(self): + """ + In case publishing fails on first attempt + """ + while not self.ctrl_c: + connections = self.pub.get_num_connections() + if connections > 0: + self.pub.publish(self.cmd) + rospy.loginfo("Cmd Published") + break + else: + self.rate.sleep() + + def shutdownhook(self): + rospy.loginfo("Shutting down. Stopping TurtleBot!") + self.stop_turtlebot() + self.ctrl_c = True + + def stop_turtlebot(self): + self.cmd.linear.x = 0.0 + self.cmd.angular.z = 0.0 + self.publish_cmd_vel_once() + + def move_time(self, moving_time = 10.0, lin_spd = 0.2, ang_spd = 0.2): + self.cmd.linear.x = lin_spd + self.cmd.angular.z = ang_spd + + self.publish_cmd_vel_once() + time.sleep(moving_time) + + def move_square(self): + i = 0 + + while not self.ctrl_c: + # Move Forward + self.move_time(moving_time = 2.0, lin_spd = 0.2, ang_spd = 0) + # Turn + ang_spd = 0.5 # rad/sec + moving_time = math.radians(90)/ang_spd + self.move_time(moving_time = moving_time, lin_spd = 0.0, ang_spd = ang_spd) + + +if __name__ == '__main__': + rospy.init_node('move_turtlebot') + move_object = MoveTurtleBot() + try: + move_object.move_square() + except rospy.ROSInterruptException: + pass +``` + +There is a lot going on in this script, but hopefully after the previous lesson some of it makes sense. To summarize, the script creates a node, `move_turtlebot`, that publishes **Twist** messages to the **/cmd_vel** topic to drive the robot in a square: drive forward, turn 90 degrees, drive forward, repeat. This script will run until killed using `ctrl+c`. + +The script is already executable, so you can run it using ROS! + +`rosrun module02 move_turtlebot_square.py` + +```{note} Note:** It won't be a perfect square as the robot doesn't turn perfectly, but it will be close! +``` + +The robot is now driving in a square until the script is killed. If you select the terminal and hit `ctrl+c`, it will kill the script. + +Run the script again and this time hit `ctrl+z`. You can see that the robot is still running, but the commands are not updating. This is because `ctrl+z` suspends the current process, but does not kill it. We can observe all running processes on Linux by typing `ps -faux`. As you can see, there are a lot! The `grep` command allows us to filter these processes. Try the following: + +`ps -faux | grep move_turtlebot_square.py` + +The first entry should be our process and the leftmost number is the process ID (PID). We can kill any process using this number and the `kill` command: + +`kill PID` replacing PID with the number listed. + +If you hit enter again, you should see that the process was killed. Unfortunately, the TurtleBot will just continue to execute the last command sent, so you need to kill the simulation as well. Just select that terminal and hit `ctrl+c`. + +The `grep` tool is very powerful and can be used with any Linux command. For example, if you wanted to see all turtlebot packages available to us, we could type the following: + +`rospack list` + +There are a lot, so this isn't very helpful, but we can filter this command! + +`rospack list | grep turtlebot` + +The vertical line, '|', pipes the results of the first command into the second command, so we can filter all packages looking only for turtlebot packages. + +## Working with a remote machine +Later in this course you will drive your TurtleBot around unplugged from a monitor and keyboard, but you will still need access to the Raspberry Pi on the robot to run ROS nodes. One of the easiest ways to remotely access a Linux machine is through a secure shell. To create a secure shell, you need the uesrname and hostname of the computer you want to remote into. For the Raspberry Pis, all usernames are `pi` and the hostname is your robot number (e.g., `robot0`). + +1. Open a terminal on your master +1. Check connectivity to the robot: + + `ping robotX` +
    +1. Create a secure shell to the robot: + + `ssh pi@robotX` +> ⌨️ **Syntax:** `ssh @` + +1. Enter the robot's password + + You should see that the terminal lists the `pi` username and your robot's hostname, `robotX`. Any command you run in this shell will execute on the robot. +
    +1. Create a new package on the robot we can use to store scripts for this module. This package should be called 'module02' and should be in your student repo. From the terminal ssh'd into the robot, type: + +`cd ece387_robot_sp23-USERNAME/robot/` + +`catkin_create_pkg module02` + +1. Change directories to your new folder. + +1. Print the working directory. You should get "/home/pi/robot_ws/src/ece387_robot_sp23-USERNAME/robot/" + +1. You can type `exit` to close an SSH connection (but don't close the connection yet!). + +> 📝️ **Note:** At times, there may be network issues and name resolution will fail. What this means is if you try to ping the robot from the master or vice versa using the hostname (e.g., `ping master0`) it will not work. However, it will work if you use the IP address (e.g., `ping 192.168.2.120`). To do this, you need the IP address of the machine you want to access. This IP address will change periodically. The easiest way to determine the current IP Address is, with the machine plugged into a monitor and keyboard, type `ip addr` in a terminal. This will list all of the network interfaces on the machine (such as *eth0* for ethernet and *wlan0* for wireless). You are looking for the `inet` field under `wlan0` (may be called `wlo1`). Now you can unplug the roobt from the monitor, ping the IP address to check connectivity, and then SSH into the robot. + +The other remote tool you may want to take advantage of is SCP, which securely copies a file to a remote machine. + +> ⌨️ **Syntax:** `scp @:` + +For example, copy the `move_turtlebot_square.py` file to your robot by typing the following in a new terminal on the master: + +`roscd module02/my_scripts` + +`scp move_turtlebot_square.py pi@robot0:/home/pi/robot_ws/src/ece387_robot_sp23-USERNAME/robot/module02/src/` + +> 📝️ **Note:** The destination uses the absolute path you printed earlier. + +In your secure shell list the contents of your 'my_scripts' folder. You should see the `move_turtlebot_square.py` file. Check that it is executable. If it is not, make it executable. + +Now let's move our TurtleBot using the script on the robot: + +1. Run roscore on the master. + +1. Run the simulated robot on the master. + +1. SSH into your robot Raspberry Pi. + +1. Run the move_turtlebot_square.py using the `rosrun` command on the robot. + +You are now controlling your simulation (which is running on the master) remotely from the robot. In future lessons you will have nodes running on your robot to drive the robot and will control the robot remotely from the master. + +## ROS +Below you will run ROS commands. The "!" character in the front allows us to run bash commands from Jupter and would **NOT** be used in the command line. + +Accomplish the following on the master by adding the commands necessary below: + +List all running nodes: + + +```python + +``` + +List the active topics: + + +``` + +``` + +Display running nodes and communication between them: + + +```python + +``` + +Exit the rqt_graph. + +Show information about a the **/cmd_vel** topic such as what type of messages are sent over the topic and publishing and subscribing nodes. + + +```python + +``` + +Display information about the message that is sent over the **/cmd_vel** topic. + + +```python + +``` + +Display messages sent over the **/cmd_vel** topic: + + +```python + +``` + +## Checkpoint +Once complete, get checked off by an instructor showing the output of each of the above. + +```{note} You will use all of the above ROS commands for each lab to write your lab reports. You could create a bash script to run these commands automatically ;) +``` + +Additionally, place screen captures showing each of the commands running or the windows they bring up into a folder within ../module02/Pictures on your master repo. Then push the repo to get credit for this work. + +## Summary +You have seen a lot of different Linux/ROS commands during this lesson but it only scratches the surface. There are tons of online resources available if you want to learn more. I recommend working through a few of these tutorials: http://www.ee.surrey.ac.uk/Teaching/Unix/. They are fairly quick and willl give you more insight into the tools we discussed. + +## Cleanup +In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. In each of the notebooks reset the Jupter kernel and clear output. Now it is safe to exit out of this window. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. diff --git a/_sources/Module3_Python3/ICE3_ClientServer.md b/_sources/Module3_Python3/ICE3_ClientServer.md old mode 100755 new mode 100644 index 1962147..6689f06 --- a/_sources/Module3_Python3/ICE3_ClientServer.md +++ b/_sources/Module3_Python3/ICE3_ClientServer.md @@ -1,198 +1,198 @@ -# ICE3 - Client and Server - -## A note on this document -This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. - - -## Purpose -This Jupyter Notebook will allow you to practice some of the techniques you have learned over the last few modules. You will develop an advanced chat client and server (similar to ICE1) enabling a client to send a message and a server to respond accordingly. - -## Initialize ROS: -The first step when utilizing ROS is to initialize *roscore*. There are two methods to accomplish this: first, by explicitly running *roscore* and second, by running a launch file (which will initialize *roscore* if it is not already running). During the first portion of this course we will explicitly run *roscore* and then take advantage of launch files later in the course. - -Copy the following code and run it in a new terminal (use the shortcut `ctrl+alt+t` to open a new terminal window or select an open terminal and hit `ctrl+shift+t` to open a new terminal tab): - -`roscore` - -## Implementing the chat client -> 📝️ **Note:** This Jupyter Notebook will require you to enter Python3 code within code sections. You can type any Python3 code and expand the block if necessary. After typing the code, execute the code block before moving forward. - -### Import modules - -> ⚠️ **Important:** Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a `*` character. Do not move to the next step until the `*` is gone. - - -```python -# import required modules for ROS and the String message from std_msgs - - -``` - -#### Client Class -1. Create a Client class with a dictionary used to map numbers to messages. -2. Initialize the class with the following: - 1. an instance variable to store the String message - 2. a publisher that publishes String messages on the client topic - 3. a subscriber to the server topic which receives String messages and calls a callback when messages are sent. - 4. a timer that runs every second and calls a class method - 5. nicely handle shutdown -3. Create the callback input class method that is ran every second and has the user pick a message to send. -4. Create the callback received class method that is called every time a message is received from the server. -5. Handle shutdown. - - -```python -class Client: - MESSAGE = {1: "Hello!", 2: "How are you?", 3: "Where are you from?", - 4: "What are you doing today?"} - - def __init__(self): - # 2.A - - # 2.B - - # 2.C - - # 2.D - - # 2.E nicely handle shutdown - self.ctrl_c = False - rospy.on_shutdown(self.shutdownhook) - - def callback_input(self, event): - valid = False - while not valid and not self.ctrl_c: - # get input from user (you must inform them their options) - - try: - # convert to int, if not number throw ValueError - val = int(chat_str) - # check if valid number, if valid then access - # that entry in the dictionary, publish the message - # and set valid to True; if not valid, print error - # message to user (make this error message useful) - - - - - - except ValueError: - # print error message to user (make - # this error message useful) - - - def callback_received(self, msg): - # print message sent to the server - - # print the response from the server - - # handle shutdown - def shutdownhook(self): - print("Shutting down the client") - self.ctrl_c = True -``` - -#### Main -The main function calls initializes our node, creates an instance of the client class, then runs forever. - - -```python -def main(): - # initialize node - - # create an instance of the client class - - # run forever - rospy.spin() -``` - -#### Run the program - - -```python -main() -``` - -### Implementing the chat server - - -```python -# import required modules for ROS and the String message from std_msgs - - -``` - -#### Server Class -1. Create a server class with a dictionary used to map messages to responses (one for each message from the client). -2. Initialize the class with the following: - 1. an instance variable to store the String message - 2. a subscriber to the client topic which receives String messages and calls a callback called received. - 3. a publisher to the server topic which sends String messages - 4. nicely handle shutdown -3. Create the callback received class method that is called every time a message is received from the client. -5. Handle shutdown. - - -```python -class Server: - # class dictionary storing server responses - MESSAGE = - - def __init__(self): - # 2.A - - # 2.B - - # 2.C - - # 2.D nicely handle shutdown - self.ctrl_c = False - rospy.on_shutdown(self.shutdownhook) - - def callback_received(self, msg): - # print the message from the client - - # print the response that will be sent to the client - - # publish the response - - # handle shutdown - def shutdownhook(self): - print("Shutting down the server") - self.ctrl_c = True -``` - -#### Main -The main function calls initializes our node, creates an instance of the server class, then runs forever. - - -```python -def main(): - # initialize node - - # create an instance of the server class - - # run forever - rospy.spin() -``` - -#### Run the program - - -```python -main() -``` - -At this point, the server is waiting for the client to send a message. Browse back to your client and type a message! You should see that message show up above. - - - - - - - - - - -### ROS commands -Open the [ROS](ROS.md) notebook and follow the instructions. +# ICE3 - Client and Server + +## A note on this document +This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. + + +## Purpose +This Jupyter Notebook will allow you to practice some of the techniques you have learned over the last few modules. You will develop an advanced chat client and server (similar to ICE1) enabling a client to send a message and a server to respond accordingly. + +## Initialize ROS: +The first step when utilizing ROS is to initialize *roscore*. There are two methods to accomplish this: first, by explicitly running *roscore* and second, by running a launch file (which will initialize *roscore* if it is not already running). During the first portion of this course we will explicitly run *roscore* and then take advantage of launch files later in the course. + +Copy the following code and run it in a new terminal (use the shortcut `ctrl+alt+t` to open a new terminal window or select an open terminal and hit `ctrl+shift+t` to open a new terminal tab): + +`roscore` + +## Implementing the chat client +> 📝️ **Note:** This Jupyter Notebook will require you to enter Python3 code within code sections. You can type any Python3 code and expand the block if necessary. After typing the code, execute the code block before moving forward. + +### Import modules + +> ⚠️ **Important:** Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a `*` character. Do not move to the next step until the `*` is gone. + + +```python +# import required modules for ROS and the String message from std_msgs + + +``` + +#### Client Class +1. Create a Client class with a dictionary used to map numbers to messages. +2. Initialize the class with the following: + 1. an instance variable to store the String message + 2. a publisher that publishes String messages on the client topic + 3. a subscriber to the server topic which receives String messages and calls a callback when messages are sent. + 4. a timer that runs every second and calls a class method + 5. nicely handle shutdown +3. Create the callback input class method that is ran every second and has the user pick a message to send. +4. Create the callback received class method that is called every time a message is received from the server. +5. Handle shutdown. + + +```python +class Client: + MESSAGE = {1: "Hello!", 2: "How are you?", 3: "Where are you from?", + 4: "What are you doing today?"} + + def __init__(self): + # 2.A + + # 2.B + + # 2.C + + # 2.D + + # 2.E nicely handle shutdown + self.ctrl_c = False + rospy.on_shutdown(self.shutdownhook) + + def callback_input(self, event): + valid = False + while not valid and not self.ctrl_c: + # get input from user (you must inform them their options) + + try: + # convert to int, if not number throw ValueError + val = int(chat_str) + # check if valid number, if valid then access + # that entry in the dictionary, publish the message + # and set valid to True; if not valid, print error + # message to user (make this error message useful) + + + + + + except ValueError: + # print error message to user (make + # this error message useful) + + + def callback_received(self, msg): + # print message sent to the server + + # print the response from the server + + # handle shutdown + def shutdownhook(self): + print("Shutting down the client") + self.ctrl_c = True +``` + +#### Main +The main function calls initializes our node, creates an instance of the client class, then runs forever. + + +```python +def main(): + # initialize node + + # create an instance of the client class + + # run forever + rospy.spin() +``` + +#### Run the program + + +```python +main() +``` + +### Implementing the chat server + + +```python +# import required modules for ROS and the String message from std_msgs + + +``` + +#### Server Class +1. Create a server class with a dictionary used to map messages to responses (one for each message from the client). +2. Initialize the class with the following: + 1. an instance variable to store the String message + 2. a subscriber to the client topic which receives String messages and calls a callback called received. + 3. a publisher to the server topic which sends String messages + 4. nicely handle shutdown +3. Create the callback received class method that is called every time a message is received from the client. +5. Handle shutdown. + + +```python +class Server: + # class dictionary storing server responses + MESSAGE = + + def __init__(self): + # 2.A + + # 2.B + + # 2.C + + # 2.D nicely handle shutdown + self.ctrl_c = False + rospy.on_shutdown(self.shutdownhook) + + def callback_received(self, msg): + # print the message from the client + + # print the response that will be sent to the client + + # publish the response + + # handle shutdown + def shutdownhook(self): + print("Shutting down the server") + self.ctrl_c = True +``` + +#### Main +The main function calls initializes our node, creates an instance of the server class, then runs forever. + + +```python +def main(): + # initialize node + + # create an instance of the server class + + # run forever + rospy.spin() +``` + +#### Run the program + + +```python +main() +``` + +At this point, the server is waiting for the client to send a message. Browse back to your client and type a message! You should see that message show up above. + + + + + + + + + + +### ROS commands +Open the [ROS](ROS.md) notebook and follow the instructions. diff --git a/_sources/Module3_Python3/Python3.md b/_sources/Module3_Python3/Python3.md old mode 100755 new mode 100644 index 9811b30..cb27d23 --- a/_sources/Module3_Python3/Python3.md +++ b/_sources/Module3_Python3/Python3.md @@ -1,436 +1,436 @@ -# Python3 for Robotics - -## A note on this document -This document is known as a Jupyter notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. - -## Purpose -This assignment will help refresh Python3 fundamentals through application. You will implement a rock paper scissors player and computer that will compete over ROS. - -## Import modules at top of Python code -With Python code, the convention is to import all required modules at the top of the python script. Modules are packages of additional functionality built by others. Typically, a module is comprised of compiled code that serves a specific function. - -By importing all modules at the beginning of the script, we ensure that all modules have been loaded by the time we need them later in the script. Also, it ensures anyone reading our code can very quickly see all the modules (dependencies) needed to run the code. - -Our imports are in the following code block. - -> ⚠️ **Important:** Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a `*` character. Do not move to the next step until the `*` is gone. - - -```python -# import required modules -import random -``` - -Here, the module is called "random", and by importing the module this way, we can use the module by referencing "random" in our code. There are a few other ways to do imports, but we will wait get to those in another script. - -Another thing to note is we can make comments in python by starting a line with "#". Comments are lines in your code that do not perform any function and are intended only to inform others (and your future self) about the purpose of your code. It is a good idea to document the purpose of your code at the beginning of the code with comments and to leave short and concise comments throughout your code. Many people get lazy and fail to comment their code, but don't fall prey to that trap! - -## Variables -In programming languages, variables are containers that store simple information. In Python, variables can store strings or numbers. A string is a block of text, such as "my TurtleBot is faster than yours". A number can be an integer or a decimal number. - - -```python -# create a variable to hold a string asking the user if they want to play rock, paper, scissors -welcome_text = 'Would you like to play rock, paper, scissors?' - -# create a variable holding a number representing the number of possible choices in rock, paper, scissors -num_choices = 3 - -# verify the variables are holding the correct information by using the "print" function to display the content of both variables -print('The variable welcome_text contains: %s' % welcome_text) -print('The variable num_choices contains: %02d' % num_choices) -``` - -A few things are going on here while creating variables. First, Python lets us define variables using the convention variable_name = value. As long as the variable name is unique (it is not the name of a function, module, or other variable), we can define our variable this way. For strings, single quotes or double quotes must be used around the text that needs to be in the variable; it doesn't matter whether you choose single or double quotes, just stay consistent. - -Second, we show the print function. The print function takes a string as its input and prints the string to the output, which is right below the block of text. Given `format % values`, *format* includes a string and `%` conversion **items** which are replaced with elements of *values* (such as a 2 digit value `%02d`). The *values* must be a tuple with the exact number of **items** specified in the *format* string. - -## Lists -In Python, we can store information in more than just variables. Python has a data type called "lists", and they store just that - lists. An example of a Python list is below. - - -```python -# create a list containing all of the possible choices in rock, paper, scissors -choices = ['Rock','Paper','Scissors'] - -# print the contents of "choices" to see what it contains -print(choices) -``` - -We chose to use strings for each item in the list above, but we could have just as easily chosen numbers or other objects instead. Lists always begin with an open hard bracket, [, and end with the closed hard bracket, ]. Additionally, list items are separated by commas. For this case, choices is the name of our list. Python has other data structures, such as dictionaries, tuples, and classes, but we will stick to variables and lists for now. - -You're probably thinking, "Lists look great and all, but how do I use them?" Great question! All of the items in the list are given a position, and in Python lingo, this is called an index. Each item (string in this case) in the list has an index, and the indices start at 0 on the furthest left and increase as you move to the right through the list. You can use an item's index to retrieve it from the list. Using the name of the list, choices in this case, followed by hard brackets [] with the index of the item in the middle will retrieve the item. - - - -```python -# retrieve each item from the list with its index and print it to the output -print('The first item is: %s' % choices[0]) -print('The second item is: %s' % choices[1]) -print('The third item is: %s' % choices[2]) -print('All items of the list include: %s' % choices) - -# find the number of items in the list (length of the list) -len(choices) -``` - -## Functions -In Python, we can create functions to perform a specific task. Functions help us organize our code and simplify our code by allowing us to call a function multiple times rather than writing the same code multiple times. Python functions start with the keyword `def` followed by the function name, an open parenthesis, any inputs to pass to the function, a close parenthesis, and a colon. When we start writing what the function will do, we need to start on a new line and "tab-in" so that Python knows we are writing within the scope of the function. The function finishes when it gets to the last line of code or it reaches a `return` keyword. Let's write a function that represents the computer randomly selecting either 'Rock', 'Paper', or 'Scissors'. - - -```python -# create a function that represents the computer selecting either 'Rock', -# 'Paper', or 'Scissors' at random -# inputs: choices, which is a list representing any item the -# computer can choose -# outputs: rand_choice, an item chosen at random from the input, -# choices -def get_comp_choice(choices_list): - # use random module imported earlier to select a random index from the list - rand_index = random.randint(0,len(choices_list)-1) - # use the index to select an item from the list - rand_choice = choices_list[rand_index] - # end the function by returning the computer's choice - return rand_choice -``` - - -```python -# call the function a few times to test that it works properly -# we are using the list 'choices' from earlier -choice1 = get_comp_choice(choices) -print('Test Choice 1: ',choice1) -choice2 = get_comp_choice(choices) -print('Test Choice 2: ',choice2) -choice3 = get_comp_choice(choices) -print('Test Choice 3: ',choice3) -``` - -In the function `get_comp_choice`, we use the `randint` function from the `random` module we imported earlier. The `randint` function accepts as its input a range of integers from which it will select an integer at random. Then, it returns this integer and stores it in the variable `rand_index`. The first input to this function is the smallest possible integer to select, and the second input is the largest possible integer to select. The random integer is used to retrieve an item from the input list `choices_list`. Finally, we use the `return` keyword to allow the user to store the choice in a variable when the function is called. - -The second code block calls the function 3 times to test that it works as we expect. Press `Shift + Enter` in the code block multiple times to see how the "Test Choices" change. As you can see above, we call a function by using its name followed by an open parenthesis, any inputs, and a close parenthesis. You can pass a different list to the function to further test that it works as you expect! - -## User input -In order to play rock, paper, scissors, we need to get input from the user. The python `input` function is perfect for this. - - -```python -# collect an input string from the user -choice = input("Please choose 'Rock', 'Paper', or 'Scissors' by typing either one of the three:") -``` - - -```python -# print the contents of the variable choice -print(choice) - -# print the type of data contained in the variable choice -print(type(choice)) -``` - -The `input` function takes a string of instructions as input and requests that the user input a string. After the user inputs a string, the function assigns that input string to a variable. In this case, the variable `choice` will contain whatever *string* the user types in the box (numbers typed in the input box will be interpreted as strings). - -In the string of instructions passed to the `input` function, the string is surrounded by double quotes, and the individual items are surrounded by single quotes. When placing quotation marks inside of a string, the quotation marks inside the string must be single if the outside quotes are double and vice versa. Lastly, the string includes `\n`, the `\n` tells python to create a newline in the output, and this ensures that the box for input is on a separate line than the instructions. - -We chose to print out the contents of `choice` to the output to show what is actually stored in the variable `choice`. Also, we use the `type` function inside of the `print` function to print the data type being stored in choice, and the output of `` confirms that strings are always stored in variables created by the `input` function. Try putting different words and numbers in the input box to verify this yourself! - -## Validating user input with if statements - -Now that we have the user input, let's verify that the user actually did input either Rock, Paper, or Scissors (humans make mistakes after all). We can do this with if statements in Python. If the user inputted a valid string, we can continue with the game. If not, the user has to start over and put in a new string. The key words `if`, `elif`, and `else` followed by a logical expression and a colon denote valid if, else if, and else statements, respectively. Below is an example of an if statement to check if the input is valid. - - -```python -# use if statement to check if user's input is valid -# start by checking if choice stores the string Rock -if choice == 'Rock': - print('Good input') - valid_input = True -#check if choice stores the string Paper -elif choice == 'Paper': - print('Good input') - valid_input = True -#check if choice stores the string Scissors -elif choice == 'Scissors': - print('Good input') - valid_input = True -# any other strings in choice are invalid -else: - print('Bad input. Try again.') - valid_input = False -``` - -Logical expressions in Python compare one value or string to another. Python uses `==` to test equality and `!=` to test if something is not equal to something else. Python can check if a value is greater or less than another number as well. Googling 'Logical operators in Python' will give a complete list. - -Variables can also store boolean values, meaning they can store `True` or `False`. These are shortcuts for saying something is on or off, and in this case, we are using `True` if the input is valid and `False` if the input is not valid. We will use them later to make our code more concise. - -Can you think of a way to rewrite the if statement above without the `elif` statements? Hint: Python uses `or` and `and` to combine multiple logical expressions into one. Hint for more advanced way: you can use the list `choices` from above. - -One final note about our input validation: if the user inputs rock, what happens? Can you think of a way to accept this as a valid input without adding another `elif` statement? - -## Classes -We will use ROS to communicate and classes enable us to bundle data and functionality together while taking advantage of ROS tools. A class creates new types of objects, allowing *instances* of that class to be made. Each class can have attributes and methods that, combined, maintain and modify a class's state. For example, we could create a `Robot` class that has attributes such as number of wheels, speed, and physics. There might be methods that provide a controller information about the robot's state, such as `get_speed` or `set_speed`. If we were using multiple robots we could have multiple instances of the class and each instance's attributes and methods would only control that particular robot. We will use classes throughout this course. - -A class name should use the CapWords convention. When we are operating within the scope of a class, we have to make sure to use the correct namespaces. Within a class you will see the keyword `self` quite a bit. This refers to an instance variable or method and is scoped within the class. For example, if a class has an instance variable `num_wheels`, when within that class, we must access it using the explicit scope: `self.num_wheels`. - -Let's discuss the components of the player class - -### Create the player class and init method -```python -class Player: - # class constant to store player choices - CHOICES = ['Rock', 'Paper', 'Scissors'] - - # initialize class - def __init__(self, num_rounds = 3): - # instance variables - self.num_rounds = num_rounds # total numer of games to be played - self.round = 0 # number of rounds played so far - self.players_choice = String() - self.computers_choice = String() - self.round_complete = True - self.user_wins = 0 - self.computer_wins = 0 - - # publisher to send player's choice over the player topic - self.pub = rospy.Publisher('player', String, queue_size=1) - - # timer to request user's input, should be called once/second, but will stall - # on user input - rospy.Timer(rospy.Duration(1), self.callback_players_choice) - - # subscriber to receive the computer's choice over the computer topic - rospy.Subscriber('computer', String, self.callback_computers_choice) -``` - -The above creates and initializes a class called Player. `CHOICES` is a class level variable which is the same for every instance of the class. When an instance of the class is created the number of rounds to be played can be passed to the init function and becomes part of the instance of the class. If no input is provided, the default is set to 3. It is always good to set a default. You can see that comments are provided to describe a few of the instance variables that might need some clarification, but not for the self explanatory variables. That is one benefit of using descriptive variable names, it is easier for some to understand what is going on. - -To access an instance variable within the class you must use the `self` keyword. - -The last three lines create the ROS publisher, timer, and subscriber. The publisher and subscriber should be familiar to you by now, and the timer periodically calls a callback which we will use to take input from the user. It runs every duration, which is every one second in this example. - -### ROS callback method part of the Player class called by timer -```python -# method to request user input and publish to computer -# inputs: event, ROS information on timing of interrupt. -# Can be ignored. -# outputs: publishes player's choice -def callback_players_choice(self, event): - # check to ensure we wait for the round to be complete to get user input - if self.round_complete and not self.is_game_complete(): - self.players_choice = input('Please choose between %s by typing either one of the three.\n' % self.CHOICES) - - # check to ensure it is a valid input - if self.players_choice in self.CHOICES: - self.round_complete = False - self.pub.publish(self.players_choice) - else: - print('Invalid input. Please choose between %s by typing either one of the three.\n' % self.CHOICES) -``` - -This is an example of a class method. In ROS we call it a callback, because some other process triggers the calling of the method instead of the main function. This particular method is called by the timer. The check is a more concise version of the one we saw earlier. If the check is valid the `choice` *String* is published over the *player* topic. - -### ROS callback method part of Player class called by topic -```python -# method to receive computer's choice and determine winner -# inputs: choice, computer's choice sent over the computer -# topic -# outputs: none -def callback_computers_choice(self, choice): - self.computers_choice = choice.data - self.round += 1 - print('Game %d: %s vs. %s' % (self.round, self.players_choice, self.computers_choice)) - - # Determine winner and increase number of wins for player by 1 - if self.players_choice == self.computers_choice: - print('Draw') - elif self.players_choice == 'Rock' and self.computers_choice == 'Scissors': - print('You win!') - self.user_wins += 1 - elif self.players_choice == 'Paper' and self.computers_choice == 'Rock': - print('You win!') - self.user_wins += 1 - elif self.players_choice == 'Scissors' and self.computers_choice == 'Paper': - print('You win!') - self.user_wins += 1 - else: - print('You lose.') - self.computer_wins += 1 - - self.round_complete = True -``` - -This class method is another callback that is called whenever a *String* message is sent over the *computer* topic. When called, the message is passed to the module. In this example we named the variable choice and store it in the `computers_choice` instance variable. - -### Class methods part of Player class -```python -# returns the number of wins by the user and computer -def get_results(self): - return self.user_wins, self.computer_wins - -# checks if the game is complete -def is_game_complete(self): - if self.num_rounds == self.round and self.round_complete: - return True - else: - return False -``` - -These are two class methods that are used either within the class or in main. Class variables are technically accessible in main, but the first method above is a better convention for accessing instance variables. - -## Let's start the game! -Alright, now that we remember the basics of a Python, let's build our game. We will start with the user player. - -### Initialize ROS: -The first step when utilizing ROS is to initialize *roscore*. There are two methods to accomplish this: first, by explicitly running *roscore* and second, by running a launch file (which will initialize *roscore* if it is not already running). During the first portion of this course we will explicitly run *roscore* and then take advantage of launch files later in the course. - -Copy the following code and run it in a new terminal (use the shortcut `ctrl+alt+t` to open a new terminal window): - -`roscore` - -### Import modules: - - -```python -import rospy -from std_msgs.msg import String -``` - -Here, we are importing two modules: *rospy*, which allows us to run ROS code in Python, and the *String* message from the *std_msgs* ROS package. - -### Build the class: -The below is a copy of what we saw above. - - -```python -class User: - # class constant to store user choices - CHOICES = ['Rock', 'Paper', 'Scissors'] - - # initialize class - def __init__(self, num_rounds = 3): - # instance variables - self.num_rounds = num_rounds # total numer of games to be played - self.round = 0 # number of rounds played so far - self.users_choice = String() - self.computers_choice = String() - self.round_complete = True - self.user_wins = 0 - self.computer_wins = 0 - - # publisher to send user's choice over the user topic - self.pub = rospy.Publisher('user_choice', String, queue_size=1) - - # timer to request user's input, should be called once per second, but will stall - # on user input - rospy.Timer(rospy.Duration(1), self.callback_users_choice) - - # subscriber to receive the computer's choice over the computer topic - rospy.Subscriber('computer_choice', String, self.callback_computers_choice) - - # method to request user input and publish to computer - # inputs: event, ROS information on timing of interrupt. - # Can be ignored. - # outputs: publishes user's choice - def callback_users_choice(self, event): - # check to ensure we wait for the round to be complete to get user input - if self.round_complete and not self.is_game_complete(): - self.users_choice = input('Please choose between %s by typing either one of the three.\n' - % self.CHOICES) - - # check to ensure it is a valid input - if self.users_choice in self.CHOICES: - self.round_complete = False - self.pub.publish(self.users_choice) - else: - print('Invalid input. Please choose between %s by typing either one of the three.\n' - % self.CHOICES) - - # method to receive computer's choice and determine winner - # inputs: choice, computer's choice sent over the computer - # topic - # outputs: none - def callback_computers_choice(self, choice): - self.computers_choice = choice.data - self.round += 1 - print('Game %d: %s vs. %s' % (self.round, self.users_choice, self.computers_choice)) - - # Determine winner and increase number of wins for user by 1 - if self.users_choice == self.computers_choice: - print('Draw') - elif self.users_choice == 'Rock' and self.computers_choice == 'Scissors': - print('You win!') - self.user_wins += 1 - elif self.users_choice == 'Paper' and self.computers_choice == 'Rock': - print('You win!') - self.user_wins += 1 - elif self.users_choice == 'Scissors' and self.computers_choice == 'Paper': - print('You win!') - self.user_wins += 1 - else: - print('You lose.') - self.computer_wins += 1 - - self.round_complete = True - - # returns the number of wins by the user and computer - def get_results(self): - return self.user_wins, self.computer_wins - - # checks if the game is complete - def is_game_complete(self): - if self.num_rounds == self.round and self.round_complete: - return True - else: - return False -``` - -### Create the main - - -```python -def main(): - # initialize a node called player - rospy.init_node('user') - - print("***Make sure your computer player is running.***") - - num_rounds = int(input('How many rounds do you want to play? ')) - - # create an instance of the class that will play the specified number of games - u = User(num_rounds) - - # run continuously until the game is complete - while not u.is_game_complete(): - pass - - # print results - user_wins, computer_wins = u.get_results() - print('Total number of user wins: %s' % (user_wins)) - print('Total number of computer wins: %s' % (computer_wins)) - print('Total number of draws: %s' % (num_rounds - (user_wins + computer_wins))) - print('User win percentage: %s' % (user_wins/num_rounds)) -``` - -In an actual Python script we will replace -```python -def main() -``` -with -```python -if __name__ == "__main__": -``` -This allows our python files to be imported into other python files that might also have a main() function. - -### Create the computer player -We must build and run the computer player before we can continue. Open the [RPSComputer](RPSComputer.ipynb) notebook and follow the instructions. - -### Run the program - - -```python -main() -``` - -### ROS refresher - -Open the [ROS](ROS.md) notebook and follow the instructions. +# Python3 for Robotics + +## A note on this document +This document is known as a Jupyter notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. + +## Purpose +This assignment will help refresh Python3 fundamentals through application. You will implement a rock paper scissors player and computer that will compete over ROS. + +## Import modules at top of Python code +With Python code, the convention is to import all required modules at the top of the python script. Modules are packages of additional functionality built by others. Typically, a module is comprised of compiled code that serves a specific function. + +By importing all modules at the beginning of the script, we ensure that all modules have been loaded by the time we need them later in the script. Also, it ensures anyone reading our code can very quickly see all the modules (dependencies) needed to run the code. + +Our imports are in the following code block. + +> ⚠️ **Important:** Importing classes may take some time. You will know the code block is still executing as the bracket on the left of the cell will change to a `*` character. Do not move to the next step until the `*` is gone. + + +```python +# import required modules +import random +``` + +Here, the module is called "random", and by importing the module this way, we can use the module by referencing "random" in our code. There are a few other ways to do imports, but we will wait get to those in another script. + +Another thing to note is we can make comments in python by starting a line with "#". Comments are lines in your code that do not perform any function and are intended only to inform others (and your future self) about the purpose of your code. It is a good idea to document the purpose of your code at the beginning of the code with comments and to leave short and concise comments throughout your code. Many people get lazy and fail to comment their code, but don't fall prey to that trap! + +## Variables +In programming languages, variables are containers that store simple information. In Python, variables can store strings or numbers. A string is a block of text, such as "my TurtleBot is faster than yours". A number can be an integer or a decimal number. + + +```python +# create a variable to hold a string asking the user if they want to play rock, paper, scissors +welcome_text = 'Would you like to play rock, paper, scissors?' + +# create a variable holding a number representing the number of possible choices in rock, paper, scissors +num_choices = 3 + +# verify the variables are holding the correct information by using the "print" function to display the content of both variables +print('The variable welcome_text contains: %s' % welcome_text) +print('The variable num_choices contains: %02d' % num_choices) +``` + +A few things are going on here while creating variables. First, Python lets us define variables using the convention variable_name = value. As long as the variable name is unique (it is not the name of a function, module, or other variable), we can define our variable this way. For strings, single quotes or double quotes must be used around the text that needs to be in the variable; it doesn't matter whether you choose single or double quotes, just stay consistent. + +Second, we show the print function. The print function takes a string as its input and prints the string to the output, which is right below the block of text. Given `format % values`, *format* includes a string and `%` conversion **items** which are replaced with elements of *values* (such as a 2 digit value `%02d`). The *values* must be a tuple with the exact number of **items** specified in the *format* string. + +## Lists +In Python, we can store information in more than just variables. Python has a data type called "lists", and they store just that - lists. An example of a Python list is below. + + +```python +# create a list containing all of the possible choices in rock, paper, scissors +choices = ['Rock','Paper','Scissors'] + +# print the contents of "choices" to see what it contains +print(choices) +``` + +We chose to use strings for each item in the list above, but we could have just as easily chosen numbers or other objects instead. Lists always begin with an open hard bracket, [, and end with the closed hard bracket, ]. Additionally, list items are separated by commas. For this case, choices is the name of our list. Python has other data structures, such as dictionaries, tuples, and classes, but we will stick to variables and lists for now. + +You're probably thinking, "Lists look great and all, but how do I use them?" Great question! All of the items in the list are given a position, and in Python lingo, this is called an index. Each item (string in this case) in the list has an index, and the indices start at 0 on the furthest left and increase as you move to the right through the list. You can use an item's index to retrieve it from the list. Using the name of the list, choices in this case, followed by hard brackets [] with the index of the item in the middle will retrieve the item. + + + +```python +# retrieve each item from the list with its index and print it to the output +print('The first item is: %s' % choices[0]) +print('The second item is: %s' % choices[1]) +print('The third item is: %s' % choices[2]) +print('All items of the list include: %s' % choices) + +# find the number of items in the list (length of the list) +len(choices) +``` + +## Functions +In Python, we can create functions to perform a specific task. Functions help us organize our code and simplify our code by allowing us to call a function multiple times rather than writing the same code multiple times. Python functions start with the keyword `def` followed by the function name, an open parenthesis, any inputs to pass to the function, a close parenthesis, and a colon. When we start writing what the function will do, we need to start on a new line and "tab-in" so that Python knows we are writing within the scope of the function. The function finishes when it gets to the last line of code or it reaches a `return` keyword. Let's write a function that represents the computer randomly selecting either 'Rock', 'Paper', or 'Scissors'. + + +```python +# create a function that represents the computer selecting either 'Rock', +# 'Paper', or 'Scissors' at random +# inputs: choices, which is a list representing any item the +# computer can choose +# outputs: rand_choice, an item chosen at random from the input, +# choices +def get_comp_choice(choices_list): + # use random module imported earlier to select a random index from the list + rand_index = random.randint(0,len(choices_list)-1) + # use the index to select an item from the list + rand_choice = choices_list[rand_index] + # end the function by returning the computer's choice + return rand_choice +``` + + +```python +# call the function a few times to test that it works properly +# we are using the list 'choices' from earlier +choice1 = get_comp_choice(choices) +print('Test Choice 1: ',choice1) +choice2 = get_comp_choice(choices) +print('Test Choice 2: ',choice2) +choice3 = get_comp_choice(choices) +print('Test Choice 3: ',choice3) +``` + +In the function `get_comp_choice`, we use the `randint` function from the `random` module we imported earlier. The `randint` function accepts as its input a range of integers from which it will select an integer at random. Then, it returns this integer and stores it in the variable `rand_index`. The first input to this function is the smallest possible integer to select, and the second input is the largest possible integer to select. The random integer is used to retrieve an item from the input list `choices_list`. Finally, we use the `return` keyword to allow the user to store the choice in a variable when the function is called. + +The second code block calls the function 3 times to test that it works as we expect. Press `Shift + Enter` in the code block multiple times to see how the "Test Choices" change. As you can see above, we call a function by using its name followed by an open parenthesis, any inputs, and a close parenthesis. You can pass a different list to the function to further test that it works as you expect! + +## User input +In order to play rock, paper, scissors, we need to get input from the user. The python `input` function is perfect for this. + + +```python +# collect an input string from the user +choice = input("Please choose 'Rock', 'Paper', or 'Scissors' by typing either one of the three:") +``` + + +```python +# print the contents of the variable choice +print(choice) + +# print the type of data contained in the variable choice +print(type(choice)) +``` + +The `input` function takes a string of instructions as input and requests that the user input a string. After the user inputs a string, the function assigns that input string to a variable. In this case, the variable `choice` will contain whatever *string* the user types in the box (numbers typed in the input box will be interpreted as strings). + +In the string of instructions passed to the `input` function, the string is surrounded by double quotes, and the individual items are surrounded by single quotes. When placing quotation marks inside of a string, the quotation marks inside the string must be single if the outside quotes are double and vice versa. Lastly, the string includes `\n`, the `\n` tells python to create a newline in the output, and this ensures that the box for input is on a separate line than the instructions. + +We chose to print out the contents of `choice` to the output to show what is actually stored in the variable `choice`. Also, we use the `type` function inside of the `print` function to print the data type being stored in choice, and the output of `` confirms that strings are always stored in variables created by the `input` function. Try putting different words and numbers in the input box to verify this yourself! + +## Validating user input with if statements + +Now that we have the user input, let's verify that the user actually did input either Rock, Paper, or Scissors (humans make mistakes after all). We can do this with if statements in Python. If the user inputted a valid string, we can continue with the game. If not, the user has to start over and put in a new string. The key words `if`, `elif`, and `else` followed by a logical expression and a colon denote valid if, else if, and else statements, respectively. Below is an example of an if statement to check if the input is valid. + + +```python +# use if statement to check if user's input is valid +# start by checking if choice stores the string Rock +if choice == 'Rock': + print('Good input') + valid_input = True +#check if choice stores the string Paper +elif choice == 'Paper': + print('Good input') + valid_input = True +#check if choice stores the string Scissors +elif choice == 'Scissors': + print('Good input') + valid_input = True +# any other strings in choice are invalid +else: + print('Bad input. Try again.') + valid_input = False +``` + +Logical expressions in Python compare one value or string to another. Python uses `==` to test equality and `!=` to test if something is not equal to something else. Python can check if a value is greater or less than another number as well. Googling 'Logical operators in Python' will give a complete list. + +Variables can also store boolean values, meaning they can store `True` or `False`. These are shortcuts for saying something is on or off, and in this case, we are using `True` if the input is valid and `False` if the input is not valid. We will use them later to make our code more concise. + +Can you think of a way to rewrite the if statement above without the `elif` statements? Hint: Python uses `or` and `and` to combine multiple logical expressions into one. Hint for more advanced way: you can use the list `choices` from above. + +One final note about our input validation: if the user inputs rock, what happens? Can you think of a way to accept this as a valid input without adding another `elif` statement? + +## Classes +We will use ROS to communicate and classes enable us to bundle data and functionality together while taking advantage of ROS tools. A class creates new types of objects, allowing *instances* of that class to be made. Each class can have attributes and methods that, combined, maintain and modify a class's state. For example, we could create a `Robot` class that has attributes such as number of wheels, speed, and physics. There might be methods that provide a controller information about the robot's state, such as `get_speed` or `set_speed`. If we were using multiple robots we could have multiple instances of the class and each instance's attributes and methods would only control that particular robot. We will use classes throughout this course. + +A class name should use the CapWords convention. When we are operating within the scope of a class, we have to make sure to use the correct namespaces. Within a class you will see the keyword `self` quite a bit. This refers to an instance variable or method and is scoped within the class. For example, if a class has an instance variable `num_wheels`, when within that class, we must access it using the explicit scope: `self.num_wheels`. + +Let's discuss the components of the player class + +### Create the player class and init method +```python +class Player: + # class constant to store player choices + CHOICES = ['Rock', 'Paper', 'Scissors'] + + # initialize class + def __init__(self, num_rounds = 3): + # instance variables + self.num_rounds = num_rounds # total numer of games to be played + self.round = 0 # number of rounds played so far + self.players_choice = String() + self.computers_choice = String() + self.round_complete = True + self.user_wins = 0 + self.computer_wins = 0 + + # publisher to send player's choice over the player topic + self.pub = rospy.Publisher('player', String, queue_size=1) + + # timer to request user's input, should be called once/second, but will stall + # on user input + rospy.Timer(rospy.Duration(1), self.callback_players_choice) + + # subscriber to receive the computer's choice over the computer topic + rospy.Subscriber('computer', String, self.callback_computers_choice) +``` + +The above creates and initializes a class called Player. `CHOICES` is a class level variable which is the same for every instance of the class. When an instance of the class is created the number of rounds to be played can be passed to the init function and becomes part of the instance of the class. If no input is provided, the default is set to 3. It is always good to set a default. You can see that comments are provided to describe a few of the instance variables that might need some clarification, but not for the self explanatory variables. That is one benefit of using descriptive variable names, it is easier for some to understand what is going on. + +To access an instance variable within the class you must use the `self` keyword. + +The last three lines create the ROS publisher, timer, and subscriber. The publisher and subscriber should be familiar to you by now, and the timer periodically calls a callback which we will use to take input from the user. It runs every duration, which is every one second in this example. + +### ROS callback method part of the Player class called by timer +```python +# method to request user input and publish to computer +# inputs: event, ROS information on timing of interrupt. +# Can be ignored. +# outputs: publishes player's choice +def callback_players_choice(self, event): + # check to ensure we wait for the round to be complete to get user input + if self.round_complete and not self.is_game_complete(): + self.players_choice = input('Please choose between %s by typing either one of the three.\n' % self.CHOICES) + + # check to ensure it is a valid input + if self.players_choice in self.CHOICES: + self.round_complete = False + self.pub.publish(self.players_choice) + else: + print('Invalid input. Please choose between %s by typing either one of the three.\n' % self.CHOICES) +``` + +This is an example of a class method. In ROS we call it a callback, because some other process triggers the calling of the method instead of the main function. This particular method is called by the timer. The check is a more concise version of the one we saw earlier. If the check is valid the `choice` *String* is published over the *player* topic. + +### ROS callback method part of Player class called by topic +```python +# method to receive computer's choice and determine winner +# inputs: choice, computer's choice sent over the computer +# topic +# outputs: none +def callback_computers_choice(self, choice): + self.computers_choice = choice.data + self.round += 1 + print('Game %d: %s vs. %s' % (self.round, self.players_choice, self.computers_choice)) + + # Determine winner and increase number of wins for player by 1 + if self.players_choice == self.computers_choice: + print('Draw') + elif self.players_choice == 'Rock' and self.computers_choice == 'Scissors': + print('You win!') + self.user_wins += 1 + elif self.players_choice == 'Paper' and self.computers_choice == 'Rock': + print('You win!') + self.user_wins += 1 + elif self.players_choice == 'Scissors' and self.computers_choice == 'Paper': + print('You win!') + self.user_wins += 1 + else: + print('You lose.') + self.computer_wins += 1 + + self.round_complete = True +``` + +This class method is another callback that is called whenever a *String* message is sent over the *computer* topic. When called, the message is passed to the module. In this example we named the variable choice and store it in the `computers_choice` instance variable. + +### Class methods part of Player class +```python +# returns the number of wins by the user and computer +def get_results(self): + return self.user_wins, self.computer_wins + +# checks if the game is complete +def is_game_complete(self): + if self.num_rounds == self.round and self.round_complete: + return True + else: + return False +``` + +These are two class methods that are used either within the class or in main. Class variables are technically accessible in main, but the first method above is a better convention for accessing instance variables. + +## Let's start the game! +Alright, now that we remember the basics of a Python, let's build our game. We will start with the user player. + +### Initialize ROS: +The first step when utilizing ROS is to initialize *roscore*. There are two methods to accomplish this: first, by explicitly running *roscore* and second, by running a launch file (which will initialize *roscore* if it is not already running). During the first portion of this course we will explicitly run *roscore* and then take advantage of launch files later in the course. + +Copy the following code and run it in a new terminal (use the shortcut `ctrl+alt+t` to open a new terminal window): + +`roscore` + +### Import modules: + + +```python +import rospy +from std_msgs.msg import String +``` + +Here, we are importing two modules: *rospy*, which allows us to run ROS code in Python, and the *String* message from the *std_msgs* ROS package. + +### Build the class: +The below is a copy of what we saw above. + + +```python +class User: + # class constant to store user choices + CHOICES = ['Rock', 'Paper', 'Scissors'] + + # initialize class + def __init__(self, num_rounds = 3): + # instance variables + self.num_rounds = num_rounds # total numer of games to be played + self.round = 0 # number of rounds played so far + self.users_choice = String() + self.computers_choice = String() + self.round_complete = True + self.user_wins = 0 + self.computer_wins = 0 + + # publisher to send user's choice over the user topic + self.pub = rospy.Publisher('user_choice', String, queue_size=1) + + # timer to request user's input, should be called once per second, but will stall + # on user input + rospy.Timer(rospy.Duration(1), self.callback_users_choice) + + # subscriber to receive the computer's choice over the computer topic + rospy.Subscriber('computer_choice', String, self.callback_computers_choice) + + # method to request user input and publish to computer + # inputs: event, ROS information on timing of interrupt. + # Can be ignored. + # outputs: publishes user's choice + def callback_users_choice(self, event): + # check to ensure we wait for the round to be complete to get user input + if self.round_complete and not self.is_game_complete(): + self.users_choice = input('Please choose between %s by typing either one of the three.\n' + % self.CHOICES) + + # check to ensure it is a valid input + if self.users_choice in self.CHOICES: + self.round_complete = False + self.pub.publish(self.users_choice) + else: + print('Invalid input. Please choose between %s by typing either one of the three.\n' + % self.CHOICES) + + # method to receive computer's choice and determine winner + # inputs: choice, computer's choice sent over the computer + # topic + # outputs: none + def callback_computers_choice(self, choice): + self.computers_choice = choice.data + self.round += 1 + print('Game %d: %s vs. %s' % (self.round, self.users_choice, self.computers_choice)) + + # Determine winner and increase number of wins for user by 1 + if self.users_choice == self.computers_choice: + print('Draw') + elif self.users_choice == 'Rock' and self.computers_choice == 'Scissors': + print('You win!') + self.user_wins += 1 + elif self.users_choice == 'Paper' and self.computers_choice == 'Rock': + print('You win!') + self.user_wins += 1 + elif self.users_choice == 'Scissors' and self.computers_choice == 'Paper': + print('You win!') + self.user_wins += 1 + else: + print('You lose.') + self.computer_wins += 1 + + self.round_complete = True + + # returns the number of wins by the user and computer + def get_results(self): + return self.user_wins, self.computer_wins + + # checks if the game is complete + def is_game_complete(self): + if self.num_rounds == self.round and self.round_complete: + return True + else: + return False +``` + +### Create the main + + +```python +def main(): + # initialize a node called player + rospy.init_node('user') + + print("***Make sure your computer player is running.***") + + num_rounds = int(input('How many rounds do you want to play? ')) + + # create an instance of the class that will play the specified number of games + u = User(num_rounds) + + # run continuously until the game is complete + while not u.is_game_complete(): + pass + + # print results + user_wins, computer_wins = u.get_results() + print('Total number of user wins: %s' % (user_wins)) + print('Total number of computer wins: %s' % (computer_wins)) + print('Total number of draws: %s' % (num_rounds - (user_wins + computer_wins))) + print('User win percentage: %s' % (user_wins/num_rounds)) +``` + +In an actual Python script we will replace +```python +def main() +``` +with +```python +if __name__ == "__main__": +``` +This allows our python files to be imported into other python files that might also have a main() function. + +### Create the computer player +We must build and run the computer player before we can continue. Open the [RPSComputer](RPSComputer.ipynb) notebook and follow the instructions. + +### Run the program + + +```python +main() +``` + +### ROS refresher + +Open the [ROS](ROS.md) notebook and follow the instructions. diff --git a/_sources/Module3_Python3/ROS.md b/_sources/Module3_Python3/ROS.md old mode 100755 new mode 100644 index 548ff5f..0626a6e --- a/_sources/Module3_Python3/ROS.md +++ b/_sources/Module3_Python3/ROS.md @@ -1,110 +1,110 @@ -# ICE3: ROS - -## ROS -Below you will see some ROS commands. The "!" character in the front allows us to run bash commands from Jupyter and would **NOT** be used in the command line. - -With the user and computer nodes running execute the below commands. - -List the active topics: - - -```bash -rostopic list -``` - -Display running nodes and communication between them: - - -```bash -rqt_graph -``` - -Exit the rqt_graph and then list all running nodes: - -```bash -rosnode list -``` - -Show information about a the **/computer_choice** topic such as what type of messages are sent over the topic and publishing and subscribing nodes: - -```bash -rostopic info /computer_choice -``` - -Display information about the message that is sent over the **/computer** topic: - -```bash -rostopic type /computer_choice | rosmsg show -``` - -Display messages sent over the **/user_choice** topic: - -```bash -rostopic echo /user_choice -``` - -In the Module3_Python3 notebook, reset the Jupter kernel and clear output: At the top menu bar select "Kernel" and "Restart & Clear Output" (you do not need to restart the computer player). Make sure to rerun the code blocks to import modules, build the class, and create the main. When you select 'Rock', 'Paper', or 'Scissors' you should see the message echoed above. - -## Cleanup -In each of the notebooks reset the Jupter kernel and clear output. Close each notebook. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `Jupter-notebook` in. Select 'y'. Kill all processes in your terminals (ctrl+c) and ensure roscore is terminated before moving on to the ICE. - -You should now have a better understanding of how to utilize ROS and Python. To learn more about the Python style guide and standards, visit [PEP 8 -- Style Guid for Python Code](https://www.python.org/dev/peps/pep-0008/#class-names). - - -## Deliverables - -Below you will run ROS commands. The "!" character in the front allows us to run bash commands from Jupyter and would **NOT** be used in the command line. - -With the client and server nodes running execute the below commands. - -List all running nodes: - - -```python - -``` - -List the active topics: - - -```python - -``` - -Display running nodes and communication between them: - - -```python - -``` - -Exit the rqt_graph. - -Show information about a the **/client** topic such as what type of messages are sent over the topic and publishing and subscribing nodes. - - -```python - -``` - -Display information about the message that is sent over the **/client** topic. - - -```python - -``` - -Display messages sent over the **/client** topic: - - -```python - -``` - -In the ICE3_Client notebook when you send a message you should see the message echoed above. - -## Checkpoint -Once complete, take the necessary screen shots of all the items above functioning. Upload those screenshots to your repo in a Module03 folder within the master folder of your repo. - -## Cleanup -In each of the notebooks reset the Jupter kernel and clear output. Close each notebook. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. +# ICE3: ROS + +## ROS +Below you will see some ROS commands. The "!" character in the front allows us to run bash commands from Jupyter and would **NOT** be used in the command line. + +With the user and computer nodes running execute the below commands. + +List the active topics: + + +```bash +rostopic list +``` + +Display running nodes and communication between them: + + +```bash +rqt_graph +``` + +Exit the rqt_graph and then list all running nodes: + +```bash +rosnode list +``` + +Show information about a the **/computer_choice** topic such as what type of messages are sent over the topic and publishing and subscribing nodes: + +```bash +rostopic info /computer_choice +``` + +Display information about the message that is sent over the **/computer** topic: + +```bash +rostopic type /computer_choice | rosmsg show +``` + +Display messages sent over the **/user_choice** topic: + +```bash +rostopic echo /user_choice +``` + +In the Module3_Python3 notebook, reset the Jupter kernel and clear output: At the top menu bar select "Kernel" and "Restart & Clear Output" (you do not need to restart the computer player). Make sure to rerun the code blocks to import modules, build the class, and create the main. When you select 'Rock', 'Paper', or 'Scissors' you should see the message echoed above. + +## Cleanup +In each of the notebooks reset the Jupter kernel and clear output. Close each notebook. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `Jupter-notebook` in. Select 'y'. Kill all processes in your terminals (ctrl+c) and ensure roscore is terminated before moving on to the ICE. + +You should now have a better understanding of how to utilize ROS and Python. To learn more about the Python style guide and standards, visit [PEP 8 -- Style Guid for Python Code](https://www.python.org/dev/peps/pep-0008/#class-names). + + +## Deliverables + +Below you will run ROS commands. The "!" character in the front allows us to run bash commands from Jupyter and would **NOT** be used in the command line. + +With the client and server nodes running execute the below commands. + +List all running nodes: + + +```python + +``` + +List the active topics: + + +```python + +``` + +Display running nodes and communication between them: + + +```python + +``` + +Exit the rqt_graph. + +Show information about a the **/client** topic such as what type of messages are sent over the topic and publishing and subscribing nodes. + + +```python + +``` + +Display information about the message that is sent over the **/client** topic. + + +```python + +``` + +Display messages sent over the **/client** topic: + + +```python + +``` + +In the ICE3_Client notebook when you send a message you should see the message echoed above. + +## Checkpoint +Once complete, take the necessary screen shots of all the items above functioning. Upload those screenshots to your repo in a Module03 folder within the master folder of your repo. + +## Cleanup +In each of the notebooks reset the Jupter kernel and clear output. Close each notebook. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. diff --git a/_sources/Module3_Python3/RPSComputer.md b/_sources/Module3_Python3/RPSComputer.md old mode 100755 new mode 100644 index 300055e..31c7257 --- a/_sources/Module3_Python3/RPSComputer.md +++ b/_sources/Module3_Python3/RPSComputer.md @@ -1,51 +1,51 @@ -# Module 3: Python3 for Robotics - -## A note on this document -This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. - -## Computer Player -We will now implement a computer player node that will send a random choice to the player node. - -First import your required modules: - - -```python -import rospy, random -from std_msgs.msg import String -``` - -Here, we are importing two modules: *rospy*, which allows us to run ROS code in Python, and the *String* message from the *std_msgs* ROS package. - - -```python -class Computer: - # class constant to store computer choices - CHOICES = ['Rock', 'Paper', 'Scissors'] - - # initialize class - def __init__(self,): - # instance variables - self.computers_choice = String() - - # subscriber to receive the computer's choice over the computer topic - rospy.Subscriber('user_choice', String, self.callback_computers_choice) - - # publisher to send player's choice over the player topic - self.pub = rospy.Publisher('computer_choice', String, queue_size=1) - - def callback_computers_choice(self, data): - # use random module imported earlier to select a random index from the list - rand_index = random.randint(0,len(self.CHOICES)-1) - # use the index to select an item from the list - self.computers_choice = self.CHOICES[rand_index] - self.pub.publish(self.computers_choice) -``` - - -```python -rospy.init_node('computer') -Computer() -rospy.spin() -``` - -Now return to the Module3_Python3 User Notebook and fill in the requested inputs. +# Module 3: Python3 for Robotics + +## A note on this document +This document is known as a Jupyter Notebook; it is used in academia and industry to allow text and executable code to coexist in a very easy to read format. Blocks can contain text or code, and for blocks containing code, press `Shift + Enter` to run the code. Earlier blocks of code need to be run for the later blocks of code to work. + +## Computer Player +We will now implement a computer player node that will send a random choice to the player node. + +First import your required modules: + + +```python +import rospy, random +from std_msgs.msg import String +``` + +Here, we are importing two modules: *rospy*, which allows us to run ROS code in Python, and the *String* message from the *std_msgs* ROS package. + + +```python +class Computer: + # class constant to store computer choices + CHOICES = ['Rock', 'Paper', 'Scissors'] + + # initialize class + def __init__(self,): + # instance variables + self.computers_choice = String() + + # subscriber to receive the computer's choice over the computer topic + rospy.Subscriber('user_choice', String, self.callback_computers_choice) + + # publisher to send player's choice over the player topic + self.pub = rospy.Publisher('computer_choice', String, queue_size=1) + + def callback_computers_choice(self, data): + # use random module imported earlier to select a random index from the list + rand_index = random.randint(0,len(self.CHOICES)-1) + # use the index to select an item from the list + self.computers_choice = self.CHOICES[rand_index] + self.pub.publish(self.computers_choice) +``` + + +```python +rospy.init_node('computer') +Computer() +rospy.spin() +``` + +Now return to the Module3_Python3 User Notebook and fill in the requested inputs. diff --git a/_sources/Module4_DrivingTheRobot/DrivingTheRobot.md b/_sources/Module4_DrivingTheRobot/DrivingTheRobot.md old mode 100755 new mode 100644 index f05015c..a3282f2 --- a/_sources/Module4_DrivingTheRobot/DrivingTheRobot.md +++ b/_sources/Module4_DrivingTheRobot/DrivingTheRobot.md @@ -1,80 +1,80 @@ -# Driving the Robot - - -## Lesson Objectives: -1. Gain additional familiarity with simulation environment -1. Gain familiarity with Turtlebot 3 robotics platform -1. Practice with ROS diagnostic tools - -## Agenda: -1. Use Linux terminals to launch and control Turtlebot 3 in simulation environment. -1. Use Linux terminals to launch and control actual Turtlebot 3. - - -## Gain Additional Familiarity with Simulation Environment. -At this point we are nearly 20% complete with the course, and we have the foundational knowledge required -about the ecosystem we will be working in. In short we will be using the Linux operating system to host -ROS. We will use ROS to execute python code to control and interact with the various subsystems on our -robotics platform. - -Before we move on to controlling our actual robot, we want to stress the importance and capabilities of our simulation environment. There will be times, where it may be inconvenient to work with the actual robot. Additionally, with all hardware systems, there can occasionally be inconsistent behaviors. For both of these reasons (and many others), having an effective simulator may be very useful. -There are two tools that we will find very useful for simulation (RViz and Gazebo). - -- RViz - (short for “ROS visualization”) is a 3D visualization software tool for robots, sensors, and -algorithms. It enables you to see the robot’s perception of its world (real or simulated). -- Gazebo - Gazebo is a 3D robot simulator. Its objective is to simulate a robot, giving you a close -substitute to how your robot would behave in a real-world physical environment. It can compute the -impact of forces (such as gravity). - -The difference between the two can be summed up in the following excerpt from Morgan Quigley (one of -the original developers of ROS) in his book Programming Robots with ROS: - -``` -RViz shows you what the robot thinks is happening, while Gazebo shows you what is really happening. -``` - -For future research or development efforts, you may need to build your own simulation environment. However, -for this course, the developers of the Turtlebot3, Robotis, have already done this for us. - - -- In a free terminal window, bring up roscore -- In a second free terminal window, we are going to use the roslaunch command to bring up the Gazebo -environment within a maze. Tip: There are numerous virtual environments, but this will work for -now -```bash -dfec@master:∼$ roslaunch turtlebot3_gazebo turtlebot3_world.launch -``` -- In a third terminal window, we are now going to bring up the RViz environment. -```bash -dfec@master:∼$ roslaunch turtlebot3_gazebo turtlebot3_gazebo_rviz.launch -``` -- In a fourth terminal window, we are going to bring up a valuable tool that will allow us to control the -robot via the keyboard. This tool is called teleop_twist. -```bash -dfec@master:∼$ rosrun teleop_twist_keyboard teleop_twist_keyboard.py -``` - -```{tip} -I strongly recommend that you commit the above sequence of commands to memory, or at a minimum -have them in a place that you can quickly recall them. There is nothing until Module 9 that absolutely -requires the real robot, as everything else can be simulated. -``` - -## Gain Familiarity with Turtlebot3 Robotics Platform. -The Module04 Jupyter Notebook will guide you through the process of connecting to and activating your -robot for the first time. - -1. On the master, open the Jupyter Notebook server (if it is not already open): -```bash -dfec@master:∼$ roscd ece387_curriculum/Module04_DrivingTheRobot -dfec@master:∼$ jupyter−lab -``` - -2. Open [ICE4: Driving the Robot](ICE4_DrivingTheRobot.md) and follow the instructions. - -## Assignments. -- Complete ICE4 if not accomplished during class. -- Push screen captures into your repo/master/module04 on github - -## Next time. +# Driving the Robot + + +## Lesson Objectives: +1. Gain additional familiarity with simulation environment +1. Gain familiarity with Turtlebot 3 robotics platform +1. Practice with ROS diagnostic tools + +## Agenda: +1. Use Linux terminals to launch and control Turtlebot 3 in simulation environment. +1. Use Linux terminals to launch and control actual Turtlebot 3. + + +## Gain Additional Familiarity with Simulation Environment. +At this point we are nearly 20% complete with the course, and we have the foundational knowledge required +about the ecosystem we will be working in. In short we will be using the Linux operating system to host +ROS. We will use ROS to execute python code to control and interact with the various subsystems on our +robotics platform. + +Before we move on to controlling our actual robot, we want to stress the importance and capabilities of our simulation environment. There will be times, where it may be inconvenient to work with the actual robot. Additionally, with all hardware systems, there can occasionally be inconsistent behaviors. For both of these reasons (and many others), having an effective simulator may be very useful. +There are two tools that we will find very useful for simulation (RViz and Gazebo). + +- RViz - (short for “ROS visualization”) is a 3D visualization software tool for robots, sensors, and +algorithms. It enables you to see the robot’s perception of its world (real or simulated). +- Gazebo - Gazebo is a 3D robot simulator. Its objective is to simulate a robot, giving you a close +substitute to how your robot would behave in a real-world physical environment. It can compute the +impact of forces (such as gravity). + +The difference between the two can be summed up in the following excerpt from Morgan Quigley (one of +the original developers of ROS) in his book Programming Robots with ROS: + +``` +RViz shows you what the robot thinks is happening, while Gazebo shows you what is really happening. +``` + +For future research or development efforts, you may need to build your own simulation environment. However, +for this course, the developers of the Turtlebot3, Robotis, have already done this for us. + + +- In a free terminal window, bring up roscore +- In a second free terminal window, we are going to use the roslaunch command to bring up the Gazebo +environment within a maze. Tip: There are numerous virtual environments, but this will work for +now +```bash +dfec@master:∼$ roslaunch turtlebot3_gazebo turtlebot3_world.launch +``` +- In a third terminal window, we are now going to bring up the RViz environment. +```bash +dfec@master:∼$ roslaunch turtlebot3_gazebo turtlebot3_gazebo_rviz.launch +``` +- In a fourth terminal window, we are going to bring up a valuable tool that will allow us to control the +robot via the keyboard. This tool is called teleop_twist. +```bash +dfec@master:∼$ rosrun teleop_twist_keyboard teleop_twist_keyboard.py +``` + +```{tip} +I strongly recommend that you commit the above sequence of commands to memory, or at a minimum +have them in a place that you can quickly recall them. There is nothing until Module 9 that absolutely +requires the real robot, as everything else can be simulated. +``` + +## Gain Familiarity with Turtlebot3 Robotics Platform. +The Module04 Jupyter Notebook will guide you through the process of connecting to and activating your +robot for the first time. + +1. On the master, open the Jupyter Notebook server (if it is not already open): +```bash +dfec@master:∼$ roscd ece387_curriculum/Module04_DrivingTheRobot +dfec@master:∼$ jupyter−lab +``` + +2. Open [ICE4: Driving the Robot](ICE4_DrivingTheRobot.md) and follow the instructions. + +## Assignments. +- Complete ICE4 if not accomplished during class. +- Push screen captures into your repo/master/module04 on github + +## Next time. - Lesson 10: Module 5 - Custom Messages \ No newline at end of file diff --git a/_sources/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.md b/_sources/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.md old mode 100755 new mode 100644 index c3dd1cc..19e38fa --- a/_sources/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.md +++ b/_sources/Module4_DrivingTheRobot/ICE4_DrivingTheRobot.md @@ -1,118 +1,118 @@ -# ICE4: Driving the Robot - -## Purpose -This In-Class Exercise will introduce you to utilizing pre-built ROS packages to accomplish a task. It will also provide you experience interacting with someone else's source code (.py files) to learn how that component works. You will use ROS to run two nodes, **turtlebot3_core** and **teleop_twist_keyboard**, to drive the Turtlebot3 with a keyboard. You will continue to practice using ROS tools to observe how these components communicate. - -## Code used to drive the robot - -1. On the Master, open a terminal and run **roscore**. - -1. Open a new terminal on the Master and create a secure shell into the Turtlebot3 using the SSH command you learned during Module 2. This will allow you to run commands as if you were on the Turtlebot3. - -1. Using the secure shell, open the source code for the **turtlebot3_core** launch using the nano command line editor tool through the rosed command: - - ```bash - $ rosed turtlebot3_bringup turtlebot3_core.launch - ``` - - > ⌨️ **Syntax:** `rosed ` - - ```{note} - You may remember when we set up our *.bashrc* file we set the system variable **EDITOR** to `nano -w`. This enables the `rosed` command to utilize the nano editor. - ``` - - We will learn more about launch files in a few modules, but just understand that a launch file is used to launch one or more ROS nodes. This particular launch file only launches one node, **serial_node.py**. This node will connect to the OpenCR controller on the Turtlebot3 using the port and baud rate parameters. This connection will enable us to send *Twist* messages over the **/cmd_vel** topic to drive the Turtlebot3 using the keyboard. - -1. Close the editor by hitting `ctrl+x`. - -1. It is always a good idea to check that the Turtlebot3 is communicating with the Master. To do this, we can list the active topics the Turtlebot3 sees. Run the following within your secure shell: - - ```bash - $ rostopic list` - ``` - - If all is well, then there should be two topics provided by **roscore** running on the Master: **/rosout** and **/rosout_agg**. We will typically ignore these topics. - -1. Run the **turtlebot3_core.launch** file using the `roslaunch` command: - - ```bash - $ roslaunch turtlebot3_bringup turtlebot3_core.launch` - ``` - - > ⌨️ **Syntax:** `roslaunch ` - - Your Turtlebot3 is now ready to drive and should be listening for *Twist* messages to be sent over the **/cmd_vel** topic. - -## Driving the robot -1. Open a new terminal on the Master and observe the nodes currently running: - - `rosrun rqt_graph rqt_graph` - - You should only see one node running right now, **turtlebot3_core**, with no connections. - -1. Open a new terminal tab and list the active topics. There should be one active topic other than the ones created by **roscore**: **/cmd_vel**. - -1. We used the **/cmd_vel** topic when driving the simulated Turtlebot3, but let's refresh our memory about the topic: - - `rostopic info cmd_vel` - - As you can see the **/cmd_vel** topic is currently subscribed to by the **turtlebot3_core** with no publishers (just as we would expect after seeing the rqt_graph). We also note that topic utilizes the *Twist* message type. The following will show information about the fields within the *Twist* message sent over the **/cmd_vel** topic: - - `rostopic type cmd_vel | rosmsg show` - -1. You can find information about pre-built packages by googling the package name along with the ROS distribution. Open up your favorite browser and google "teleop twist keyboard noetic". The first result should be from the ROS wiki page. - -1. Ensure the ROS package **teleop_twist_keyboard** is installed on your Master: - - `rospack find teleop_twist_keyboard` - - If installed, the command should return the absolute path to the package, similar to `/opt/ros/noetic/share/teleop_twist_keyboard` - - If the command instead returns an error, then you need to install the package using apt: - - `sudo apt install ros-noetic-teleop-twist-keyboard` - - ```{tip} - All packages built for Noetic can be downloaded in the above manner (ros-noetic-desired-pkg with underscores in the package name replaced by dashes). Some packages were only built for previous ROS distribution and will have to be built from source (we will demonstrate this at a future time). - ``` - -1. Run the **teleop_twist_keyboard** node on the Master: - - ```{tip} - Don't forget your tab completion! You can start typing a package name or node and then hit tab for it to complete the command for you! - ``` - - `rosrun teleop_twist_keyboard teleop_twist_keyboard.py` - -1. Before we get too excited and drive the Turtlebot3 off a cliff, observe how the nodes communicate using the **rqt_graph** tool in a new terminal (if you still have the previous rqt_graph running, you can hit the refresh button in the top left corner). - -1. Select the terminal that has the **teleop_twist_keyboard** node running and observe the instructions for sending *Twist* messages. These are the same as when driving the simulated Turtlebot3. - -1. The Turtlebot3 operates best with a linear velocity between 0.2 m/s and 0.5 m/s. It turns best with an angular velocity between 0.5 rad/s and 1.5 rad/s. Drive the Turtlebot3 using these parameters. - -## ROS - -In labs throughout this course we will request information about the topics, nodes, and messages within your system. Accomplish the following in a new terminal on your Master (you can ignore all nodes/topics that result from **roscore**). - -1. List all running nodes. - -1. Determine what topics the nodes subscribe and publish to (repeat for each node). - -1. Display running nodes and communication between them. - -1. List the active topics. - -1. Determine the type of messages sent over the topics (repeat for each topic). - -1. Determine the fields of the messages. - -1. Observe the information sent over a topic (repeat for each topic). - -## Checkpoint -Once complete, push screenshots showing the output of each of the above to your student repo on github in a /master/module04 folder. - -## Summary -In this exercise you examined and used pre-built packages and source code to drive the Turtlebot3 and understand how the system worked. You then were able to analyze the topics, nodes, and messages within the ROS system to better understand the flow of information and control. The **pro-tips** presented throughout this exercise will make you a better user of Linux and ROS. - -## Cleanup +# ICE4: Driving the Robot + +## Purpose +This In-Class Exercise will introduce you to utilizing pre-built ROS packages to accomplish a task. It will also provide you experience interacting with someone else's source code (.py files) to learn how that component works. You will use ROS to run two nodes, **turtlebot3_core** and **teleop_twist_keyboard**, to drive the Turtlebot3 with a keyboard. You will continue to practice using ROS tools to observe how these components communicate. + +## Code used to drive the robot + +1. On the Master, open a terminal and run **roscore**. + +1. Open a new terminal on the Master and create a secure shell into the Turtlebot3 using the SSH command you learned during Module 2. This will allow you to run commands as if you were on the Turtlebot3. + +1. Using the secure shell, open the source code for the **turtlebot3_core** launch using the nano command line editor tool through the rosed command: + + ```bash + $ rosed turtlebot3_bringup turtlebot3_core.launch + ``` + + > ⌨️ **Syntax:** `rosed ` + + ```{note} + You may remember when we set up our *.bashrc* file we set the system variable **EDITOR** to `nano -w`. This enables the `rosed` command to utilize the nano editor. + ``` + + We will learn more about launch files in a few modules, but just understand that a launch file is used to launch one or more ROS nodes. This particular launch file only launches one node, **serial_node.py**. This node will connect to the OpenCR controller on the Turtlebot3 using the port and baud rate parameters. This connection will enable us to send *Twist* messages over the **/cmd_vel** topic to drive the Turtlebot3 using the keyboard. + +1. Close the editor by hitting `ctrl+x`. + +1. It is always a good idea to check that the Turtlebot3 is communicating with the Master. To do this, we can list the active topics the Turtlebot3 sees. Run the following within your secure shell: + + ```bash + $ rostopic list` + ``` + + If all is well, then there should be two topics provided by **roscore** running on the Master: **/rosout** and **/rosout_agg**. We will typically ignore these topics. + +1. Run the **turtlebot3_core.launch** file using the `roslaunch` command: + + ```bash + $ roslaunch turtlebot3_bringup turtlebot3_core.launch` + ``` + + > ⌨️ **Syntax:** `roslaunch ` + + Your Turtlebot3 is now ready to drive and should be listening for *Twist* messages to be sent over the **/cmd_vel** topic. + +## Driving the robot +1. Open a new terminal on the Master and observe the nodes currently running: + + `rosrun rqt_graph rqt_graph` + + You should only see one node running right now, **turtlebot3_core**, with no connections. + +1. Open a new terminal tab and list the active topics. There should be one active topic other than the ones created by **roscore**: **/cmd_vel**. + +1. We used the **/cmd_vel** topic when driving the simulated Turtlebot3, but let's refresh our memory about the topic: + + `rostopic info cmd_vel` + + As you can see the **/cmd_vel** topic is currently subscribed to by the **turtlebot3_core** with no publishers (just as we would expect after seeing the rqt_graph). We also note that topic utilizes the *Twist* message type. The following will show information about the fields within the *Twist* message sent over the **/cmd_vel** topic: + + `rostopic type cmd_vel | rosmsg show` + +1. You can find information about pre-built packages by googling the package name along with the ROS distribution. Open up your favorite browser and google "teleop twist keyboard noetic". The first result should be from the ROS wiki page. + +1. Ensure the ROS package **teleop_twist_keyboard** is installed on your Master: + + `rospack find teleop_twist_keyboard` + + If installed, the command should return the absolute path to the package, similar to `/opt/ros/noetic/share/teleop_twist_keyboard` + + If the command instead returns an error, then you need to install the package using apt: + + `sudo apt install ros-noetic-teleop-twist-keyboard` + + ```{tip} + All packages built for Noetic can be downloaded in the above manner (ros-noetic-desired-pkg with underscores in the package name replaced by dashes). Some packages were only built for previous ROS distribution and will have to be built from source (we will demonstrate this at a future time). + ``` + +1. Run the **teleop_twist_keyboard** node on the Master: + + ```{tip} + Don't forget your tab completion! You can start typing a package name or node and then hit tab for it to complete the command for you! + ``` + + `rosrun teleop_twist_keyboard teleop_twist_keyboard.py` + +1. Before we get too excited and drive the Turtlebot3 off a cliff, observe how the nodes communicate using the **rqt_graph** tool in a new terminal (if you still have the previous rqt_graph running, you can hit the refresh button in the top left corner). + +1. Select the terminal that has the **teleop_twist_keyboard** node running and observe the instructions for sending *Twist* messages. These are the same as when driving the simulated Turtlebot3. + +1. The Turtlebot3 operates best with a linear velocity between 0.2 m/s and 0.5 m/s. It turns best with an angular velocity between 0.5 rad/s and 1.5 rad/s. Drive the Turtlebot3 using these parameters. + +## ROS + +In labs throughout this course we will request information about the topics, nodes, and messages within your system. Accomplish the following in a new terminal on your Master (you can ignore all nodes/topics that result from **roscore**). + +1. List all running nodes. + +1. Determine what topics the nodes subscribe and publish to (repeat for each node). + +1. Display running nodes and communication between them. + +1. List the active topics. + +1. Determine the type of messages sent over the topics (repeat for each topic). + +1. Determine the fields of the messages. + +1. Observe the information sent over a topic (repeat for each topic). + +## Checkpoint +Once complete, push screenshots showing the output of each of the above to your student repo on github in a /master/module04 folder. + +## Summary +In this exercise you examined and used pre-built packages and source code to drive the Turtlebot3 and understand how the system worked. You then were able to analyze the topics, nodes, and messages within the ROS system to better understand the flow of information and control. The **pro-tips** presented throughout this exercise will make you a better user of Linux and ROS. + +## Cleanup In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. \ No newline at end of file diff --git a/_sources/Module5_CustomMessages/CustomMessages.md b/_sources/Module5_CustomMessages/CustomMessages.md old mode 100755 new mode 100644 index f1d9816..37bad9b --- a/_sources/Module5_CustomMessages/CustomMessages.md +++ b/_sources/Module5_CustomMessages/CustomMessages.md @@ -1,366 +1,366 @@ -# Custom Messages - -## Purpose -This In-Class Exercise will provide you more insight into ROS messages and how information is passed between two nodes. A node can publish specific messages over a topic and other nodes are able to subscribe to that topic to receive the message. The format of these messages must be pre-defined and each node needs to know the format of the message. ROS provides a number of pre-built messages, but also allows for developers to create custom messages. In this lesson you will learn the method for and practice creating custom messages. In the corresponding lab you will develop a custom message to drive the robot. The custom message will eventually be used to enable a controller to drive the robot based on multiple data sources (e.g., IMU, LIDAR, keyboard). - -## ROS msgs -### msg -ROS utilizes a simplified message description language to describe data values that ROS nodes publish. There are a lot of built-in messages that are widely used by ROS packages ([common_msgs](http://wiki.ros.org/common_msgs?distro=noetic)). The *geometry_msgs* package is one example of a pre-built message which provides the *Twist* message type used to drive the robot in the previous ICE. - -### Custom messages -If a pre-built message type does not meet the needs of a system, custom messages can be created. A custom message is created using a `.msg` file, a simple text file that descries the fields of a ROS message. The *catkin_make* tool uses the message file to generate source code for messages. The `.msg` files are stored in the **msg** directory of a package. There are two parts to a `.msg` file: fields and constants. Fields are the data that is sent inside of the message. Constants define useful values that can be used to interpret those fields. We will primarily use fields. - -### Fields -Each field consists of a type and a name, separated by a space per line. The field types you can use are: - -- int8, int16, int64 (plus uint*) -- float32, float64 -- string -- time, duration -- other msg files -- variable length array[] and fixed-length array[X]. - -### Header -The header contains a timestamp and coordinate frame information that are commonly used in ROS. You will frequently see the first line in a `.msg` file have a header. - -### Format - Header header - fieldtype fieldname - -For example, if we created a custom message file titled `Person.msg` that describes a person it might look like this: - - Header header - string firstname - string lastname - int32 age - -### Importing messages -To utilize a *msg* in a node it must first be imported into the script. - -```python - from geometry_msgs.msg import Twist - from ice5.msg import Person -``` -> ⌨️ **Syntax:** `from .msg import msg` - -### Using messages -After importing the *msg* you can access the fields similar to any object. For example, if we created an instance variable to store our Person message: - -```python -person = Person() -``` -You would then access the fields using the field names of the *msg*: - -```python -print("%s %s is %d years old!" % (person.firstname, person.lastname, person.age)) -``` - -If you wanted to set the linear x and angular z speeds of a *Twist* message before publishing it to the TurtleBot3 you would again use the field names provided by the pre-built message. If you googled the Twist message ([Twist Message](http://docs.ros.org/en/lunar/api/geometry_msgs/html/msg/Twist.html), you would see the contents of the Twist message include two other messages, linear and angular, of type Vector3: - - geometry_msgs/Vector3 linear - geometry_msgs/Vector3 angular - -Each of those two messages include the same fields and fieldnames: - - float64 x - float64 y - float64 z - -To set the linear x and angular z values, we have to access those fields using an objected oriented method. For example: - -```python - from gemoetry_msgs.msg import Twist - - # create a publisher to send Twist messages over the cmd_vel topic - pub = rospy.Publisher('cmd_vel', Twist, queue_size = 1) - - # set the Twist message to drive the robot straight at 0.25 m/s - bot_cmd = Twist() - bot_cmd.linear.x = 0.25 # notice you have to access both the "linear" & "x" fields of the Twist message - bot_cmd.angular.z = 0.0 - - # publish the Twist message - pub.publish(bot_cmd) -``` - -## In-Class Exercise 5 -In this exercise you will create a custom message that describes a person. This message will provide two strings, first and last name, and an integer age for a person. We will then create a node that publishes information about that person and a node that subscribes to that information. - -### Create the custom message: -1. In a new terminal on the **Master**, create an **ice5** package which depends on the *std_msgs* package and *rospy* package, compile and source the ws: - - ```bash - cd ~/master_ws/src/ece387_master_spring2024-USERNAME/master - catkin_create_pkg ice5 std_msgs rospy - cd ~/master_ws - catkin_make - source ~/.bashrc - ``` -1. Change directory to the package folder and create a *msg* directory: - - ```bash - roscd ice5 - mkdir msg - cd msg - ``` - -1. Create the *msg* file for the Person and add the fields previously discussed (header, firstname, lastname, and age): - - ```bash - nano Person.msg - ``` - -1. Save and exit: `ctrl+s`, `ctrl+x` - -### Write the Publisher -1. Create the file for the publisher: - - ```bash - roscd ice5/src - touch ice5_publisher.py - ``` - -1. Copy the below code to the `ice5_publisher.py` file and fill in the required lines (look for the TODO tag). You can edit via the terminal using nano, but it is often easier to use a GUI editor. Browse to the publisher in the file browser and double-click. This will open the file in thonny (if it is open in any other editor, stop, raise your hand, and get help from an instructor) - -```python -#!/usr/bin/env python3 -import rospy -from ice5.msg import Person # import the message: from PKG.msg import MSG - -class Talker: - """Class that publishes basic information about person""" - def __init__(self, first = "Cadet", last = "Snuffy", age = 21): - self.msg = Person() # creates a Person message - self.msg.firstname = first # assign the firstname field - self.msg.lastname = last # assign the lastname field - self.msg.age = age # assign the age field - - # TODO: create the publisher that publishes Person messages over the person topic - # Since we don't care about losing messages we can set the queue size to 1 - self.pub = - - # TODO: create a timer that will call the callback_publish() function every .1 seconds (10 Hz) - rospy.Timer() - - # nicely handle shutdown (ctrl+c) - self.ctrl_c = False - rospy.on_shutdown(self.shutdownhook) - - def callback_publish(self, event): - if not self.ctrl_c: - # TODO: publish the msg - - - def shutdownhook(self): - print("Shutting down publisher.") - self.ctrl_c = True - -if __name__ == '__main__': - rospy.init_node('talker') - - # create an instance of the Talker class changing the class variables - Talker("Steven", "Beyer", 33) - rospy.spin() # run forever until ctrl+c -``` - -3. Save and exit. - -4. Make the node executable. - -### Write the Subscriber -1. Create the file for the subscriber: - - ```bash - touch ice5_subscriber.py - ``` - -1. Copy the below code to the `ice5_subscriber.py` file and fill in the required lines (look for the TODO tag). - -```python -#!/usr/bin/env python3 -import rospy -from ice5.msg import Person # import the message: from PKG.msg import MSG - -class Listener: - """Listener class that prints information about person""" - def __init__(self): - # TODO: create the subscriber that receives Person messages over the person topic - # and calls the callback_person() function. - - - # nicely handle shutdown (Ctrl+c) - rospy.on_shutdown(self.shutdownhook) - - def callback_person(self, person): - # TODO: print the information about the person - - - def shutdownhook(self): - print("Shutting down subscriber.") - -if __name__ == '__main__': - rospy.init_node('listener') - # create an instance of the class - Listener() - # keeps python from exiting until this node is stopped - rospy.spin() -``` - -3. Save and exit. - -4. Make the node executable. - -### Requirements to use custom messages. -There are a number of settings that have to be set within the `package.xml` and `CMakeLists.txt` files that tell catkin to compile the messages. - -#### package.xml -1. Edit `package.xml` (`rosed ice5 package.xml`) and uncomment these two lines (remove arrows on both sides of the line): - - ``` - message_generation - message_runtime - ``` - -1. Save and exit. - -#### CMakeLists.txt -1. Edit `CMakeLists.txt` (`rosed ice5 CMakeLists.txt`) and make the following changes: - - 1. Add the `message_generation` dependency to the `find_package` call so that you can generate messages: - - ```python - # Do not just add this to your CMakeLists.txt, modify the existing text to - # add message_generation before the closing parenthesis - find_package(catkin REQUIRED COMPONENTS - rospy - std_msgs - message_generation - ) - ``` - - 1. Find the following block of code: - - ```python - ## Generate messages in the 'msg' folder - # add_message_files( - # FILES - # Message1.msg - # Message2.msg - #) - ``` - - and uncomment by removing the `#` symbols and then replace the `Message*.msg` files with your `.msg` file, such that it looks like this: - - ```python - add_message_files( - FILES - Person.msg - ) - ``` - - 1. Find the following block of code: - - ```python - # generate_messages( - # DEPENDENCIES - # std_msgs - #) - ``` - - and uncomment so it looks like: - - ```python - generate_messages( - DEPENDENCIES - std_msgs - ) - ``` - - 1. Uncomment and add the `message_runtime` dependency to the `CATKIN_DEPENDS` line in the `catkin_package()` function near the bottom without changing anything else: - - ```python - catkin_package( - ... - CATKIN_DEPENDS rospy std_msgs message_runtime - ... - ) - - 1. Save and exit. - -### Compile and run the code -1. Make and source your package: - - ```bash - cd ~/master_ws - catkin_make - source ~/.bashrc - ``` - -1. Run roscore! - -1. The `rospy` tool can measure certain statistics for every topic connection. We can visualize this in `rqt_graph`, but we have to enable it after `roscore`, but before any nodes. In a new terminal run the following to enable statistics: - - ```bash - rosparam set enable_statistics true - ``` - -1. Run the publisher: - - ```bash - rosrun ice5 ice5_publisher.py - ``` - -1. In a new terminal, run the subscriber: - - ```bash - rosrun ice5 ice5_subscriber.py - ``` - -1. In a new terminal observe the nodes running: - - ```bash - rosnode list - ``` - -1. Observe information about each node: - - ```bash - rosnode info /talker - ``` - -1. Observe how information is being passed: - - ```bash - rosrun rqt_graph rqt_graph - ``` - - > 📝️ **Note:** You may have to hit refresh a few times to get the statistics previously mentioned. - -1. Observe all active topics: - - ```bash - rostopic list - ``` - -1. Observe the information about the message sent over the topics (repeat for each topic, remember we do not care about topics we did not create (e.g., rosout, rosout_agg, statistics)). - - ```bash - rostopic info person - ``` - -1. Observe the fields of the message sent over each topic: - - ```bash - rostopic type person | rosmsg show - ``` - -## Checkpoint -Once complete, get checked off by an instructor showing the output of each of the above. - -## Summary -In this lesson you learned about ROS pre-built and custom messages! You created your own message which described a Person. You then developed a publisher to send information about the Person to a subscriber. - -## Cleanup -In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. - -**Ensure roscore is terminated before moving on to the lab.** +# Custom Messages + +## Purpose +This In-Class Exercise will provide you more insight into ROS messages and how information is passed between two nodes. A node can publish specific messages over a topic and other nodes are able to subscribe to that topic to receive the message. The format of these messages must be pre-defined and each node needs to know the format of the message. ROS provides a number of pre-built messages, but also allows for developers to create custom messages. In this lesson you will learn the method for and practice creating custom messages. In the corresponding lab you will develop a custom message to drive the robot. The custom message will eventually be used to enable a controller to drive the robot based on multiple data sources (e.g., IMU, LIDAR, keyboard). + +## ROS msgs +### msg +ROS utilizes a simplified message description language to describe data values that ROS nodes publish. There are a lot of built-in messages that are widely used by ROS packages ([common_msgs](http://wiki.ros.org/common_msgs?distro=noetic)). The *geometry_msgs* package is one example of a pre-built message which provides the *Twist* message type used to drive the robot in the previous ICE. + +### Custom messages +If a pre-built message type does not meet the needs of a system, custom messages can be created. A custom message is created using a `.msg` file, a simple text file that descries the fields of a ROS message. The *catkin_make* tool uses the message file to generate source code for messages. The `.msg` files are stored in the **msg** directory of a package. There are two parts to a `.msg` file: fields and constants. Fields are the data that is sent inside of the message. Constants define useful values that can be used to interpret those fields. We will primarily use fields. + +### Fields +Each field consists of a type and a name, separated by a space per line. The field types you can use are: + +- int8, int16, int64 (plus uint*) +- float32, float64 +- string +- time, duration +- other msg files +- variable length array[] and fixed-length array[X]. + +### Header +The header contains a timestamp and coordinate frame information that are commonly used in ROS. You will frequently see the first line in a `.msg` file have a header. + +### Format + Header header + fieldtype fieldname + +For example, if we created a custom message file titled `Person.msg` that describes a person it might look like this: + + Header header + string firstname + string lastname + int32 age + +### Importing messages +To utilize a *msg* in a node it must first be imported into the script. + +```python + from geometry_msgs.msg import Twist + from ice5.msg import Person +``` +> ⌨️ **Syntax:** `from .msg import msg` + +### Using messages +After importing the *msg* you can access the fields similar to any object. For example, if we created an instance variable to store our Person message: + +```python +person = Person() +``` +You would then access the fields using the field names of the *msg*: + +```python +print("%s %s is %d years old!" % (person.firstname, person.lastname, person.age)) +``` + +If you wanted to set the linear x and angular z speeds of a *Twist* message before publishing it to the TurtleBot3 you would again use the field names provided by the pre-built message. If you googled the Twist message ([Twist Message](http://docs.ros.org/en/lunar/api/geometry_msgs/html/msg/Twist.html), you would see the contents of the Twist message include two other messages, linear and angular, of type Vector3: + + geometry_msgs/Vector3 linear + geometry_msgs/Vector3 angular + +Each of those two messages include the same fields and fieldnames: + + float64 x + float64 y + float64 z + +To set the linear x and angular z values, we have to access those fields using an objected oriented method. For example: + +```python + from gemoetry_msgs.msg import Twist + + # create a publisher to send Twist messages over the cmd_vel topic + pub = rospy.Publisher('cmd_vel', Twist, queue_size = 1) + + # set the Twist message to drive the robot straight at 0.25 m/s + bot_cmd = Twist() + bot_cmd.linear.x = 0.25 # notice you have to access both the "linear" & "x" fields of the Twist message + bot_cmd.angular.z = 0.0 + + # publish the Twist message + pub.publish(bot_cmd) +``` + +## In-Class Exercise 5 +In this exercise you will create a custom message that describes a person. This message will provide two strings, first and last name, and an integer age for a person. We will then create a node that publishes information about that person and a node that subscribes to that information. + +### Create the custom message: +1. In a new terminal on the **Master**, create an **ice5** package which depends on the *std_msgs* package and *rospy* package, compile and source the ws: + + ```bash + cd ~/master_ws/src/ece387_master_spring2024-USERNAME/master + catkin_create_pkg ice5 std_msgs rospy + cd ~/master_ws + catkin_make + source ~/.bashrc + ``` +1. Change directory to the package folder and create a *msg* directory: + + ```bash + roscd ice5 + mkdir msg + cd msg + ``` + +1. Create the *msg* file for the Person and add the fields previously discussed (header, firstname, lastname, and age): + + ```bash + nano Person.msg + ``` + +1. Save and exit: `ctrl+s`, `ctrl+x` + +### Write the Publisher +1. Create the file for the publisher: + + ```bash + roscd ice5/src + touch ice5_publisher.py + ``` + +1. Copy the below code to the `ice5_publisher.py` file and fill in the required lines (look for the TODO tag). You can edit via the terminal using nano, but it is often easier to use a GUI editor. Browse to the publisher in the file browser and double-click. This will open the file in thonny (if it is open in any other editor, stop, raise your hand, and get help from an instructor) + +```python +#!/usr/bin/env python3 +import rospy +from ice5.msg import Person # import the message: from PKG.msg import MSG + +class Talker: + """Class that publishes basic information about person""" + def __init__(self, first = "Cadet", last = "Snuffy", age = 21): + self.msg = Person() # creates a Person message + self.msg.firstname = first # assign the firstname field + self.msg.lastname = last # assign the lastname field + self.msg.age = age # assign the age field + + # TODO: create the publisher that publishes Person messages over the person topic + # Since we don't care about losing messages we can set the queue size to 1 + self.pub = + + # TODO: create a timer that will call the callback_publish() function every .1 seconds (10 Hz) + rospy.Timer() + + # nicely handle shutdown (ctrl+c) + self.ctrl_c = False + rospy.on_shutdown(self.shutdownhook) + + def callback_publish(self, event): + if not self.ctrl_c: + # TODO: publish the msg + + + def shutdownhook(self): + print("Shutting down publisher.") + self.ctrl_c = True + +if __name__ == '__main__': + rospy.init_node('talker') + + # create an instance of the Talker class changing the class variables + Talker("Steven", "Beyer", 33) + rospy.spin() # run forever until ctrl+c +``` + +3. Save and exit. + +4. Make the node executable. + +### Write the Subscriber +1. Create the file for the subscriber: + + ```bash + touch ice5_subscriber.py + ``` + +1. Copy the below code to the `ice5_subscriber.py` file and fill in the required lines (look for the TODO tag). + +```python +#!/usr/bin/env python3 +import rospy +from ice5.msg import Person # import the message: from PKG.msg import MSG + +class Listener: + """Listener class that prints information about person""" + def __init__(self): + # TODO: create the subscriber that receives Person messages over the person topic + # and calls the callback_person() function. + + + # nicely handle shutdown (Ctrl+c) + rospy.on_shutdown(self.shutdownhook) + + def callback_person(self, person): + # TODO: print the information about the person + + + def shutdownhook(self): + print("Shutting down subscriber.") + +if __name__ == '__main__': + rospy.init_node('listener') + # create an instance of the class + Listener() + # keeps python from exiting until this node is stopped + rospy.spin() +``` + +3. Save and exit. + +4. Make the node executable. + +### Requirements to use custom messages. +There are a number of settings that have to be set within the `package.xml` and `CMakeLists.txt` files that tell catkin to compile the messages. + +#### package.xml +1. Edit `package.xml` (`rosed ice5 package.xml`) and uncomment these two lines (remove arrows on both sides of the line): + + ``` + message_generation + message_runtime + ``` + +1. Save and exit. + +#### CMakeLists.txt +1. Edit `CMakeLists.txt` (`rosed ice5 CMakeLists.txt`) and make the following changes: + + 1. Add the `message_generation` dependency to the `find_package` call so that you can generate messages: + + ```python + # Do not just add this to your CMakeLists.txt, modify the existing text to + # add message_generation before the closing parenthesis + find_package(catkin REQUIRED COMPONENTS + rospy + std_msgs + message_generation + ) + ``` + + 1. Find the following block of code: + + ```python + ## Generate messages in the 'msg' folder + # add_message_files( + # FILES + # Message1.msg + # Message2.msg + #) + ``` + + and uncomment by removing the `#` symbols and then replace the `Message*.msg` files with your `.msg` file, such that it looks like this: + + ```python + add_message_files( + FILES + Person.msg + ) + ``` + + 1. Find the following block of code: + + ```python + # generate_messages( + # DEPENDENCIES + # std_msgs + #) + ``` + + and uncomment so it looks like: + + ```python + generate_messages( + DEPENDENCIES + std_msgs + ) + ``` + + 1. Uncomment and add the `message_runtime` dependency to the `CATKIN_DEPENDS` line in the `catkin_package()` function near the bottom without changing anything else: + + ```python + catkin_package( + ... + CATKIN_DEPENDS rospy std_msgs message_runtime + ... + ) + + 1. Save and exit. + +### Compile and run the code +1. Make and source your package: + + ```bash + cd ~/master_ws + catkin_make + source ~/.bashrc + ``` + +1. Run roscore! + +1. The `rospy` tool can measure certain statistics for every topic connection. We can visualize this in `rqt_graph`, but we have to enable it after `roscore`, but before any nodes. In a new terminal run the following to enable statistics: + + ```bash + rosparam set enable_statistics true + ``` + +1. Run the publisher: + + ```bash + rosrun ice5 ice5_publisher.py + ``` + +1. In a new terminal, run the subscriber: + + ```bash + rosrun ice5 ice5_subscriber.py + ``` + +1. In a new terminal observe the nodes running: + + ```bash + rosnode list + ``` + +1. Observe information about each node: + + ```bash + rosnode info /talker + ``` + +1. Observe how information is being passed: + + ```bash + rosrun rqt_graph rqt_graph + ``` + + > 📝️ **Note:** You may have to hit refresh a few times to get the statistics previously mentioned. + +1. Observe all active topics: + + ```bash + rostopic list + ``` + +1. Observe the information about the message sent over the topics (repeat for each topic, remember we do not care about topics we did not create (e.g., rosout, rosout_agg, statistics)). + + ```bash + rostopic info person + ``` + +1. Observe the fields of the message sent over each topic: + + ```bash + rostopic type person | rosmsg show + ``` + +## Checkpoint +Once complete, get checked off by an instructor showing the output of each of the above. + +## Summary +In this lesson you learned about ROS pre-built and custom messages! You created your own message which described a Person. You then developed a publisher to send information about the Person to a subscriber. + +## Cleanup +In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. + +**Ensure roscore is terminated before moving on to the lab.** diff --git a/_sources/Module5_CustomMessages/Lab1_CustomMessages.md b/_sources/Module5_CustomMessages/Lab1_CustomMessages.md old mode 100755 new mode 100644 index f2404ba..3c34a46 --- a/_sources/Module5_CustomMessages/Lab1_CustomMessages.md +++ b/_sources/Module5_CustomMessages/Lab1_CustomMessages.md @@ -1,70 +1,70 @@ -# Lab 1: Custom Messages - -## Synopsis -This lab will provide practice in creating custom messages. You will use provided code that listens for events from the mouse. Specifically, the code is listening to the position of the cursor, and any buttons pressed. You will create a custom message that is going to pass the cursor position and mouse button events across the `/mouse_info` topic. You will develop a controller that will subscribe to the `/mouse_info` topic and then publish a Twist message to `/cmd_vel` according to the requirements in the lab handout. - - -## Due by 0730 Lesson 13 (M or T depending on which day your class meets) -Before starting this lab, you NEED to complete ICE5. We will work most of this together as a class, but some will be self effort to practice the concepts needed for this lab. - -## Lab Objectives: -In this lab, we will be building upon everything we have learned in the first 5 modules of the course. We are -going add functionality to use basic code that I have created to: - -1. Read the mouse position -1. Detect button or scroll wheel events from the mouse -1. Send custom message to controller -1. The Controller will then scale a Twist message on the `\cmd_vel` topic to drive the robot - -In short, we are going to use the mouse to drive the robot. Moving the mouse cursor up or down on the screen will drive the robot forward and backward. Moving the mouse cursor side to side on the screen will cause the robot to turn left or right. There should also be a dead zone within +/- .25 of the center of the screen where no movement will occur The code template that I have provided for the mouse client depends on a couple of different libraries. We are going to use the python installer (pip) to install two libraries, pyautogui and pynput. - -```bash -dfec@master:∼$ pip install pyautogui -dfec@master:∼$ pip install pynput -``` - -Essentially the code you are going to create will develop the following graph. This graph below does not have the robot nodes on it, so you won’t see the `/cmd_vel` topic active. However, you will either need to use the turtlebot simulation or your actual robot to prove that the code is working. Part of your lab report will require you to show the complete graph. - -```{image} ./mouse_info_topic.png -:width: 450 -:align: center -``` - -### Mouse Node: -The Mouse node will be created by the `mouse_client_OO.py` code. The framework for this code is provided [here](mouse_client_OO.py). Look for the TODO sections to see where you need to edit the code. The Mouse node will primarily detect mouse position and events and publish a custom `MouseController` message that you will create in a lab1 package within the master folder of your repo. - -### Controller Node: -The controller node is created by the `turtlebot_controller.py` code. I have also provided the framework for this code [here](turtlebot_controller.py). The controller node will subscribe to the `/mouse_info` topic and publish a Twist message to the `/cmd_vel` topic - -## Your Custom MouseController Message. -Your `MouseController` message needs to have precisely the following 4 fields in it: -1. Header -1. status (of type `std_msgs/Bool`) -1. xPos (of type float32) -1. yPos (of type float32) - -You will need to use the procedure highlighted in ICE5 to create this custom message. Notice that the second field in the messages above is a message from the `std_msgs` group in ROS. Remember, this is an acceptable field based on what you learned in Module05. If you want to get more familiar with the Bool -message, you can use the built in ROS functionality to see what the format of this message will be: -```bash -dfec@master:∼$ rosmsg info std_msgs/Bool -``` - -## Additional Requirements. -Your code must adhere to the following additional requirements: -1. All code and message files should be stored in a package within the master folder called lab1. **DO NOT EDIT THE CODE FILES IN THE Module05 FOLDER IN THE ECE387 CURRICULUM!!!** -1. The controller will only be activated by scrolling down with the mouse (on the center scroll wheel) -1. The controller will be immediately deactivated by scrolling up with the mouse. -1. If the controller is deactivated, the robot will stop all movement, until an override by another module. -1. You don’t need to handle the override at this point, but your controller code should be organized and flexible enough that it can handle additional capability in the future. - -## Report. -Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under `Resources/Lab Template`. - - -## Turn-in Requirements -**[25 points]** Demonstration of mouse control of Turtlebot (preferably in person, but can be recorded and posted to Teams under the Lab1 channel). - -**[50 points]** Report via Gradescope. - -**[25 points]** Code/ROS package: push your code to your repository. Also, include a screenshot or the raw text of the `turtlebot_controller.py` and `mouse_client_OO.py` files at the end of your report. - +# Lab 1: Custom Messages + +## Synopsis +This lab will provide practice in creating custom messages. You will use provided code that listens for events from the mouse. Specifically, the code is listening to the position of the cursor, and any buttons pressed. You will create a custom message that is going to pass the cursor position and mouse button events across the `/mouse_info` topic. You will develop a controller that will subscribe to the `/mouse_info` topic and then publish a Twist message to `/cmd_vel` according to the requirements in the lab handout. + + +## Due by 0730 Lesson 13 (M or T depending on which day your class meets) +Before starting this lab, you NEED to complete ICE5. We will work most of this together as a class, but some will be self effort to practice the concepts needed for this lab. + +## Lab Objectives: +In this lab, we will be building upon everything we have learned in the first 5 modules of the course. We are +going add functionality to use basic code that I have created to: + +1. Read the mouse position +1. Detect button or scroll wheel events from the mouse +1. Send custom message to controller +1. The Controller will then scale a Twist message on the `\cmd_vel` topic to drive the robot + +In short, we are going to use the mouse to drive the robot. Moving the mouse cursor up or down on the screen will drive the robot forward and backward. Moving the mouse cursor side to side on the screen will cause the robot to turn left or right. There should also be a dead zone within +/- .25 of the center of the screen where no movement will occur The code template that I have provided for the mouse client depends on a couple of different libraries. We are going to use the python installer (pip) to install two libraries, pyautogui and pynput. + +```bash +dfec@master:∼$ pip install pyautogui +dfec@master:∼$ pip install pynput +``` + +Essentially the code you are going to create will develop the following graph. This graph below does not have the robot nodes on it, so you won’t see the `/cmd_vel` topic active. However, you will either need to use the turtlebot simulation or your actual robot to prove that the code is working. Part of your lab report will require you to show the complete graph. + +```{image} ./mouse_info_topic.png +:width: 450 +:align: center +``` + +### Mouse Node: +The Mouse node will be created by the `mouse_client_OO.py` code. The framework for this code is provided [here](mouse_client_OO.py). Look for the TODO sections to see where you need to edit the code. The Mouse node will primarily detect mouse position and events and publish a custom `MouseController` message that you will create in a lab1 package within the master folder of your repo. + +### Controller Node: +The controller node is created by the `turtlebot_controller.py` code. I have also provided the framework for this code [here](turtlebot_controller.py). The controller node will subscribe to the `/mouse_info` topic and publish a Twist message to the `/cmd_vel` topic + +## Your Custom MouseController Message. +Your `MouseController` message needs to have precisely the following 4 fields in it: +1. Header +1. status (of type `std_msgs/Bool`) +1. xPos (of type float32) +1. yPos (of type float32) + +You will need to use the procedure highlighted in ICE5 to create this custom message. Notice that the second field in the messages above is a message from the `std_msgs` group in ROS. Remember, this is an acceptable field based on what you learned in Module05. If you want to get more familiar with the Bool +message, you can use the built in ROS functionality to see what the format of this message will be: +```bash +dfec@master:∼$ rosmsg info std_msgs/Bool +``` + +## Additional Requirements. +Your code must adhere to the following additional requirements: +1. All code and message files should be stored in a package within the master folder called lab1. **DO NOT EDIT THE CODE FILES IN THE Module05 FOLDER IN THE ECE387 CURRICULUM!!!** +1. The controller will only be activated by scrolling down with the mouse (on the center scroll wheel) +1. The controller will be immediately deactivated by scrolling up with the mouse. +1. If the controller is deactivated, the robot will stop all movement, until an override by another module. +1. You don’t need to handle the override at this point, but your controller code should be organized and flexible enough that it can handle additional capability in the future. + +## Report. +Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under `Resources/Lab Template`. + + +## Turn-in Requirements +**[25 points]** Demonstration of mouse control of Turtlebot (preferably in person, but can be recorded and posted to Teams under the Lab1 channel). + +**[50 points]** Report via Gradescope. + +**[25 points]** Code/ROS package: push your code to your repository. Also, include a screenshot or the raw text of the `turtlebot_controller.py` and `mouse_client_OO.py` files at the end of your report. + diff --git a/_sources/Module6_IMU/IMU.md b/_sources/Module6_IMU/IMU.md old mode 100755 new mode 100644 index f22e996..d5f8ca1 --- a/_sources/Module6_IMU/IMU.md +++ b/_sources/Module6_IMU/IMU.md @@ -1,145 +1,145 @@ -# Inertial Measurement Unit - -## Purpose -In practice, an inertial measurement unit (IMU) device provides orientation, angular velocity, and linear acceleration. The [ICM-20648 6-Axis MEMS MotionTracking Device](https://invensense.tdk.com/products/motion-tracking/6-axis/icm-20648/) from TDK includes a 3-axis gyroscope, 3-axis accelerometer, and a Digital Motion Processor (DMP). This IMU is integrated on the OpenCR1.0 board, an open source robot controller embedded with an ARM Cortex-M7 processor. The OpenCR combines sensor data using an EKF to generate IMU estimates 170 times a second. - -![logo](Figures/IMU.jpg) - -The IMU provides values that are part of the robot state and allow the robot to navigate more accurately. Combined with data from the tachometers these values provide the odometry of the robot to estimate change in position over time. We will primarily use the IMU to perform 90 and 180 degree turns. - -![logo](Figures/bot.PNG) - -## Calibrating the IMU -As described above, there are a number of different sensors that work together to provide the attitude and heading estimates for the Turtlebot3. These sensors are sensitive to magnetic fields which are unique to locale and device. As you will learn in future ECE classes, all electronic devices create small magnetic fields. Even electrons traveling over a wire create magnetic fields. The OpenCR board and IMU are strategically placed in the center of the robot for best attitude and heading performance, however, this location is also in the center of a number of magnetic fields. Luckily for us, the creators of the Turtlebot3 were aware of these issues and whenever you run the serial node to connect to the robot the IMU is calibrated. - -## Setup -The ICM-20648 is already integrated into the Turtlebot3 robot, therefore, there is no setup required. Whenever the serial node is ran to connect to the OpenCR board, the IMU is initialized and will start publishing data. - -## Test the IMU -Open a new terminal on the master and run roscore and setup for statistics: - -```bash -roscore -rosparam set enable_statistics true -``` - -Create a secure shell connection to your **Robot** and launch the Turtlebot3 core launchfile. - -```bash -roslaunch turtlebot3_bringup turtlebot3_core.launch -``` - -Open a new terminal on your **Master** and observe what topics are running. - -You should note two topics of interest: **/imu** and **/odom**. - -Echo the output of each of the topics and rotate the **Robot** to see the values change. - -The **/imu** topic combines information from the gyroscope and accelerometer to provide orientation, angular velocity, and linear acceleration. The **/odom** topic combines the information from the **/imu** topic and tachometers to estimate position, orientation, and linear and angular velocities. - -Both of these topics provide the orientation of the robot using a quaternion representation. While quaternions can make computation easier, they are not very human readable, so we will convert to Euler angles. To do this we will use a Python library called [squaternion](https://pypi.org/project/squaternion/). - -The two main functions we will use from the squaternion library: - -First we will need to create a Quaternion object: -``` -q = Quaternion(w, x, y, z) -``` - -Then we will conver that Quaternion to an Euler: -``` -e = q.to_euler(degrees=True) -``` - -Now we have `e`, an array representing our Euler angles, `e[roll, pitch, yaw]`. - -You can keep the node running for the next portion of the lesson. - -## Write the Subscriber -1. In a new terminal on the **Master**, create an **ice6** package which depends on the *geometry_msgs*, *rospy*, and *turtlebot3_bringup* packages, compile and source the workspace: - - ```bash - cd ~/master_ws/src/ece387_master_sp2X-USERNAME/master - catkin_create_pkg ice6 std_msgs rospy turtlebot3_bringup - cd ~/master_ws - catkin_make - source ~/.bashrc - ``` - -1. Create an IMU node: - - ```bash - roscd ice6/src - touch imu_sub.py - ``` - -1. Copy and complete the below code using a GUI editor tool, such as **Sublime** or **VS Code**. Browse to the subscriber you just created and double-click. This will open the file in **Sublime** or **VS Code** (if it is open in any other editor, stop, raise your hand, and get help from an instructor) -> 💡️ **Tip:** Look for the **"TODO"** tag which indicates where you should insert your own code. - -```python -#!/usr/bin/env python3 -import rospy -from squaternion import Quaternion -# TODO: import message type sent over imu topic - - -class IMU: - """Class to read orientation data from Turtlebot3 IMU""" - def __init__(self): - # TODO: subscribe to the imu topic that is published by the - # Turtlebot3 and provides the robot orientation - - - # nicely handle shutdown (Ctrl+c) - self.ctrl_c = False - rospy.on_shutdown(self.shutdownhook) - - # The IMU provides yaw from -180 to 180. This function - # converts the yaw (in degrees) to 0 to 360 - def convert_yaw (self, yaw): - return 360 + yaw if yaw < 0 else yaw - - # Print the current Yaw - def callback_imu(self, imu): - if not self.ctrl_c: - # TODO: create a quaternion using the x, y, z, and w values - # from the correct imu message - - - # TODO: convert the quaternion to euler in degrees - - - # TODO: get the yaw component of the euler - yaw = - - # convert yaw from -180 to 180 to 0 to 360 - yaw = self.convert_yaw(yaw) - print("Current heading is %f degrees." % (yaw)) - - # clean shutdown - def shutdownhook(self): - print("Shutting down the IMU subscriber") - self.ctrl_c = True - -if __name__ == '__main__': - rospy.init_node('imu_sub') - IMU() - rospy.spin() -``` - -4. Save, exit, and make the node executable. - -4. Open a new terminal on the **Master** and run the **imu_sub.py** node. - -4. Rotate the **Robot** and observe the output. - -## Checkpoint -Once complete, get checked off by an instructor showing the output of your **imu_sub** and **rqt_graph** node. Push your ice6 package to your repo for credit - -## Summary -In this lesson you learned how to utilize the on-board IMU and determine the orientation of the Turtlebot3. In the lab that corresponds to this lesson you will apply this knowledge to turn the robot in 90 and 180 degree turns.ROS - -## Cleanup -In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-lab` in. Select 'y'. - -**Ensure roscore is terminated before moving on to the lab.** +# Inertial Measurement Unit + +## Purpose +In practice, an inertial measurement unit (IMU) device provides orientation, angular velocity, and linear acceleration. The [ICM-20648 6-Axis MEMS MotionTracking Device](https://invensense.tdk.com/products/motion-tracking/6-axis/icm-20648/) from TDK includes a 3-axis gyroscope, 3-axis accelerometer, and a Digital Motion Processor (DMP). This IMU is integrated on the OpenCR1.0 board, an open source robot controller embedded with an ARM Cortex-M7 processor. The OpenCR combines sensor data using an EKF to generate IMU estimates 170 times a second. + +![logo](Figures/IMU.jpg) + +The IMU provides values that are part of the robot state and allow the robot to navigate more accurately. Combined with data from the tachometers these values provide the odometry of the robot to estimate change in position over time. We will primarily use the IMU to perform 90 and 180 degree turns. + +![logo](Figures/bot.PNG) + +## Calibrating the IMU +As described above, there are a number of different sensors that work together to provide the attitude and heading estimates for the Turtlebot3. These sensors are sensitive to magnetic fields which are unique to locale and device. As you will learn in future ECE classes, all electronic devices create small magnetic fields. Even electrons traveling over a wire create magnetic fields. The OpenCR board and IMU are strategically placed in the center of the robot for best attitude and heading performance, however, this location is also in the center of a number of magnetic fields. Luckily for us, the creators of the Turtlebot3 were aware of these issues and whenever you run the serial node to connect to the robot the IMU is calibrated. + +## Setup +The ICM-20648 is already integrated into the Turtlebot3 robot, therefore, there is no setup required. Whenever the serial node is ran to connect to the OpenCR board, the IMU is initialized and will start publishing data. + +## Test the IMU +Open a new terminal on the master and run roscore and setup for statistics: + +```bash +roscore +rosparam set enable_statistics true +``` + +Create a secure shell connection to your **Robot** and launch the Turtlebot3 core launchfile. + +```bash +roslaunch turtlebot3_bringup turtlebot3_core.launch +``` + +Open a new terminal on your **Master** and observe what topics are running. + +You should note two topics of interest: **/imu** and **/odom**. + +Echo the output of each of the topics and rotate the **Robot** to see the values change. + +The **/imu** topic combines information from the gyroscope and accelerometer to provide orientation, angular velocity, and linear acceleration. The **/odom** topic combines the information from the **/imu** topic and tachometers to estimate position, orientation, and linear and angular velocities. + +Both of these topics provide the orientation of the robot using a quaternion representation. While quaternions can make computation easier, they are not very human readable, so we will convert to Euler angles. To do this we will use a Python library called [squaternion](https://pypi.org/project/squaternion/). + +The two main functions we will use from the squaternion library: + +First we will need to create a Quaternion object: +``` +q = Quaternion(w, x, y, z) +``` + +Then we will conver that Quaternion to an Euler: +``` +e = q.to_euler(degrees=True) +``` + +Now we have `e`, an array representing our Euler angles, `e[roll, pitch, yaw]`. + +You can keep the node running for the next portion of the lesson. + +## Write the Subscriber +1. In a new terminal on the **Master**, create an **ice6** package which depends on the *geometry_msgs*, *rospy*, and *turtlebot3_bringup* packages, compile and source the workspace: + + ```bash + cd ~/master_ws/src/ece387_master_sp2X-USERNAME/master + catkin_create_pkg ice6 std_msgs rospy turtlebot3_bringup + cd ~/master_ws + catkin_make + source ~/.bashrc + ``` + +1. Create an IMU node: + + ```bash + roscd ice6/src + touch imu_sub.py + ``` + +1. Copy and complete the below code using a GUI editor tool, such as **Sublime** or **VS Code**. Browse to the subscriber you just created and double-click. This will open the file in **Sublime** or **VS Code** (if it is open in any other editor, stop, raise your hand, and get help from an instructor) +> 💡️ **Tip:** Look for the **"TODO"** tag which indicates where you should insert your own code. + +```python +#!/usr/bin/env python3 +import rospy +from squaternion import Quaternion +# TODO: import message type sent over imu topic + + +class IMU: + """Class to read orientation data from Turtlebot3 IMU""" + def __init__(self): + # TODO: subscribe to the imu topic that is published by the + # Turtlebot3 and provides the robot orientation + + + # nicely handle shutdown (Ctrl+c) + self.ctrl_c = False + rospy.on_shutdown(self.shutdownhook) + + # The IMU provides yaw from -180 to 180. This function + # converts the yaw (in degrees) to 0 to 360 + def convert_yaw (self, yaw): + return 360 + yaw if yaw < 0 else yaw + + # Print the current Yaw + def callback_imu(self, imu): + if not self.ctrl_c: + # TODO: create a quaternion using the x, y, z, and w values + # from the correct imu message + + + # TODO: convert the quaternion to euler in degrees + + + # TODO: get the yaw component of the euler + yaw = + + # convert yaw from -180 to 180 to 0 to 360 + yaw = self.convert_yaw(yaw) + print("Current heading is %f degrees." % (yaw)) + + # clean shutdown + def shutdownhook(self): + print("Shutting down the IMU subscriber") + self.ctrl_c = True + +if __name__ == '__main__': + rospy.init_node('imu_sub') + IMU() + rospy.spin() +``` + +4. Save, exit, and make the node executable. + +4. Open a new terminal on the **Master** and run the **imu_sub.py** node. + +4. Rotate the **Robot** and observe the output. + +## Checkpoint +Once complete, get checked off by an instructor showing the output of your **imu_sub** and **rqt_graph** node. Push your ice6 package to your repo for credit + +## Summary +In this lesson you learned how to utilize the on-board IMU and determine the orientation of the Turtlebot3. In the lab that corresponds to this lesson you will apply this knowledge to turn the robot in 90 and 180 degree turns.ROS + +## Cleanup +In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-lab` in. Select 'y'. + +**Ensure roscore is terminated before moving on to the lab.** diff --git a/_sources/Module6_IMU/Lab2_IMU.md b/_sources/Module6_IMU/Lab2_IMU.md old mode 100755 new mode 100644 index 4d65974..77a9c4e --- a/_sources/Module6_IMU/Lab2_IMU.md +++ b/_sources/Module6_IMU/Lab2_IMU.md @@ -1,106 +1,106 @@ -# Lab 2: Inertial Measurement Unit - -## Purpose -This lab will integrate the on-board IMU with the Turtlebot3 controller to turn the robot 90 degrees left or right. Do not disable any of the mouse controller functionality in the turtlebot_controller.py code. The IMU and mouse control functionality should be able to coexist seemlessly. - -## Master -### Setup: -In the `/master_ws/src/ece387_master_sp2X-USERNAME/master` folder, create a **lab2** package which depends on **std_msgs**, **rospy**, **geometry_msgs**, and **turtlebot3_bringup**. - -### controller.py -1. Copy the turtlebot_controller.py file from lab1 into the lab2 package. - -1. Open the turtlebot_controller.py file from lab2 using an editor. - -1. Import the squaternion library and Imu message used in ICE6. - -1. Add the following Class variables within the class **above** the `__init__()` function: - - 1. `K_HDG = 0.1 # rotation controller constant` - 1. `HDG_TOL = 10 # heading tolerance +/- degrees` - 1. `MIN_ANG_Z = 0.5 # limit rad/s values sent to Turtlebot3` - 1. `MAX_ANG_Z = 1.5 # limit rad/s values sent to Turtlebot3` - -1. Add the following to the `__init__()` function: - - 1. Instance variable, `self.curr_yaw`, initialized to 0 to store the current orientation of the robot - 1. Instance variable, `self.goal_yaw`, initialized to 0 to store the goal orientation of the robot - 1. Instance variable, `self.turning`, initialized to `False` to store if the robot is currently turning - 1. A subscriber to the IMU topic of interest with a callback to the callback_imu() function - -1. Add the `convert_yaw()` function from ICE6. - -1. Add the `callback_imu()` function from ICE6, removing print statements and setting the instance variable, `self.curr_yaw`. - -1. Edit the `callback_controller()` function so it turns the robot 90 degrees in the direction inputed by the user (left or right). Below is some pseudo-code to help you code the controller function - -> ⚠️ **WARNING:** Pseudo-code is not actual code and cannot be copied and expected to work! - -```python -def callback_controller(self, event): - # local variables do not need the self - yaw_err = 0 - ang_z = 0 - # not turning, so get user input - if not turning: - read from user and set value to instance variable, self.goal_yaw - input("Input l or r to turn 90 deg") - - # check input and determine goal yaw - if input is equal to "l" - set goal yaw to curr yaw plus/minus 90 - turning equals True - else if input is equal to "r" - set goal yaw to curr yaw plus/minus 90 - turning equals True - else - print error and tell user valid inputs - - # check bounds - if goal_yaw is less than 0 then add 360 - else if goal_yaw is greater than 360 then subtract 360 - - # turn until goal is reached - elif turning: - yaw_err = self.goal_yaw - self.curr_yaw - - # determine if robot should turn clockwise or counterclockwise - if yaw_err > 180: - yaw_err = yaw_err - 360 - elif yaw_err < -180: - yaw_err = yaw_err + 360 - - # proportional controller that turns the robot until goal - # yaw is reached - ang_z = self.K_HDG * yaw_err - - if ang_z < self.MIN: ang_z = self.MIN # need to add negative test as well! - elif ang_Z > self.MAX: ang_z = self.MAX # need to add negative test as well! - - # check goal orientation - if abs(yaw_err) < self.HDG_TOL - turning equals False - ang_z = 0 - - # set twist message and publish - self.cmd.linear.x = 0 - self.cmd.angular.z = ang_z - publish message -``` - -## Run your nodes -1. On the **Master** open a terminal and run **roscore**. -1. Open another terminal and enable statistics for **rqt_graph**. -1. Run the controller node. -1. Open secure shell into the **Robot** and run the **turtlebot3_core** launch file. -1. Type "l" or "r" to turn the robot 90 degrees. - -## Report -Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under `Resources/Lab Template`. - -## Turn-in Requirements -**[25 points]** Demonstration of keyboard control of Turtlebot3 (preferably in person, but can be recorded and posted to Teams under the Lab 2 channel). - -**[50 points]** Report via Gradescope. - -**[25 points]** Code: push your code to your repository. Also, include a screen shot of the **turtlebot_controller.py** file at the end of your report. +# Lab 2: Inertial Measurement Unit + +## Purpose +This lab will integrate the on-board IMU with the Turtlebot3 controller to turn the robot 90 degrees left or right. Do not disable any of the mouse controller functionality in the turtlebot_controller.py code. The IMU and mouse control functionality should be able to coexist seemlessly. + +## Master +### Setup: +In the `/master_ws/src/ece387_master_sp2X-USERNAME/master` folder, create a **lab2** package which depends on **std_msgs**, **rospy**, **geometry_msgs**, and **turtlebot3_bringup**. + +### controller.py +1. Copy the turtlebot_controller.py file from lab1 into the lab2 package. + +1. Open the turtlebot_controller.py file from lab2 using an editor. + +1. Import the squaternion library and Imu message used in ICE6. + +1. Add the following Class variables within the class **above** the `__init__()` function: + + 1. `K_HDG = 0.1 # rotation controller constant` + 1. `HDG_TOL = 10 # heading tolerance +/- degrees` + 1. `MIN_ANG_Z = 0.5 # limit rad/s values sent to Turtlebot3` + 1. `MAX_ANG_Z = 1.5 # limit rad/s values sent to Turtlebot3` + +1. Add the following to the `__init__()` function: + + 1. Instance variable, `self.curr_yaw`, initialized to 0 to store the current orientation of the robot + 1. Instance variable, `self.goal_yaw`, initialized to 0 to store the goal orientation of the robot + 1. Instance variable, `self.turning`, initialized to `False` to store if the robot is currently turning + 1. A subscriber to the IMU topic of interest with a callback to the callback_imu() function + +1. Add the `convert_yaw()` function from ICE6. + +1. Add the `callback_imu()` function from ICE6, removing print statements and setting the instance variable, `self.curr_yaw`. + +1. Edit the `callback_controller()` function so it turns the robot 90 degrees in the direction inputed by the user (left or right). Below is some pseudo-code to help you code the controller function + +> ⚠️ **WARNING:** Pseudo-code is not actual code and cannot be copied and expected to work! + +```python +def callback_controller(self, event): + # local variables do not need the self + yaw_err = 0 + ang_z = 0 + # not turning, so get user input + if not turning: + read from user and set value to instance variable, self.goal_yaw + input("Input l or r to turn 90 deg") + + # check input and determine goal yaw + if input is equal to "l" + set goal yaw to curr yaw plus/minus 90 + turning equals True + else if input is equal to "r" + set goal yaw to curr yaw plus/minus 90 + turning equals True + else + print error and tell user valid inputs + + # check bounds + if goal_yaw is less than 0 then add 360 + else if goal_yaw is greater than 360 then subtract 360 + + # turn until goal is reached + elif turning: + yaw_err = self.goal_yaw - self.curr_yaw + + # determine if robot should turn clockwise or counterclockwise + if yaw_err > 180: + yaw_err = yaw_err - 360 + elif yaw_err < -180: + yaw_err = yaw_err + 360 + + # proportional controller that turns the robot until goal + # yaw is reached + ang_z = self.K_HDG * yaw_err + + if ang_z < self.MIN: ang_z = self.MIN # need to add negative test as well! + elif ang_Z > self.MAX: ang_z = self.MAX # need to add negative test as well! + + # check goal orientation + if abs(yaw_err) < self.HDG_TOL + turning equals False + ang_z = 0 + + # set twist message and publish + self.cmd.linear.x = 0 + self.cmd.angular.z = ang_z + publish message +``` + +## Run your nodes +1. On the **Master** open a terminal and run **roscore**. +1. Open another terminal and enable statistics for **rqt_graph**. +1. Run the controller node. +1. Open secure shell into the **Robot** and run the **turtlebot3_core** launch file. +1. Type "l" or "r" to turn the robot 90 degrees. + +## Report +Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under `Resources/Lab Template`. + +## Turn-in Requirements +**[25 points]** Demonstration of keyboard control of Turtlebot3 (preferably in person, but can be recorded and posted to Teams under the Lab 2 channel). + +**[50 points]** Report via Gradescope. + +**[25 points]** Code: push your code to your repository. Also, include a screen shot of the **turtlebot_controller.py** file at the end of your report. diff --git a/_sources/Module7_LaunchFile/LaunchFile.md b/_sources/Module7_LaunchFile/LaunchFile.md old mode 100755 new mode 100644 index 5741cac..b37543b --- a/_sources/Module7_LaunchFile/LaunchFile.md +++ b/_sources/Module7_LaunchFile/LaunchFile.md @@ -1,253 +1,253 @@ -# Launch Files - -## Purpose -Large applications in robotics typically involve several interconnected ROS nodes, each of which have many parameters. Your current setup is a good example: as you experienced in the IMU lab, you had to open 3 different terminals to run all of the nodes necessary for our system to that point: - -**Robot:** -- turtlebot3_core.launch - -**Master:** -- roscore -- lab2/turtlebot_controller.py - -This problem is only going to get more complex as we add additional functionality to our robot. As it stands right now, every node requires a separate terminal window and the associated command to run it. Using the *roslaunch* tool, we can eliminate that administrivia of running each node separately. We will create/edit two launch files to bring up the nodes on the master and robot. - -## [roslaunch](http://wiki.ros.org/roslaunch) -The *roslaunch* tool is used to launch multiple ROS nodes locally and remotely via SSH. We can run nodes that we have created, nodes from pre-built packages, and other launch files. The roslaunch tool takes in one or more XML configuration files (with the .launch extension) that specify the parameters to set and nodes to launch. - -A launch file is an XML document which specifies: -- which nodes to execute -- their parameters -- what other files to include - -An XML file stands for Extensible Markup Language (XML). This is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable. That isn't necessarily important for this class, but you can read about XML on Wikipedia if you are interested. - -We will then use a tool embedded within ROS called *roslaunch* to easily launch multiple nodes or even other launch files. - -By convention, we will give our launch files the *.launch* extension and store them in a *launch* folder within our package. This isn't required, but it is the common convention. - -## Current State -In this section we will first use the conventional technique to bring up all of the nodes required for **Lab 2** using the currently understood techniques. - -Open a new terminal on your **Master** and start *roscore*: - -```bash -roscore -``` - -Notice, running **roscore** now monopolized that terminal and you can no longer use it for anything else. - -Open a new terminal or tab on your **Master** and run the **controller.py** node: - -```bash -rosrun lab2 turtlebot_controller.py -``` - -At this point we are done with the master. We only needed to bring up two terminals, however, this is still a relatively simple system in teh grand scheme of things. - -> 📝️ **Note:** We did need to keep the terminal with the **turtlebot_controller.py** node open so we can enter commands. - -We can now transition to the robot and bring up the required nodes. - -Open a new terminal window on the **Master** and use SSH to create a secure shell into the **Robot**: - -```bash -ssh pi@robotX -``` - -Utilize the SSH instance to start the **turtlebot3_core** launch file: - -```bash -roslaunch turtlebot3_bringup turtlebot3_core.launch -``` - -Overall you needed 3 terminal windows, including one SSH connection, to bring up this relatively simple system of sensors. - -Kill all nodes and roscore. - -## roslaunch on the Robot -Navigate to one of the terminals with a secure shell connection to the robot. - -Open the *turtlebot3_core.launch* file: - -```bash -rosed turtlebot3_bringup turtlebot3_core.launch -``` - -At this time, the *turtlebot3_core.launch* file includes everything you need to run the robot's core functionality (driving and reading data from the IMU). Let's talk about each line of the file: - -The XML declaration is typically included at the beginning of an XML file to indicate the version of XML being used (in this case, version 1.0). However, when writing ROS (Robot Operating System) launch files in XML format, you generally do not need to include this declaration. - -ROS launch files typically have a specific structure and do not require the XML version declaration. Instead, a ROS launch file typically starts with the tag. For example: - -```xml - - - -``` - -In ROS launch files, the tag serves as the root element for defining various nodes, parameters, and other configurations. Therefore, every launch file opens and closes with the *launch* root element. - -```xml - - - - - - - -``` - -The above establishes the node to create a serial connection to the Turtlebot3. We can see a few parameters were set, `port` and `baud`, to help in establishing the connection. - -Close the editor: `ctrl+x` - - -## roslaunch on the Master -Navigate to your **lab2** package on the **Master** and create a launch directory: - -```bash -roscd lab2 -mkdir launch -cd launch -touch lab2.launch -``` - -Open the launch file to edit (I recommend using **sublime**). - -Now we are going to edit the launch file to bring up all of the nodes (both on the **Master** locally and remotely on the **Robot**) - -> 📝️ **Note:** *roslaunch* will look to see if *roscore* is started. If it is not, it will automatically run *roscore*. - -Add the following to the **lab2.launch** file - -```xml - - - - - - - - - - - - - - - - - - - - -``` - -> 📝️ **Note:** Remember earlier how we reminded you that we need to keep the terminal available for the controller to type commands. By using the two additional parameters *screen* and *launch-prefix*, we can ensure the terminal is available for use. - -Save and close the launch file. - -## Setting up remote connectivity -Before we can run the above launch file we need to accomplish some steps to ensure we can connect remotely to our **Robot**. - -1. Open a terminal on your **Master** and type the following: - - ```bash - ssh -oHostKeyAlgorithms='ssh-rsa' pi@robotX - ``` - -1. Type `yes` and press **Enter**. - -1. When prompted, type in the password for the pi user on the robot. - -1. Type `exit`. - -1. On the **Master** type the following to create private and public keys: - - ```bash - ssh-keygen -t rsa - ``` - -1. Press **Enter** - -1. Press **Enter** twice more for *no passphrase** - -1. On the **Master** send the public key to the **Robot** - - ```bash - ssh-copy-id pi@robotX - ``` - -1. Test that you can now create an SSH connection to the **Robot** without having to enter a password. - -1. In your secure shell to the **Robot** create a remote ROS environment shell script file: - - ```bash - cd robot_ws/devel/ - nano remote_env_loader.sh - ``` - -1. Copy the following environmental variables into the file: - - ```bash - #!/bin/bash - - source /opt/ros/noetic/setup.bash - source ~/robot_ws/devel/setup.bash - export ROS_PACKAGE_PATH=~/robot_ws/src:/opt/ros/noetic/share - export ROS_HOSTNAME=`hostname` - export ROS_MASTER_URI=http://masterX:11311 - export EDITOR='nano -w' - export HOSTNAME=`hostname` - export TURTLEBOT3_MODEL=burger - export LDS_MODEL=LDS-01 - - exec "$@" - ``` - - > 📝️ **Note:** These are the same environmental variables we have inserted to our `.bashrc` files on the **Master** and **Robot**. When we run nodes remotely on a system the `remote_env_loader.sh` file is loaded instead of the `.bashrc` file. - -1. Save and exit. - -1. Make the file executable: - - ```bash - chmod +x remote_env_loader.sh - ``` - -## Running Launch Files -Browse to a terminal on the **Master** and make and source your workspace: - -Utilize the *roslaunch* utility to execute the **lab2** launch file on your **Master**: - -```bash -roslaunch lab2 lab2.launch -``` - -Open a new terminal and list the running nodes. You should see *rosout* (*roscore*), *controller*, *joint* and *robot* state publishers, *rviz* and the remote node, *turtlebot3_core*. - - -In a separate terminal, bring up **rqt_graph**. Your output should look similar to this: - -![logo](Figures/rqt_graph.png) - -## Checkpoint -Once complete, get checked off by an instructor showing the output of your **rqt_graph** node. - -## Summary -There is clearly a lot more we can do with launch files, but this will get you started. You now know how to run nodes, other launch files, and provide parameters to a node using a launch file. I encourage you to visit the ROS tutorials online if you need to do more complex functions with the launch files. - -## Cleanup -In each terminal window, close the nodes by typing `ctrl+c`. Exit any SSH connections. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. - -**Ensure roscore is terminated before moving on to the next lesson.** +# Launch Files + +## Purpose +Large applications in robotics typically involve several interconnected ROS nodes, each of which have many parameters. Your current setup is a good example: as you experienced in the IMU lab, you had to open 3 different terminals to run all of the nodes necessary for our system to that point: + +**Robot:** +- turtlebot3_core.launch + +**Master:** +- roscore +- lab2/turtlebot_controller.py + +This problem is only going to get more complex as we add additional functionality to our robot. As it stands right now, every node requires a separate terminal window and the associated command to run it. Using the *roslaunch* tool, we can eliminate that administrivia of running each node separately. We will create/edit two launch files to bring up the nodes on the master and robot. + +## [roslaunch](http://wiki.ros.org/roslaunch) +The *roslaunch* tool is used to launch multiple ROS nodes locally and remotely via SSH. We can run nodes that we have created, nodes from pre-built packages, and other launch files. The roslaunch tool takes in one or more XML configuration files (with the .launch extension) that specify the parameters to set and nodes to launch. + +A launch file is an XML document which specifies: +- which nodes to execute +- their parameters +- what other files to include + +An XML file stands for Extensible Markup Language (XML). This is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable. That isn't necessarily important for this class, but you can read about XML on Wikipedia if you are interested. + +We will then use a tool embedded within ROS called *roslaunch* to easily launch multiple nodes or even other launch files. + +By convention, we will give our launch files the *.launch* extension and store them in a *launch* folder within our package. This isn't required, but it is the common convention. + +## Current State +In this section we will first use the conventional technique to bring up all of the nodes required for **Lab 2** using the currently understood techniques. + +Open a new terminal on your **Master** and start *roscore*: + +```bash +roscore +``` + +Notice, running **roscore** now monopolized that terminal and you can no longer use it for anything else. + +Open a new terminal or tab on your **Master** and run the **controller.py** node: + +```bash +rosrun lab2 turtlebot_controller.py +``` + +At this point we are done with the master. We only needed to bring up two terminals, however, this is still a relatively simple system in teh grand scheme of things. + +> 📝️ **Note:** We did need to keep the terminal with the **turtlebot_controller.py** node open so we can enter commands. + +We can now transition to the robot and bring up the required nodes. + +Open a new terminal window on the **Master** and use SSH to create a secure shell into the **Robot**: + +```bash +ssh pi@robotX +``` + +Utilize the SSH instance to start the **turtlebot3_core** launch file: + +```bash +roslaunch turtlebot3_bringup turtlebot3_core.launch +``` + +Overall you needed 3 terminal windows, including one SSH connection, to bring up this relatively simple system of sensors. + +Kill all nodes and roscore. + +## roslaunch on the Robot +Navigate to one of the terminals with a secure shell connection to the robot. + +Open the *turtlebot3_core.launch* file: + +```bash +rosed turtlebot3_bringup turtlebot3_core.launch +``` + +At this time, the *turtlebot3_core.launch* file includes everything you need to run the robot's core functionality (driving and reading data from the IMU). Let's talk about each line of the file: + +The XML declaration is typically included at the beginning of an XML file to indicate the version of XML being used (in this case, version 1.0). However, when writing ROS (Robot Operating System) launch files in XML format, you generally do not need to include this declaration. + +ROS launch files typically have a specific structure and do not require the XML version declaration. Instead, a ROS launch file typically starts with the tag. For example: + +```xml + + + +``` + +In ROS launch files, the tag serves as the root element for defining various nodes, parameters, and other configurations. Therefore, every launch file opens and closes with the *launch* root element. + +```xml + + + + + + + +``` + +The above establishes the node to create a serial connection to the Turtlebot3. We can see a few parameters were set, `port` and `baud`, to help in establishing the connection. + +Close the editor: `ctrl+x` + + +## roslaunch on the Master +Navigate to your **lab2** package on the **Master** and create a launch directory: + +```bash +roscd lab2 +mkdir launch +cd launch +touch lab2.launch +``` + +Open the launch file to edit (I recommend using **sublime**). + +Now we are going to edit the launch file to bring up all of the nodes (both on the **Master** locally and remotely on the **Robot**) + +> 📝️ **Note:** *roslaunch* will look to see if *roscore* is started. If it is not, it will automatically run *roscore*. + +Add the following to the **lab2.launch** file + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +> 📝️ **Note:** Remember earlier how we reminded you that we need to keep the terminal available for the controller to type commands. By using the two additional parameters *screen* and *launch-prefix*, we can ensure the terminal is available for use. + +Save and close the launch file. + +## Setting up remote connectivity +Before we can run the above launch file we need to accomplish some steps to ensure we can connect remotely to our **Robot**. + +1. Open a terminal on your **Master** and type the following: + + ```bash + ssh -oHostKeyAlgorithms='ssh-rsa' pi@robotX + ``` + +1. Type `yes` and press **Enter**. + +1. When prompted, type in the password for the pi user on the robot. + +1. Type `exit`. + +1. On the **Master** type the following to create private and public keys: + + ```bash + ssh-keygen -t rsa + ``` + +1. Press **Enter** + +1. Press **Enter** twice more for *no passphrase** + +1. On the **Master** send the public key to the **Robot** + + ```bash + ssh-copy-id pi@robotX + ``` + +1. Test that you can now create an SSH connection to the **Robot** without having to enter a password. + +1. In your secure shell to the **Robot** create a remote ROS environment shell script file: + + ```bash + cd robot_ws/devel/ + nano remote_env_loader.sh + ``` + +1. Copy the following environmental variables into the file: + + ```bash + #!/bin/bash + + source /opt/ros/noetic/setup.bash + source ~/robot_ws/devel/setup.bash + export ROS_PACKAGE_PATH=~/robot_ws/src:/opt/ros/noetic/share + export ROS_HOSTNAME=`hostname` + export ROS_MASTER_URI=http://masterX:11311 + export EDITOR='nano -w' + export HOSTNAME=`hostname` + export TURTLEBOT3_MODEL=burger + export LDS_MODEL=LDS-01 + + exec "$@" + ``` + + > 📝️ **Note:** These are the same environmental variables we have inserted to our `.bashrc` files on the **Master** and **Robot**. When we run nodes remotely on a system the `remote_env_loader.sh` file is loaded instead of the `.bashrc` file. + +1. Save and exit. + +1. Make the file executable: + + ```bash + chmod +x remote_env_loader.sh + ``` + +## Running Launch Files +Browse to a terminal on the **Master** and make and source your workspace: + +Utilize the *roslaunch* utility to execute the **lab2** launch file on your **Master**: + +```bash +roslaunch lab2 lab2.launch +``` + +Open a new terminal and list the running nodes. You should see *rosout* (*roscore*), *controller*, *joint* and *robot* state publishers, *rviz* and the remote node, *turtlebot3_core*. + + +In a separate terminal, bring up **rqt_graph**. Your output should look similar to this: + +![logo](Figures/rqt_graph.png) + +## Checkpoint +Once complete, get checked off by an instructor showing the output of your **rqt_graph** node. + +## Summary +There is clearly a lot more we can do with launch files, but this will get you started. You now know how to run nodes, other launch files, and provide parameters to a node using a launch file. I encourage you to visit the ROS tutorials online if you need to do more complex functions with the launch files. + +## Cleanup +In each terminal window, close the nodes by typing `ctrl+c`. Exit any SSH connections. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. + +**Ensure roscore is terminated before moving on to the next lesson.** diff --git a/_sources/Module8_LIDAR/LIDAR.md b/_sources/Module8_LIDAR/LIDAR.md old mode 100755 new mode 100644 index af35c6a..3261d86 --- a/_sources/Module8_LIDAR/LIDAR.md +++ b/_sources/Module8_LIDAR/LIDAR.md @@ -1,191 +1,194 @@ -# LIDAR - -## Purpose -In this lesson we will enable the robot to avoid obstacles. Many sensors provide obstacle avoidance capabilities: camera, sonar, infrared, LIDAR, etc. All of these will work to enable the robot to avoid obstacles, but we will use LIDAR as it is an affordable, but very capable solution. - -## LIDAR -[Robotis's LDS-01](https://www.robotis.us/360-laser-distance-sensor-lds-01-lidar/) is a 360 deg Laser Distance Sensor (LDS). It is based on laser triangulation ranging principles and uses high-speed vision acquisition and processing hardware. It measures distance data in more than 1800 times per second. It has a detection range between .12 m and 3.5 m and an angular resolution of 1 degree. The distance accuracy is .015 m between .12 m and .499 m then +/- 5% up to 3.5 m. - -![logo](Figures/rplidar.png) - -### Videos: -[Airborne LiDAR](https://www.youtube.com/watch?v=EYbhNSUnIdU) - -[Turtlebot3 LDS](https://www.youtube.com/watch?v=9oic8aT3wIc&t) - -## Quick Check on LIDAR Variant -The robots for our class have two different LIDAR variants. The older bots have the LDS-01 which is exactly what is pictured above. The newer bots likely have the LDS-02 (pictured below). If you have the LDS-02, you will need to go into the .bashrc file and change the last line in the file to indicate the proper variant. - -![logo](Figures/LDS02.jpeg) - -```bash -sudo nano ~/.bashrc -``` -You are looking for a line that looks like this: - -```bash -export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR -``` -You will need to change that line to LDS-02. **You will need to accomplish this on both the master and the robot** - -## Setup -The [hls_lfcd_lds_driver](http://wiki.ros.org/hls_lfcd_lds_driver) package enables data to be received from the LIDAR over the **/scan** topic. The package is pre-installed on your **Robot**, but as always, trust, but verify. Open a new secure shell into your **Robot** and run the following: - -```bash -rospack find hls_lfcd_lds_driver -``` - -If installed, the command should return the absolute path to the package, similar to: - -```bash -/opt/ros/noetic/share/hls_lfcd_lds_driver -``` - -If the command instead returns an error, then you need to install the package. - -```bash -sudo apt install ros-noetic-hls-lfcd-lds-driver -``` - -## Testing LIDAR -Open a new terminal on the master and run roscore and setup for statistics: - -```bash -roscore -rosparam set enable_statistics true -``` - -Select the terminal with the secure shell connection to your **Robot** and open the `turtlebot3_lidar.launch` file: - -``` -rosed turtlebot3_bringup turtlebot3_lidar.launch -``` - -We can see that this launch file is pretty simple and only launches the **hls_laser_publisher** node. - -Run the launch file on the **Robot**: - -```bash -roslaunch turtlebot3_bringup turtlebot3_lidar.launch -``` - -In a new terminal on the **Master**, we can visualize the Turtlebot3 and LIDAR data using another launch file from the Turtlebot3: - -```bash -roslaunch turtlebot3_bringup turtlebot3_model.launch -``` - -This should open an RVIZ window where we can visualize ROS components of our system. In the "Displays" menu on the left you should see two submenus of interest: "LaserScan" and "RobotModel". These allow us to depict the Turtlebot3 and LIDAR data. - -You should see red dots fill the **rviz** map where obstacles exist. - -Investigate what data the **hls_laser_publisher** is sending. Type the following and observe the command output: - -```bash -rostopic list -rostopic info /scan -rostopic type /scan -rostopic type /scan | rosmsg show -rostopic echo /scan -``` - -At this point you can kill all nodes on the master, but keep the **turtlebot3_lidar** launch file running on the **Robot**. - -## LIDAR Subscriber -In this section we will build a subscriber that will print the range data from the Turtlebot3 LIDAR. - -1. Browse to a terminal on the **Master** and create an `ice8` package: - ```bash - cd ~/master_ws/src/ece387_master_sp2X-USERNAME/master - catkin_create_pkg ice8 rospy sensor_msgs geometry_msgs turtlebot3_bringup - cd ~/master_ws - catkin_make - source ~/.bashrc - ``` - -1. Create an lidar node: - - ```bash - roscd ice8/src - touch lidar_sub.py - ``` - -1. Copy and complete the below code using the GUI editor tool, **Atom**. Browse to the subscriber you just created and double-click. This will open the file in **Atom** (if it is open in any other editor, stop, raise your hand, and get help from an instructor) -> 💡️ **Tip:** Look for the **"TODO"** tag which indicates where you should insert your own code. - -The code should obtain the list of range data from the LIDAR launch file running on the robot, convert the angles from 0 to 180 degrees and 0 to -180 degrees to 0 to 360 degrees. Lastly, the subscriber will print the average distance of obstacles 30 degrees off the nose of the robot. - -```python -#!/usr/bin/env python3 -import rospy, math -# TODO: import correct message - - -# lambda function to convert rad to deg -RAD2DEG = lambda x: ((x)*180./math.pi) -# convert LaserScan degree from -180 - 180 degs to 0 - 360 degs -DEG_CONV = lambda deg: deg + 360 if deg < 0 else deg - -class LIDAR: - """Class to read lidar data from the Turtlebot3 LIDAR""" - def __init__(self): - # TODO: create a subscriber to the scan topic published by the lidar launch file - - - self.ctrl_c = False - rospy.on_shutdown(self.shutdownhook) - - def callback_lidar(self, scan): - if not self.ctrl_c: - degrees = [] - ranges = [] - - # determine how many scans were taken during rotation - count = int(scan.scan_time / scan.time_increment) - - for i in range(count): - # using min angle and incr data determine curr angle, - # convert to degrees, convert to 360 scale - degrees.append(int(DEG_CONV(RAD2DEG(scan.angle_min + scan.angle_increment*i)))) - rng = scan.ranges[i] - - # ensure range values are valid; set to 0 if not - if rng < scan.range_min or rng > scan.range_max: - ranges.append(0.0) - else: - ranges.append(rng) - - # python way to iterate two lists at once! - for deg, rng in zip(degrees, ranges): - # TODO: sum and count the ranges 30 degrees off the nose of the robot - - - # TODO: ensure you don't divide by 0 and print average off the nose - - - def shutdownhook(self): - print("Shutting down lidar subscriber") - self.ctrl_c = True - -if __name__ == '__main__': - rospy.init_node('lidar_sub') - LIDAR() - rospy.spin() -``` - -4. Save, exit, and make the node executable. - -4. Open a new terminal on the **Master** and run the **lidar_sub.py** node. - -4. Rotate the **Robot** and observe the distance off the nose. - -## Checkpoint -Once complete, get checked off by an instructor showing the output of your **lidar_sub** and **rqt_graph** node. - -## Summary -In this lesson you learned how to integrate the LIDAR and get the distance of objects off the nose of the robot using the pre-built LIDAR package. In the lab that corresponds to this lesson you will apply this knowledge to stop the robot a specified distance from an obstacle and turn. - -## Cleanup -In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. - -**Ensure roscore is terminated before moving on to the lab.** +# LIDAR + +## Purpose +In this lesson we will enable the robot to avoid obstacles. Many sensors provide obstacle avoidance capabilities: camera, sonar, infrared, LIDAR, etc. All of these will work to enable the robot to avoid obstacles, but we will use LIDAR as it is an affordable, but very capable solution. + +## LIDAR +[Robotis's LDS-01](https://www.robotis.us/360-laser-distance-sensor-lds-01-lidar/) is a 360 deg Laser Distance Sensor (LDS). It is based on laser triangulation ranging principles and uses high-speed vision acquisition and processing hardware. It measures distance data in more than 1800 times per second. It has a detection range between .12 m and 3.5 m and an angular resolution of 1 degree. The distance accuracy is .015 m between .12 m and .499 m then +/- 5% up to 3.5 m. + +![logo](Figures/rplidar.png) + +### Videos: +[Airborne LiDAR](https://www.youtube.com/watch?v=EYbhNSUnIdU) + +[Turtlebot3 LDS](https://www.youtube.com/watch?v=9oic8aT3wIc&t) + +## Quick Check on LIDAR Variant +The robots for our class have two different LIDAR variants. The older bots have the LDS-01 which is exactly what is pictured above. The newer bots likely have the LDS-02 (pictured below). If you have the LDS-02, you will need to go into the .bashrc file and change the last line in the file to indicate the proper variant. + +![logo](Figures/LDS02.jpeg) + +```bash +sudo nano ~/.bashrc +``` +You are looking for a line that looks like this: + +```bash +export LDS_MODEL=LDS-01 # replace with LDS-02 if using new LIDAR +``` +You will need to change that line to LDS-02. **You will need to accomplish this on both the master and the robot** + +## Setup +The [hls_lfcd_lds_driver](http://wiki.ros.org/hls_lfcd_lds_driver) package enables data to be received from the LIDAR over the **/scan** topic. The package is pre-installed on your **Robot**, but as always, trust, but verify. Open a new secure shell into your **Robot** and run the following: + +```bash +rospack find hls_lfcd_lds_driver +``` + +If installed, the command should return the absolute path to the package, similar to: + +```bash +/opt/ros/noetic/share/hls_lfcd_lds_driver +``` + +If the command instead returns an error, then you need to install the package. + +```bash +sudo apt install ros-noetic-hls-lfcd-lds-driver +``` + +## Testing LIDAR +Open a new terminal on the master and run roscore and setup for statistics: + +```bash +roscore +rosparam set enable_statistics true +``` + +Select the terminal with the secure shell connection to your **Robot** and open the `turtlebot3_lidar.launch` file: + +``` +rosed turtlebot3_bringup turtlebot3_lidar.launch +``` + +We can see that this launch file is pretty simple and only launches the **hls_laser_publisher** node. + +Run the launch file on the **Robot**: + +```bash +roslaunch turtlebot3_bringup turtlebot3_lidar.launch +``` + +In a new terminal on the **Master**, we can visualize the Turtlebot3 and LIDAR data using another launch file from the Turtlebot3: + +```bash +roslaunch turtlebot3_bringup turtlebot3_model.launch +``` + +This should open an RVIZ window where we can visualize ROS components of our system. In the "Displays" menu on the left you should see two submenus of interest: "LaserScan" and "RobotModel". These allow us to depict the Turtlebot3 and LIDAR data. + +You should see red dots fill the **rviz** map where obstacles exist as shown below. + +![logo](Figures/lidarscan_example.png) + + +Investigate what data the **hls_laser_publisher** is sending. Type the following and observe the command output: + +```bash +rostopic list +rostopic info /scan +rostopic type /scan +rostopic type /scan | rosmsg show +rostopic echo /scan +``` + +At this point you can kill all nodes on the master, but keep the **turtlebot3_lidar** launch file running on the **Robot**. + +## LIDAR Subscriber +In this section we will build a subscriber that will print the range data from the Turtlebot3 LIDAR. + +1. Browse to a terminal on the **Master** and create an `ice8` package: + ```bash + cd ~/master_ws/src/ece387_master_sp2X-USERNAME/master + catkin_create_pkg ice8 rospy sensor_msgs geometry_msgs turtlebot3_bringup + cd ~/master_ws + catkin_make + source ~/.bashrc + ``` + +1. Create an lidar node: + + ```bash + roscd ice8/src + touch lidar_sub.py + ``` + +1. Copy and complete the below code using the GUI editor tool, **Atom**. Browse to the subscriber you just created and double-click. This will open the file in **Atom** (if it is open in any other editor, stop, raise your hand, and get help from an instructor) +> 💡️ **Tip:** Look for the **"TODO"** tag which indicates where you should insert your own code. + +The code should obtain the list of range data from the LIDAR launch file running on the robot, convert the angles from 0 to 180 degrees and 0 to -180 degrees to 0 to 360 degrees. Lastly, the subscriber will print the average distance of obstacles 30 degrees off the nose of the robot. + +```python +#!/usr/bin/env python3 +import rospy, math +# TODO: import correct message + + +# lambda function to convert rad to deg +RAD2DEG = lambda x: ((x)*180./math.pi) +# convert LaserScan degree from -180 - 180 degs to 0 - 360 degs +DEG_CONV = lambda deg: deg + 360 if deg < 0 else deg + +class LIDAR: + """Class to read lidar data from the Turtlebot3 LIDAR""" + def __init__(self): + # TODO: create a subscriber to the scan topic published by the lidar launch file + + + self.ctrl_c = False + rospy.on_shutdown(self.shutdownhook) + + def callback_lidar(self, scan): + if not self.ctrl_c: + degrees = [] + ranges = [] + + # determine how many scans were taken during rotation + count = int(scan.scan_time / scan.time_increment) + + for i in range(count): + # using min angle and incr data determine curr angle, + # convert to degrees, convert to 360 scale + degrees.append(int(DEG_CONV(RAD2DEG(scan.angle_min + scan.angle_increment*i)))) + rng = scan.ranges[i] + + # ensure range values are valid; set to 0 if not + if rng < scan.range_min or rng > scan.range_max: + ranges.append(0.0) + else: + ranges.append(rng) + + # python way to iterate two lists at once! + for deg, rng in zip(degrees, ranges): + # TODO: sum and count the ranges 30 degrees off the nose of the robot + + + # TODO: ensure you don't divide by 0 and print average off the nose + + + def shutdownhook(self): + print("Shutting down lidar subscriber") + self.ctrl_c = True + +if __name__ == '__main__': + rospy.init_node('lidar_sub') + LIDAR() + rospy.spin() +``` + +4. Save, exit, and make the node executable. + +4. Open a new terminal on the **Master** and run the **lidar_sub.py** node. + +4. Rotate the **Robot** and observe the distance off the nose. + +## Checkpoint +Once complete, get checked off by an instructor showing the output of your **lidar_sub** and **rqt_graph** node. + +## Summary +In this lesson you learned how to integrate the LIDAR and get the distance of objects off the nose of the robot using the pre-built LIDAR package. In the lab that corresponds to this lesson you will apply this knowledge to stop the robot a specified distance from an obstacle and turn. + +## Cleanup +In each terminal window, close the node by typing `ctrl+c`. Exit any SSH connections. Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. + +**Ensure roscore is terminated before moving on to the lab.** diff --git a/_sources/Module8_LIDAR/Lab3_LIDAR.md b/_sources/Module8_LIDAR/Lab3_LIDAR.md old mode 100755 new mode 100644 index d25fad1..700c403 --- a/_sources/Module8_LIDAR/Lab3_LIDAR.md +++ b/_sources/Module8_LIDAR/Lab3_LIDAR.md @@ -1,68 +1,68 @@ -# Lab 3: LIDAR - -## Purpose -This lab will integrate the Turtlebot3 LIDAR with the existing controller to drive the robot forward and turn 90 degrees when there is an obstacle. - -## Master -### Setup: -In the `/master_ws/src/ece387_master_spring202X-USERNAME/` folder, create a **lab3** package which depends on **rospy**, **std_msgs**, **geometry_msgs**, and **turtlebot3_bringup**. - -Make and source your workspace. - -### controller.py -1. Copy the controller.py file from lab2 into the lab3 package. - -1. Open the controller.py file from lab3 using the **Atom** editor. - -1. Import the laser message used in ICE8. - -1. Copy the 2 lambda functions from ICE8 (RAD2DEG & DEG_CONV). - -1. Add the following Class variables within the class above the `__init__()` function: - - 1. `DISTANCE = 0.4 # distance from the wall to stop` - 1. `K_POS = 100 # proportional constant for slowly stopping as you get closer to the wall` - 1. `MIN_LIN_X = 0.05 # limit m/s values sent to Turtlebot3` - 1. `MAX_LIN_X = 0.2 # limit m/s values sent to Turtlebot3` - -1. Add the following to the `__init__()` function: - - 1. Instance variable, `self.avg_dist`, initialized to 0 to store the average dist off the nose. - 1. Instance variable, `self.got_avg`, initialized to False to store when an average is calculated. - 1. A subscriber to the LIDAR topic of interest with a callback to the callback_lidar() function. - -1. Add the `callback_lidar()` function from ICE8, removing print statements and setting the instance variables, `self.avg_dist` and `self.got_avg`. - -1. Edit the `callback_controller()` to accomplish the following: - - 1. Remove user input. - 1. When not turning and you have an average LIDAR reading, calculate the distance error (`actual dist` - `desired dist`) and use that to drive your robot straight at a proportional rate (very similar to how we calculated the turn rate in lab 2). - 1. Limit the linear speed of the robot to `MIN_LIN_X` and `MAX_LIN_X`. - 1. If within `DISTANCE` of a wall, then stop and start turning (left or right, you decide). - - > 💡️ **Tip:** You should be able to reuse a lot of code for this step! - - 1. Save the linear x and angular z values to the `Twist` message and publish. - -1. Save, exit, and make executable if necessary. - -## Create a launch file -1. Create a launch directory in your lab3 folder. -1. Copy the launch file from lab2 to lab3. -1. Open the **turtlebot3_lidar.launch** file from the *turtlebot3_bringup* package and copy the arguments and nodes to your lab3 launch file. -1. Add the machine tag to the lidar node. - -## Run your nodes -1. On the **Master**, open a terminal and run the **lab3.launch** file - -## Report -Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under `Resources/Lab Template`. - -> 📝️ **NOTE:** We will be primarily grading sections 3.3 System level design and 3.4 Testing for this lab, but do include the entire lab as you will need other components for the final project report. - -## Turn-in Requirements -**[25 points]** Demonstration of Turtlebot3 driving and not hitting a wall (preferably in person, but can be recorded and posted to Teams under the Lab3 channel). - -**[50 points]** Report via Gradescope. - -**[25 points]** Code: push your code to your repository. Also, include a screen shot of the **controller.py** file at the end of your report. +# Lab 3: LIDAR + +## Purpose +This lab will integrate the Turtlebot3 LIDAR with the existing controller to drive the robot forward and turn 90 degrees when there is an obstacle. + +## Master +### Setup: +In the `/master_ws/src/ece387_master_spring202X-USERNAME/` folder, create a **lab3** package which depends on **rospy**, **std_msgs**, **sensor_msgs**, **geometry_msgs**, and **turtlebot3_bringup**. + +Make and source your workspace. + +### controller.py +1. Copy the controller.py file from lab2 into the lab3 package. + +1. Open the controller.py file from lab3 using the **Atom** editor. + +1. Import the laser message used in ICE8. + +1. Copy the 2 lambda functions from ICE8 (RAD2DEG & DEG_CONV). + +1. Add the following Class variables within the class above the `__init__()` function: + + 1. `DISTANCE = 0.4 # distance from the wall to stop` + 1. `K_POS = 100 # proportional constant for slowly stopping as you get closer to the wall` + 1. `MIN_LIN_X = 0.05 # limit m/s values sent to Turtlebot3` + 1. `MAX_LIN_X = 0.2 # limit m/s values sent to Turtlebot3` + +1. Add the following to the `__init__()` function: + + 1. Instance variable, `self.avg_dist`, initialized to 0 to store the average dist off the nose. + 1. Instance variable, `self.got_avg`, initialized to False to store when an average is calculated. + 1. A subscriber to the LIDAR topic of interest with a callback to the callback_lidar() function. + +1. Add the `callback_lidar()` function from ICE8, removing print statements and setting the instance variables, `self.avg_dist` and `self.got_avg`. + +1. Edit the `callback_controller()` to accomplish the following: + + 1. Remove user input. + 1. When not turning and you have an average LIDAR reading, calculate the distance error (`actual dist` - `desired dist`) and use that to drive your robot straight at a proportional rate (very similar to how we calculated the turn rate in lab 2). + 1. Limit the linear speed of the robot to `MIN_LIN_X` and `MAX_LIN_X`. + 1. If within `DISTANCE` of a wall, then stop and start turning (left or right, you decide). + + > 💡️ **Tip:** You should be able to reuse a lot of code for this step! + + 1. Save the linear x and angular z values to the `Twist` message and publish. + +1. Save, exit, and make executable if necessary. + +## Create a launch file +1. Create a launch directory in your lab3 folder. +1. Copy the launch file from lab2 to lab3. +1. Open the **turtlebot3_lidar.launch** file from the *turtlebot3_bringup* package and copy the arguments and nodes to your lab3 launch file. +1. Add the machine tag to the lidar node. + +## Run your nodes +1. On the **Master**, open a terminal and run the **lab3.launch** file + +## Report +Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under `Resources/Lab Template`. + +> 📝️ **NOTE:** We will be primarily grading sections 3.3 System level design and 3.4 Testing for this lab, but do include the entire lab as you will need other components for the final project report. + +## Turn-in Requirements +**[25 points]** Demonstration of Turtlebot3 driving and not hitting a wall (preferably in person, but can be recorded and posted to Teams under the Lab3 channel). + +**[50 points]** Report via Gradescope. + +**[25 points]** Code: push your code to your repository. Also, include a screen shot of the **controller.py** file at the end of your report. diff --git a/_sources/Module9_CV/Lab4_ComputerVision.md b/_sources/Module9_CV/Lab4_ComputerVision.md old mode 100755 new mode 100644 index 7deae2a..acad58a --- a/_sources/Module9_CV/Lab4_ComputerVision.md +++ b/_sources/Module9_CV/Lab4_ComputerVision.md @@ -1,240 +1,240 @@ -# Lab 4: Computer Vision - - -## Purpose -This lab will integrate a USB Camera with the Robot. You will use a Python script to take pictures of the stop sign and build a stop sign detector then test it using a live video feed. You will then use the detector and known size of the stop sign to estimate how far the stop sign is from the camera. Lastly, you will create a node to identify and determine how far an April Tag is from the robot. - -## Setup packages -Open a terminal on the **Master** and create a lab4 package: - -```bash -cd ~/master_ws/src/ece387_master_spring202X-USERNAME/ -catkin_create_pkg lab4 rospy sensor_msgs std_msgs cv_bridge apriltag_ros -``` - -Make and source your workspace. - -If you have not already done so, repeat on the **Robot** - -## Create a ROS node to save images -Browse to your lab4 source folder on the **Master** and create a node called **image_capture.py**. - -```python -#!/usr/bin/env python3 -import rospy, cv2, argparse -from sensor_msgs.msg import Image -from cv_bridge import CvBridge, CvBridgeError - -class SavingImage(object): - def __init__(self, img_dest): - self.img_dest = img_dest - self.ctrl_c = False - self.count = 0 - - # subscribe to the topic created by the usb_cam node - self.image_sub = rospy.Subscriber("/usb_cam/image_raw",Image,self.camera_callback) - - # CV bridge converts between ROS Image messages and OpenCV images - self.bridge_object = CvBridge() - - # callback to save images when user presses button - rospy.Timer(rospy.Duration(.1), self.callback_save) - - rospy.on_shutdown(self.shutdownhook) - - def camera_callback(self,img): - if not self.ctrl_c: - try: - # convert ROS image to OpenCV image - self.cv_image = self.bridge_object.imgmsg_to_cv2(img, desired_encoding="bgr8") - except CvBridgeError as e: - print(e) - - # show the image (waitKey(1) allows for automatic refressh creating video) - cv2.imshow('image', self.cv_image) - cv2.waitKey(1) - - def callback_save(self, event): - # when user is ready to take picture press button - _ = input("Press enter to save the next image.") - dest = self.img_dest + "img" + str(self.count) + ".jpg" - self.count += 1 - print(dest) - try: - # write to file - cv2.imwrite(dest, self.cv_image) - except: - print("Not valid image name. Try restarting with valid path.") - - def shutdownhook(self): - print("Shutting down") - cv2.destroyAllWindows() - -if __name__ == '__main__': - rospy.init_node('image_saver', anonymous=True) - ap = argparse.ArgumentParser() - ap.add_argument("-o", "--output", required=True, help="path to output img") - args = vars(ap.parse_args()) - saving_image_object = SavingImage(args["output"]) - try: - rospy.spin() - except KeyboardInterrupt: - pass -``` - -Save, exit, and make executable. - -## Train your stop detector - -Create a new folder in your **lab4** package called **training_images**. - -Run the `image_capture.py` node on the **Master** using the following command: - -```bash -rosrun lab4 image_capture.py -o /home/dfec/master_ws/src/ece387_master_spring202X-NAME/lab4/training_images/ -``` - -Store images of the stop sign by pressing `enter` when prompted. You decide how many and at what orientations to properly train your detector. When complete, hit `ctrl+c` to exit. - -Utilize the steps from Module 9: [Building a detector using HOG features](CV:HOG) to label your images and train your object detector using the new images, saving the `stop_detector.svm` file within the **training_images** folder. - -## Test your stop detector -Create a node in the **lab4** package on the **Master** called `stop_detector.py` and copy the below into it: - -```python -#!/usr/bin/env python3 -import rospy, cv2, dlib -from cv_bridge import CvBridge, CvBridgeError - -# TODO: import usb_cam message type - - -class StopDetector(object): - - def __init__(self, detectorLoc): - self.ctrl_c = False - - #TODO: create subscriber to usb_cam image topic - - - self.bridge_object = CvBridge() - self.detector = dlib.simple_object_detector(detectorLoc) - - rospy.on_shutdown(self.shutdownhook) - - def camera_callback(self,data): - if not self.ctrl_c: - #TODO: write code to get ROS image, convert to OpenCV image, - # apply detector, add boxes to image, and display image - - - def shutdownhook(self): - print("Shutting down") - self.ctrl_c = True - cv2.destroyAllWindows() - -if __name__ == '__main__': - rospy.init_node('stop_detector') - detector = rospy.get_param("/stop_detector/detector") - stop_detector = StopDetector(detector) - try: - rospy.spin() - except KeyboardInterrupt: - pass -``` - -Edit the `stop_detector.py` node so it utilizes the `camera_callback()` function we used above to get images from the camera. - -After getting the `cv_image` within the `camera_callback()`, apply the detector in a similar method as Module 9: [Testing a detector](CV:HOG) creating boxes around all detected stop signs. Using a `waitKey(1)` will allow for the image to refresh automatically without user input and display the video. - -## Checkpoint 1 -Demonstrate the stop detector on the **Master** detecting a stop sign from the **Robot's** camera. - -```bash -rosrun lab4 stop_detector.py _detector:=/home/dfec/master_ws/src/ece387_master_spring202X-NAME/lab4/training_images/stop_detector.svm -``` - ->📝️ **Note:** You must have the `lab4.launch` file running. - -## Move detector to robot -Copy the detector and node to the robot: - -```bash -roscd lab4/training_images -scp stop_detector.svm pi@robotX:/home/pi/robot_ws/src/ece387_robot_spring202X-NAME/lab4/training_images/stop_detector.svm -roscd lab4/src -scp stop_detector.py pi@robotX:/home/pi/robot_ws/src/ece387_robot_spring202X-NAME/lab4/src/stop_detector.py -``` - -Remove the lines that display the video and instead print "Stop detected" if `boxes` is not empty. - -Do you note a difference in processing speed? - -## Launch file -Edit the `lab4.launch` file so it will run the stop detector node with the `detector` param set to the location of the detector. For example: - -```xml - - - -``` - -## Checkpoint 2 -Demonstrate the stop detector on the **Robot** detecting a stop sign. - -## Determine distance from stop sign - -### Edit `stop_detector.py` - -You will edit your stop sign detector on the **Robot** to calculate an estimated distance between the camera and the stop sign using triangle similarity. - -Given a stop sign with a known width, $W$, we can place the stop sign at a known distance, $D$, from our camera. The detector will then detect the stop sign and provide a perceived width in pixels, $P$. Using these values we can calculate the focal length, $F$ of our camera: - -$F = \frac{(P\times D)}{W}$ - -We can then use the calculated focal length, $F$, known width, $W$, and perceived width in pixels, $P$ to calculate the distance from the camera: - -$D' = \frac{(W\times F)}{P}$ - -Use the above information and create two class variables, `FOCAL` and `STOP_WIDTH`, and a class function to calculate distance given a known `FOCAL` length and a known width of the stop sign, `STOP_WIDTH`. You will need to print the perceived width of the stop sign to determine the $P$ value used in the calculation to find the focal length. - -> 💡️ **Tip:** Pay attention to what the `x` and `w` variables of the `box` actually represent! - -Create a new publisher that will publish the distance using **Float32** *std_msgs* messages over the */stop_dist* topic. - -Publish the distance of each object seen in the image. - -Remove any print statements after troubleshooting! - -## Checkpoint 3 -Demonstrate the **stop_detector** node publishing distance from the stop sign. - -## Printing April Tag information - -Create a node on the master in lab4 called `apriltag_dist.py`. Import the appropriate AprilTag message. Subscribe to the `tag_detections` topic. Print the identified AprilTag ID and distance. If the camera sees multiple tags, it should print the information for each tag. - -In your callback function you will want to create a for loop such as: - -```python -for tag in data.detections: -``` - -Use print statements to determine the characteristics of the message (you can also google the message). - -Add the `apriltag_dist` node to the **lab4** launch file. - -## Checkpoint 4 - -Demonstrate the `apriltag_dist` node printing the ID and distance of each April Tag. - -## Report -Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under `Resources/Lab Template`. - -> 📝️ **Note:** We will be primarily grading sections 3.1, 3.2, and 3.3 for this lab, but do include the entire lab as you will need other components for the final project report. - -## Turn-in Requirements -**[25 points]** All checkpoints marked off. - -**[50 points]** Report via Gradescope. - -**[25 points]** Code: push your code to your repository. Also, include a screen shot of the **apriltag_dist.py** and **stop_detector.py** files at the end of your report. +# Lab 4: Computer Vision + + +## Purpose +This lab will integrate a USB Camera with the Robot. You will use a Python script to take pictures of the stop sign and build a stop sign detector then test it using a live video feed. You will then use the detector and known size of the stop sign to estimate how far the stop sign is from the camera. Lastly, you will create a node to identify and determine how far an April Tag is from the robot. + +## Setup packages +Open a terminal on the **Master** and create a lab4 package: + +```bash +cd ~/master_ws/src/ece387_master_spring202X-USERNAME/ +catkin_create_pkg lab4 rospy sensor_msgs std_msgs cv_bridge apriltag_ros +``` + +Make and source your workspace. + +If you have not already done so, repeat on the **Robot** + +## Create a ROS node to save images +Browse to your lab4 source folder on the **Master** and create a node called **image_capture.py**. + +```python +#!/usr/bin/env python3 +import rospy, cv2, argparse +from sensor_msgs.msg import Image +from cv_bridge import CvBridge, CvBridgeError + +class SavingImage(object): + def __init__(self, img_dest): + self.img_dest = img_dest + self.ctrl_c = False + self.count = 0 + + # subscribe to the topic created by the usb_cam node + self.image_sub = rospy.Subscriber("/usb_cam/image_raw",Image,self.camera_callback) + + # CV bridge converts between ROS Image messages and OpenCV images + self.bridge_object = CvBridge() + + # callback to save images when user presses button + rospy.Timer(rospy.Duration(.1), self.callback_save) + + rospy.on_shutdown(self.shutdownhook) + + def camera_callback(self,img): + if not self.ctrl_c: + try: + # convert ROS image to OpenCV image + self.cv_image = self.bridge_object.imgmsg_to_cv2(img, desired_encoding="bgr8") + except CvBridgeError as e: + print(e) + + # show the image (waitKey(1) allows for automatic refressh creating video) + cv2.imshow('image', self.cv_image) + cv2.waitKey(1) + + def callback_save(self, event): + # when user is ready to take picture press button + _ = input("Press enter to save the next image.") + dest = self.img_dest + "img" + str(self.count) + ".jpg" + self.count += 1 + print(dest) + try: + # write to file + cv2.imwrite(dest, self.cv_image) + except: + print("Not valid image name. Try restarting with valid path.") + + def shutdownhook(self): + print("Shutting down") + cv2.destroyAllWindows() + +if __name__ == '__main__': + rospy.init_node('image_saver', anonymous=True) + ap = argparse.ArgumentParser() + ap.add_argument("-o", "--output", required=True, help="path to output img") + args = vars(ap.parse_args()) + saving_image_object = SavingImage(args["output"]) + try: + rospy.spin() + except KeyboardInterrupt: + pass +``` + +Save, exit, and make executable. + +## Train your stop detector + +Create a new folder in your **lab4** package called **training_images**. + +Run the `image_capture.py` node on the **Master** using the following command: + +```bash +rosrun lab4 image_capture.py -o /home/dfec/master_ws/src/ece387_master_spring202X-NAME/lab4/training_images/ +``` + +Store images of the stop sign by pressing `enter` when prompted. You decide how many and at what orientations to properly train your detector. When complete, hit `ctrl+c` to exit. + +Utilize the steps from Module 9: [Building a detector using HOG features](CV:HOG) to label your images and train your object detector using the new images, saving the `stop_detector.svm` file within the **training_images** folder. + +## Test your stop detector +Create a node in the **lab4** package on the **Master** called `stop_detector.py` and copy the below into it: + +```python +#!/usr/bin/env python3 +import rospy, cv2, dlib +from cv_bridge import CvBridge, CvBridgeError + +# TODO: import usb_cam message type + + +class StopDetector(object): + + def __init__(self, detectorLoc): + self.ctrl_c = False + + #TODO: create subscriber to usb_cam image topic + + + self.bridge_object = CvBridge() + self.detector = dlib.simple_object_detector(detectorLoc) + + rospy.on_shutdown(self.shutdownhook) + + def camera_callback(self,data): + if not self.ctrl_c: + #TODO: write code to get ROS image, convert to OpenCV image, + # apply detector, add boxes to image, and display image + + + def shutdownhook(self): + print("Shutting down") + self.ctrl_c = True + cv2.destroyAllWindows() + +if __name__ == '__main__': + rospy.init_node('stop_detector') + detector = rospy.get_param("/stop_detector/detector") + stop_detector = StopDetector(detector) + try: + rospy.spin() + except KeyboardInterrupt: + pass +``` + +Edit the `stop_detector.py` node so it utilizes the `camera_callback()` function we used above to get images from the camera. + +After getting the `cv_image` within the `camera_callback()`, apply the detector in a similar method as Module 9: [Testing a detector](CV:HOG) creating boxes around all detected stop signs. Using a `waitKey(1)` will allow for the image to refresh automatically without user input and display the video. + +## Checkpoint 1 +Demonstrate the stop detector on the **Master** detecting a stop sign from the **Robot's** camera. + +```bash +rosrun lab4 stop_detector.py _detector:=/home/dfec/master_ws/src/ece387_master_spring202X-NAME/lab4/training_images/stop_detector.svm +``` + +>📝️ **Note:** You must have the `lab4.launch` file running. + +## Move detector to robot +Copy the detector and node to the robot: + +```bash +roscd lab4/training_images +scp stop_detector.svm pi@robotX:/home/pi/robot_ws/src/ece387_robot_spring202X-NAME/lab4/training_images/stop_detector.svm +roscd lab4/src +scp stop_detector.py pi@robotX:/home/pi/robot_ws/src/ece387_robot_spring202X-NAME/lab4/src/stop_detector.py +``` + +Remove the lines that display the video and instead print "Stop detected" if `boxes` is not empty. + +Do you note a difference in processing speed? + +## Launch file +Edit the `lab4.launch` file so it will run the stop detector node with the `detector` param set to the location of the detector. For example: + +```xml + + + +``` + +## Checkpoint 2 +Demonstrate the stop detector on the **Robot** detecting a stop sign. + +## Determine distance from stop sign + +### Edit `stop_detector.py` + +You will edit your stop sign detector on the **Robot** to calculate an estimated distance between the camera and the stop sign using triangle similarity. + +Given a stop sign with a known width, $W$, we can place the stop sign at a known distance, $D$, from our camera. The detector will then detect the stop sign and provide a perceived width in pixels, $P$. Using these values we can calculate the focal length, $F$ of our camera: + +$F = \frac{(P\times D)}{W}$ + +We can then use the calculated focal length, $F$, known width, $W$, and perceived width in pixels, $P$ to calculate the distance from the camera: + +$D' = \frac{(W\times F)}{P}$ + +Use the above information and create two class variables, `FOCAL` and `STOP_WIDTH`, and a class function to calculate distance given a known `FOCAL` length and a known width of the stop sign, `STOP_WIDTH`. You will need to print the perceived width of the stop sign to determine the $P$ value used in the calculation to find the focal length. + +> 💡️ **Tip:** Pay attention to what the `x` and `w` variables of the `box` actually represent! + +Create a new publisher that will publish the distance using **Float32** *std_msgs* messages over the */stop_dist* topic. + +Publish the distance of each object seen in the image. + +Remove any print statements after troubleshooting! + +## Checkpoint 3 +Demonstrate the **stop_detector** node publishing distance from the stop sign. + +## Printing April Tag information + +Create a node on the master in lab4 called `apriltag_dist.py`. Import the appropriate AprilTag message. Subscribe to the `tag_detections` topic. Print the identified AprilTag ID and distance. If the camera sees multiple tags, it should print the information for each tag. + +In your callback function you will want to create a for loop such as: + +```python +for tag in data.detections: +``` + +Use print statements to determine the characteristics of the message (you can also google the message). + +Add the `apriltag_dist` node to the **lab4** launch file. + +## Checkpoint 4 + +Demonstrate the `apriltag_dist` node printing the ID and distance of each April Tag. + +## Report +Complete a short 2-3 page report that utilizes the format and answers the questions within the report template. The report template and an example report can be found within the Team under `Resources/Lab Template`. + +> 📝️ **Note:** We will be primarily grading sections 3.1, 3.2, and 3.3 for this lab, but do include the entire lab as you will need other components for the final project report. + +## Turn-in Requirements +**[25 points]** All checkpoints marked off. + +**[50 points]** Report via Gradescope. + +**[25 points]** Code: push your code to your repository. Also, include a screen shot of the **apriltag_dist.py** and **stop_detector.py** files at the end of your report. diff --git a/_sources/Module9_CV/computer_vision.md b/_sources/Module9_CV/computer_vision.md old mode 100755 new mode 100644 index 3ee6df0..19a2c70 --- a/_sources/Module9_CV/computer_vision.md +++ b/_sources/Module9_CV/computer_vision.md @@ -1,689 +1,689 @@ -# Computer Vision - -## Part 1: Image Basics -When we talk about the sizes of images, we generally talk about them in terms of the number of pixels the image possesses in the x(horizontal) or y(vertical) direction. If the image is a color image, we also need to concern ourselves with the depth of the image as well. Normally, each individual pixel is represented by the “color” or the “intensity” of light that appears in a given place in our image. - -If we think of an image as a grid, each square in the grid contains a single pixel. - -Most pixels are represented in two ways: grayscale and color. In a grayscale image, each pixel has a value between 0 and 255, where zero is corresponds to “black” and 255 being “white”. The values in between 0 and 255 are varying shades of gray, where values closer to 0 are darker and values closer 255 are lighter: - -![logo](Figures/Grayscale.JPG) - -The grayscale gradient image in the figure above demonstrates darker pixels on the left-hand side and progressively lighter pixels on the right-hand side. - -Color pixels, however, are normally represented in the RGB color space (this is where the term color-depth comes from)— one value for the Red component, one for Green, and one for Blue, leading to a total of 3 values per pixel: - -![logo](Figures/RGB.JPG) - -Other color spaces exist, and ordering of the colors may differ as well, but let’s start with the common RGB system. If we say the image is a 24-bit image, each of the three Red, Green, and Blue colors are represented by an integer in the range 0 to 255 (8-bits), which indicates how “much” of the color there is. Given that the pixel value only needs to be in the range [0, 255] we normally use an 8-bit unsigned integer to represent each color intensity. We then combine these values into a RGB tuple in the form (red, green, blue) . This tuple represents our color. For example: - -- To construct a white color, we would fill each of the red, green, and blue buckets completely up, like this: (255, 255, 255) — since white is the presence of all color. -- Then, to create a black color, we would empty each of the buckets out: (0, 0, 0) — since black is the absence of color. -- To create a pure red color, we would fill up the red bucket (and only the red bucket) up completely: (255, 0, 0) . -- etc - -Take a look at the following image to make this concept more clear: - - -![logo](Figures/RGB_Tuple.JPG) - -For your reference, here are some common colors represented as RGB tuples: - -- Black: (0, 0, 0) -- White: (255, 255, 255) -- Red: (255, 0, 0) -- Green: (0, 255, 0) -- Blue: (0, 0, 255) -- Aqua: (0, 255, 255) -- Fuchsia: (255, 0, 255) -- Maroon: (128, 0, 0) -- Navy: (0, 0, 128) -- Olive: (128, 128, 0) -- Purple: (128, 0, 128) -- Teal: (0, 128, 128) -- Yellow: (255, 255, 0) - -## Part 2: Coding with OpenCV-Python -It is time to build our first bit of code working with OpenCV. Just like ROS, OpenCV is well supported by both Python and C++. For simplicity, we will use Python throughout this course. However, continue to recognize that if speed and efficiency become important, switching to a more robust language like C++ may become necessary. To make use of OpenCV with Python, we need to import cv2. The code below will simply load in the RGB figure above and print out the pixel values in each of the 4-quadrants. - -First we need to import the OpenCV Python library, `cv2`: - - -```python -import cv2 -``` - -Then we can load the image: - - -```python -image = cv2.imread("RGB_Tuple.JPG") -``` - -The `shape` characteristic of the image returns a tuple of the number of rows, columns, and channels (if the image is color): - - -```python -print("width: %d pixels" %(image.shape[1])) -print("height: %d pixels" % (image.shape[0])) -print("color channels: %d" % (image.shape[2])) -``` - -You an also access specific pixels within the image (the `image` variable is really just an array of pixel values) by the row and column coordinates. Each pixel values is an array of Blue, Green, and Red values. - - -```python -# print the BGR values of a pixel in the upper left of the image -print(image[10, 10, :]) - -# print the red value of a pixel in the bottom left of the image -print(image[700, 100, 2]) -``` - -Fill in the code to do the following: - - -```python -# TODO: print BGR values of a pixel in the upper right of the image -print(image[ , , :]) - -# TODO: print BGR values of a pixel in the lower left of the image -print(image[ , , :]) - -# TODO: print blue value of a pixel in the lower right of the image -print(image[ , , ]) -``` - -We can display the image as well. -> ⚠️ **WARNING:** To exit the image just press any key. **DO NOT** press the 'X' in the corner. If you do press the 'X' (smh) you will have to Restart & Clear the Kernel: in the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". - - -```python -cv2.imshow("Loaded image", image) -cv2.waitKey(0) -cv2.destroyAllWindows() # close the image window -``` - -When executing the code above, there were two minor surprises. What do you think they were? Now lets take a look at additional functionality embedded within OpenCV. - -Convert image to RGB and print the same pixel values. Remember that the image is already loaded within the `image` variable. - - -```python -# TODO: Convert image to RGB - - -# TODO: print the RGB values of a pixel in the upper left of the image - - -# TODO: print the red value of a pixel in the bottom left of the image - - -# TODO: print RGB values of a pixel in the upper right of the image - - -# TODO: print RGB values of a pixel in the lower left of the image - - -# TODO: print blue value of a pixel in the lower right of the image - -``` - -Modify the code to convert to grayscale and print the same pixel values. - - -```python -# TODO: Convert image to Grayscale - - -# TODO: print the Grayscale values of a pixel in the upper left of the image - - -# TODO: print Grayscale values of a pixel in the upper right of the image - - -# TODO: print Grayscale values of a pixel in the lower left of the image - -``` - -### Summary -These examples barely scratch the surface of what is possible with OpenCV. In the upcoming lessons we will learn a few more ways to manipulate images, but if you want to learn more you can either explore the [OpenCV-Python Source Documentation](https://docs.opencv.org/3.4/index.html) or the [OpenCV-Python Tutorial](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html). - -### Assignment -Scan the article on the [Histogram of Oriented Gradients (HOG)](https://arxiv.org/pdf/1406.2419.pdf) feature descriptor and be prepared to discuss. I don't need you to understand the math, but you should be able to understand the advantages of the technique. - -### Cleanup -In the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. - -## Part 3: Gradients - -The objective of this portion of the lesson is for you to start the process of learning how to create custom object detectors in an image. There are many techniques, but the one technique I am interested in applying first is what is known as Histogram of Oriented Gradients. Before we can dig into the technique, we should first understand a bit about image gradients and contours. - -By the end of today's lesson you will be able to: -- Define what an image gradient is -- Compute changes in direction of an input image -- Define both gradient magnitude and gradient orientation -- Use OpenCV to approximate image gradients - -The image gradient is one of the fundamental building blocks in computer vision image processing. - -We use gradients for detecting edges in images, which allows us to find contours and outlines of objects in images. We use them as inputs for quantifying images through feature extraction — in fact, highly successful and well-known image descriptors such as Histogram of Oriented Gradients and SIFT are built upon image gradient representations. Gradient images are even used to construct saliency maps, which highlight the subjects of an image. We use gradients all the time in computer vision and image processing. I would go as far as to say they are one of the most important building blocks you will learn about in this module. While they are not often discussed in detail since other more powerful and interesting methods build on top of them, we are going to take the time and discuss them in detail. - -As I mentioned in the introduction, image gradients are used as the basic building blocks in many computer vision and image processing applications. However, the main application of image gradients lies within edge detection. As the name suggests, edge detection is the process of finding edges in an image, which reveals structural information regarding the objects in an image. Edges could therefore correspond to: - -- Boundaries of an object in an image. -- Boundaries of shadowing or lighting conditions in an image. -- Boundaries of “parts” within an object. - -As we mentioned in the previous portion of the lab, we will often work with grayscale images, because of the massive reduction in images. OpenCV will convert to grayscale using the following conversion formula: - -$Y = 0.299 R + 0.587 G + 0.114 B$ - -Let's see if that matches our expectations in the figure below: - -![logo](Figures/RGB_Gray.png) - -The below figure is an image of edges being detected simply by looking for the contours in an image: - -![logo](Figures/EdgeDet.png) - -As you can see, all of the edges (or changes in contrast are clearly identified), but how did we do it? Lets look at the math below, and then we will look at how simple the code is by taking advantage of OpenCV. - -Formally, an image gradient is defined as a directional change in image intensity. Or put more simply, at each pixel of the input (grayscale) image, a gradient measures the change in pixel intensity in a given direction. By estimating the direction or orientation along with the magnitude (i.e. how strong the change in direction is), we are able to detect regions of an image that look like edges. - -Lets look at a blown up version of a basic pixel map. Our goal here is to establish the basic framework for how we will eventually compute the gradient: - -![logo](Figures/PixelCont.png) - -In the image above we essentially wish to examine the \(3 \times 3\) neighborhood surrounding the central pixel. Our x values run from left to right, and our y values from top to bottom. In order to compute any changes in direction we will need the north, south, east, and west pixels, which are marked on the above figure. - -If we denote our input image as *I*, then we define the north, south, east, and west pixels using the following notation: - -- North: $I(x, y - 1)$ -- South: $I(x, y + 1)$ -- East: $I(x + 1, y)$ -- West: $I(x - 1, y)$ - -Again, these four values are critical in computing the changes in image intensity in both the x and y direction. - -To demonstrate this, let us compute the vertical change or the y-change by taking the difference between the south and north pixels: - -$G_{y} = I(x, y + 1) - I(x, y - 1)$ - -Similarly, we can compute the horizontal change or the x-change by taking the difference between the east and west pixels: - -$G_{x} = I(x + 1, y) - I(x - 1, y)$ - -Awesome — so now we have $G_{x}$ and $G_{y}$, which represent the change in image intensity for the central pixel in both the x and y direction. Lets look at a relatively intuitive example at first without all the math. - -![logo](Figures/GradientEx.png) - -On the left we have a $3 \times 3$ region of an image where the top half of the image is white and the bottom half of the image is black. The gradient orientation is thus equal to $\theta=-90^{\circ}$ - -And on the right we have another $3 \times 3$ neighborhood of an image, where the upper triangular region is white and the lower triangular region is black. Here we can see the change in direction is equal to $\theta=-45^{\circ}$. While these two examples are both relatively easy to understand, lets use our knowledge of the Pythagorean theorem to actually compute the magnitude and orientation of the gradient with actual values now. - -![logo](Figures/GradTrig.png) - -Inspecting the triangle in the above figure you can see that the gradient magnitude G is the hypotenuse of the triangle. Therefore, all we need to do is apply the Pythagorean theorem and we will end up with the gradient magnitude: - -$G = \sqrt{G_{x}^{2} + G_{y}^{2}}$ - -The gradient orientation can then be given as the ratio of $G_{y}$ to $G_{x}$. We can use the $tan^{-1}$ to compute the gradient orientation, - -$\theta = tan^{-1}(\frac{G_{y}}{G_{x}}) \times (\frac{180}{\pi})$ - -We converted to degrees by multiplying by the ratio of $180/\pi$. Lets now add pixel intensity values and put this to the test. - -![logo](Figures/GradEx2.png) - -In the above image we have an image where the upper-third is white and the bottom two-thirds is black. Using the equations for $G_{x}$ and $G_{y}$, we arrive at: - -$G_{x} = $ - -and - -$G_{y} = $ - -Plugging these values into our gradient magnitude equation we get: - -$G = $ - -As for our gradient orientation: - -$\theta = $ - -Now you try with the following example: - -![logo](Figures/GradEx3.png) - -$G_{x} = $ - -and - -$G_{y} = $ - -Plugging these values into our gradient magnitude equation we get: - -$G = $ - -As for our gradient orientation: - -$\theta = $ - -Now that you know how to compute both the orientation and the magnitude of the gradients, you essentially have the most basic building block established for computing the necessary information for HOG w/ SVM. Additionally, you can use the following code to compute very effective contours in images. - -Fortunately, in practice we don't need to do any of the math above. Instead we can use what is known as the Sobel Kernel to compute the values for $G_{x}$ and $G_{y}$. OpenCV and numpy have functionality built in that allow us to do all of this very quickly. - -> To exit the image just press any key. **DO NOT** press the 'X' in the corner. If you do press the 'X' (smh) you will have to Restart & Clear the Kernel: in the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". - - -```python -import cv2 -import numpy as np -``` - - -```python -#load the image -image=cv2.imread("RGB_Tuple.JPG") -gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) -``` - - -```python -#Show the original image along with the grayscale image -cv2.imshow("Original image", image) -cv2.imshow("Grayscale Image", gray) - -# Lets now compute the gradients along the X and Y axis, respectively -gX = cv2.Sobel(gray,cv2.CV_64F,1,0) -gY = cv2.Sobel(gray,cv2.CV_64F,0,1) - -# the `gX` and `gY` images are now of the floating point data type, -# so we need to take care to convert them back to an unsigned 8-bit -# integer representation so other OpenCV functions can utilize them -gX = cv2.convertScaleAbs(gX) -gY = cv2.convertScaleAbs(gY) - -# combine the sobel X and Y representations into a single image -sobelCombined = cv2.addWeighted(gX, 0.5, gY, 0.5, 0) -cv2.imshow("Gradient Image", sobelCombined) -cv2.waitKey(0) -cv2.destroyAllWindows() # close the image window -``` - -### Summary -Gradients are one important tool used in object detection. Next lesson we will learn how to apply gradients using the Histogram of Oriented Gradients to train an object detector. - -### Assignment -Watch the following video on [Histogram of Oriented Gradients](https://youtube.com/watch?v=4ESLTAd3IOM). - -### Cleanup -In the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. - -## Part 4: Histogram of Oriented Gradients (HOG) Features - -The objective of this portion of the lesson is to demonstrate the functionality of the HOG with SVM (Support Vector Machine) algorithm for object detection. By this point, we should all be well aware of what a histogram is. The application of the histogram for the HOG feature extraction is to further simplify the tested image to enable our computer to rapidly and accurately identify the presence of an object within the image. -Instead of using each individual gradient direction of each individual pixel of an image, we group the pixels into small cells. For each cell, we compute all the gradient directions and group them into a number of orientation bins. We sum up the gradient magnitude in each sample. So stronger gradients contribute more weight to their bins, and effects of small random orientations due to noise is reduced. Doing this for all cells gives us a representation of the structure of the image. The HOG features keep the representation of an object distinct but also allow for some variations in shape. For example, lets consider an object detector for a car, see the below figure. - -![logo](Figures/HOG_Features.JPG) - -Comparing each individual pixel of this training image with another test image would not only be time consuming, but it would also be highly subject to noise. As previously mentioned, the HOG feature will consider a block of pixels. The size of this block is variable and will naturally impact both accuracy and speed of execution for the algorithm. Once the block size is determined, the gradient for each pixel within the block is computed. Once the gradients are computed for a block, the entire cell can then be represented by this histogram. Not only does this reduce the amount of data to compare with a test image, but it also reduces the impacts of noise in the image and measurements. - -![logo](Figures/HOG_Histogram.JPG) - -Now that we have an understanding of the HOG features, lets use tools embedded within OpenCV and Dlib to build our first detector for a stop sign. But first we need to download a pre-created repository of test and training data. Remember, we won't use our training data to test the effectiveness of the algorithm. Of course the algorithm will work effectively on the training data. Our hope is that we can create a large enough sampling of test data that we can have a highly effective detector that is robust against new images. - -(CV:HOG)= -### Building a detector using HOG features -Download the example demo into the `my_scripts` folder you created earlier in the semester. It should be located under `~/master_ws/src/ece387_curriculum/`. - -```bash -roscd ece387_curriculum/my_scripts -git clone git@github.com:ECE495/HOG_Demo.git -cd HOG_Demo -``` - -Take a look at what is contained within the repo. Essentially you have both a training data folder and a test folder. We will now use a tool called **imglab** to annotate the images for building our detector. - -Browse to the [imglab tool](https://imglab.in/) and select **"UMM, MAYBE NEXT TIME!"**. - -In the bottom left of the site, select the load button and browse to the training folder: - -![logo](Figures/load.png) - -Select the first stop sign and the **"Rectangle"** tool. - -![logo](Figures/rectangle.png) - -Highlight the border of the stop sign: drag-and-draw a bounding rectangle, ensuring to **only** select the stop sign and to select all examples of the object in the image. - -> 📝️ **NOTE:** It is important to label all examples of objects in an image; otherwise, Dlib will implicitly assume that regions not labeled are regions that should not be detected (i.e., hard-negative mining applied during extraction time). - -You can select a bounding box and hit the delete key to remove it. - -If you press `alt+left/right arrow` you can navigate through images in the slider and repeat highlighting the objects. - -Once all stop signs are complete hit `ctrl+e` to save the annotations (bounding box information) as a **"Dlib XML"** file within the `training` folder using a descriptive name such as `stop_annotations.xml`. - -![logo](Figures/xml.png) - -We now need to create the code to build the detector based on our annotated training data. - -```bash -cd ~/master_ws/src/ece387_curriculum/my_scripts/HOG_Demo -touch trainDetector.py -``` - -Now open this in your favorite editor to add the following code. I have built into the code the ability to provide command line arguments. This will make the code a bit more flexible such that you don't need to recreate it in the future if you want to reuse if for another project. You will provide two arguments at runtime. First you need to tell the program where the .xml file is. Second, you will state where you want to put the detector that you create... the detector should have a .svm extension. - -```python -# import the necessary packages -from __future__ import print_function -import argparse -import dlib - -# construct the argument parser and parse the arguments -ap = argparse.ArgumentParser() -ap.add_argument("-x", "--xml", required=True, help="path to input XML file") -ap.add_argument("-d", "--detector", required=True, help="path to output detector") -args = vars(ap.parse_args()) - -# grab the default training options for the HOG + Linear SVM detector, then -# train the detector -- in practice, the `C` parameter can be adjusted... -# feel free to research and see if you can improve -print("[INFO] training detector...") -options = dlib.simple_object_detector_training_options() -options.C = 1.0 -options.num_threads = 4 -options.be_verbose = True -dlib.train_simple_object_detector(args["xml"], args["detector"], options) - -# show the training accuracy -print("[INFO] training accuracy: {}".format( - dlib.test_simple_object_detector(args["xml"], args["detector"]))) - -# load the detector and visualize the HOG filter -detector = dlib.simple_object_detector(args["detector"]) -win = dlib.image_window() -win.set_image(detector) -dlib.hit_enter_to_continue() -``` - -Once you have the code entered, you can run it with the following command. Remember, you need to provide two command line arguments: - -```bash -roscd ece387_curriculum/my_scripts/HOG_Demo -python3 trainDetector.py --xml training/stop_annotations.xml --detector training/stop_detector.svm -``` - -You may get a few errors pop up during execution based on your choice for bounding boxes. Make sure you address those errors before continuing. If everything executed correctly, you should ultimately see a picture of the HOG feature you designed. - -### Testing a detector -Now it is time to build our code to test the detector. The following code will make use of the imutils library as well. - -You may get a few errors pop up during execution based on your choice for bounding boxes. Make sure you address those errors before continuing. If everything executed correctly, you should ultimately see a picture of the HOG feature you designed. - -Now it is time to build our code to test the detector. - -```bash -roscd ece387_curriculum/my_scripts/HOG_Demo -touch testDetector.py -``` - -Again, use your preferred editor to enter the code below: - -```python -# import the necessary packages -from imutils import paths -import argparse -import dlib -import cv2 - -# construct the argument parser and parse the arguments -ap = argparse.ArgumentParser() -ap.add_argument("-d", "--detector", required=True, help="Path to trained object detector") -ap.add_argument("-t", "--testing", required=True, help="Path to directory of testing images") -args = vars(ap.parse_args()) - -# load the detector -detector = dlib.simple_object_detector(args["detector"]) - -# loop over the testing images -for testingPath in paths.list_images(args["testing"]): - # load the image and make predictions - image = cv2.imread(testingPath) - boxes = detector(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) - - # loop over the bounding boxes and draw them - for b in boxes: - (x, y, w, h) = (b.left(), b.top(), b.right(), b.bottom()) - cv2.rectangle(image, (x, y), (w, h), (0, 255, 0), 2) - - # show the image - cv2.imshow("Image", image) - cv2.waitKey(0) -``` - -Run the test detector: - -```bash -roscd ece387_curriculum/my_scripts/HOG_Demo -python3 testDetector.py --detector training/stop_detector.svm --testing test -``` - -OK, so how did you do? What surprises did you have? What might you consider to improve the detector? - -### Summary -You have now trained and tested your first detector! In the future you will train a new detector using the camera on your robot and a real stop sign. This will be used in your final project to detect and react to stop signs in the wild! - -### Assignment -Research Dlib's simple object detector, and see how you might want to tune the options to improve the performance. - -### Cleanup -In the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. - -## Part 5: ROS and Image Capture -ROS provides a number of tools to interact with a commercial-off-the-shelf camera such as the USB camera connected to your robot. The primary tool we will use is the [usb_cam](http://wiki.ros.org/usb_cam) package which is already installed on your robot. - -Let's create a **lab4** package on the **Master** we can use to start developing a launch file to run our computer vision tools. - -In a terminal create a **lab4** package, `launch` folder, and `lab4.launch` file: - -```bash -cd ~/master_ws/src/ece387_robot_spring202X-USERNAME/ -catkin_create_pkg lab4 rospy sensor_msgs std_msgs cv_bridge apriltag_ros -cd lab4 -mkdir launch -cd launch -touch lab4.launch -``` - -Make and source your workspace. - -### Launch File - USB Cam - -Edit the `lab4.launch` file to call the **usb_cam_node** on the robot which will automatically connect to the camera and publish the video over a topic. - -```xml - - - - - - - - - - - - - - - -``` - -Save and exit. - -Ensure **roscore** is running on the **Master**. - -Run the **usb_cam** node on the **Robot** using the **lab4** launch file. - -Open a terminal on the **Master** and view the topics created by the node. - -The primary topic we will look at is */usb_cam/image_raw*. What type of message is sent over this topic? Take note as we will use this in the lab! - -Let's display the video using the **image_view** tool on the **Master**. - -```bash -rostopic list -rosrun rqt_image_view rqt_image_view -``` -Ensure the `/usb_cam/image_raw` topic is selected. - -### Calibrate USB Camera - -A camera must first be calibrated to utilize computer vision based tasks. Otherwise, there is no reference for how large objects are in regards to the camera frame. The [ROS Calibration Tool](http://wiki.ros.org/camera_calibration) creates a calibration file that is then used by other ROS packages to enable size and distance calculations. The **camera_calibration** package utilizes OpenCV camera calibration to allow easy calibration of monocular or stereo cameras using a checkerboard calibration target. The complete guide can be found on the [Camera Calibration Tutorial](http://wiki.ros.org/camera_calibration/Tutorials/MonocularCalibration). - -Connect to the camera using the **usb_cam** node: - -```bash -roslaunch lab4 lab4.launch -``` - -Run the camera calibrate package with the correct parameters (even though the checkerboard says it is a 9x6 board with 3.0 cm squares it is actually a 8x5 board with 2.7 cm squares - the size the calibration tool uses is actually the interior vertex points, not the squares). - -Open a new terminal on the **Master** and run the folowing: - -```bash -rosrun camera_calibration cameracalibrator.py --size 8x5 --square 0.027 image:=/usb_cam/image_raw camera:=/usb_cam -``` -You should see a window open that looks like this: -![logo](Figures/callibration.png) - -In order to get a good calibration you will need to move the checkerboard around in the camera frame such that: - -- checkerboard on the camera's left, right, top and bottom of field of view - - X bar - left/right in field of view - - Y bar - top/bottom in field of view - - Size bar - toward/away and tilt from the -- checkerboard filling the whole field of view -- checkerboard tilted to the left, right, top and bottom (Skew) - - -As you move the checkerboard around you will see three bars on the calibration sidebar increase in length. - -When the CALIBRATE button lights, you have enough data for calibration and can click CALIBRATE to see the results. Calibration can take a couple minutes. The windows might be greyed out but just wait, it is working. - -![logo](Figures/callibrate.png) - -When complete, select the save button and then commit. - -Browse to the location of the calibration data, extract, and move to the appropriate ROS folder on the robot: - -```bash -cd /tmp -tar xf calibrationdata.tar.gz -scp ost.yaml pi@robotX:/home/pi/.ros/camera_info/head_camera.yaml -``` - -Kill the `lab4.launch`. - -Create a secure shell to the robot and edit the calibration data and replace "narrow\_stero" with "head\_camera": - -```bash -ssh pi@robotX -nano /home/pi/.ros/camera_info/head_camera.yaml -``` - -Rerun the `lab4.launch` file on the robot. You should see the camera feed reopen and see no errors in the command line (you may need to unplug and plug your camera back in). - -### Checkpoint -Show an instructor the working camera feed and that the **usb_cam** node was able to open the camera calibration file. - -### Summary -You now are able to connect to a USB camera using ROS, display the image provided by the node, and have a calibration file that ROS can use to identify the size of objects in the frame. - -### Cleanup -Kill all rosnodes and roscore! - -## Part 6: Fiducial Markers - -In this lesson we will learn how fiducial markers are used in image processing. Specifically, we will utilize ROS tools to identify different [April Tags](https://april.eecs.umich.edu/papers/details.php?name=olson2011tags) and use the 3D position and orientation to determine the robot's distance from an object. - -A fiducial marker is an artificial feature used in creating controllable experiments, ground truthing, and in simplifying the development of systems where perception is not the central objective. A few examples of fiducial markers include ArUco Markers, AprilTags, and QR codes. Each of these different tags hold information such as an ID or, in the case of QR codes, websites, messages, and etc. We will primarily be focusing on AprilTags as there is a very robust ROS package already built. This library identifies AprilTags and will provide information about the tags size, distance, and orientation. - -### AprilTag ROS -Browse to the AprilTag_ROS package on the **Master** and edit the config file: - -```bash -roscd apriltag_ros/config -sudo nano tags.yaml -``` - -This is where you provide the package with information about the tags it should identify. You should have gotten tags 0-3. Each of these tags is $.165 m$ wide and should have a corresponding name: "tag_0" (in the final project, you might want to change these names as we will be providing you commands that correspond to each tag). In the `tags.yaml` file, add a line for each tag under "standalone tags" (replace ... with last two tags): - -```python -standalone_tags: - [ - {id: 0, size: .165, name: tag_0}, - {id: 1, size: .165, name: tag_1}, - ... - ] -``` - -Repeat these steps on the **Robot**. - -### Launch File - Apriltag_Ros - -Edit the `lab4.launch` file on the **Master**, calling the `continuous_detection` node provided by the **apriltag_ros** package. We need to set the arguments to the values provided by the `usb_cam` node: - -Add the following arguments and parameters to the top of the launch file: - -```xml - - - - - - - - -``` - -Add the apriltag node in the remote section: - - -```xml - - - - - - - - - -``` - -Save and exit. - -Launch the `lab4.launch` file. - -In a terminal on the master open the **rqt_image_view** node (`rosrun rqt_image_view rqt_image_view`) and select the *tag_detections_image* topic. If you hold up each tag, you should see a yellow box highlight the tag with an id in the middle of the tag. - -In another terminal on the master echo the topic `tag_detections`. What information do you see? Will the apriltag_ros node identify only one tag at a time? Which value do you think we would use to determine distance from the tag? What kind of message is this? What package does this message come from? - -### Checkpoint -Show an instructor that the **apriltag_ros** can identify tags and provides position data. - -### Summary -You now have the ability to identify AprilTags and because you have a calibrated camera, you can detect the size, orientation, and distance of a tag. - -### Cleanup -Kill all rosnodes and roscore! +# Computer Vision + +## Part 1: Image Basics +When we talk about the sizes of images, we generally talk about them in terms of the number of pixels the image possesses in the x(horizontal) or y(vertical) direction. If the image is a color image, we also need to concern ourselves with the depth of the image as well. Normally, each individual pixel is represented by the “color” or the “intensity” of light that appears in a given place in our image. + +If we think of an image as a grid, each square in the grid contains a single pixel. + +Most pixels are represented in two ways: grayscale and color. In a grayscale image, each pixel has a value between 0 and 255, where zero is corresponds to “black” and 255 being “white”. The values in between 0 and 255 are varying shades of gray, where values closer to 0 are darker and values closer 255 are lighter: + +![logo](Figures/Grayscale.JPG) + +The grayscale gradient image in the figure above demonstrates darker pixels on the left-hand side and progressively lighter pixels on the right-hand side. + +Color pixels, however, are normally represented in the RGB color space (this is where the term color-depth comes from)— one value for the Red component, one for Green, and one for Blue, leading to a total of 3 values per pixel: + +![logo](Figures/RGB.JPG) + +Other color spaces exist, and ordering of the colors may differ as well, but let’s start with the common RGB system. If we say the image is a 24-bit image, each of the three Red, Green, and Blue colors are represented by an integer in the range 0 to 255 (8-bits), which indicates how “much” of the color there is. Given that the pixel value only needs to be in the range [0, 255] we normally use an 8-bit unsigned integer to represent each color intensity. We then combine these values into a RGB tuple in the form (red, green, blue) . This tuple represents our color. For example: + +- To construct a white color, we would fill each of the red, green, and blue buckets completely up, like this: (255, 255, 255) — since white is the presence of all color. +- Then, to create a black color, we would empty each of the buckets out: (0, 0, 0) — since black is the absence of color. +- To create a pure red color, we would fill up the red bucket (and only the red bucket) up completely: (255, 0, 0) . +- etc + +Take a look at the following image to make this concept more clear: + + +![logo](Figures/RGB_Tuple.JPG) + +For your reference, here are some common colors represented as RGB tuples: + +- Black: (0, 0, 0) +- White: (255, 255, 255) +- Red: (255, 0, 0) +- Green: (0, 255, 0) +- Blue: (0, 0, 255) +- Aqua: (0, 255, 255) +- Fuchsia: (255, 0, 255) +- Maroon: (128, 0, 0) +- Navy: (0, 0, 128) +- Olive: (128, 128, 0) +- Purple: (128, 0, 128) +- Teal: (0, 128, 128) +- Yellow: (255, 255, 0) + +## Part 2: Coding with OpenCV-Python +It is time to build our first bit of code working with OpenCV. Just like ROS, OpenCV is well supported by both Python and C++. For simplicity, we will use Python throughout this course. However, continue to recognize that if speed and efficiency become important, switching to a more robust language like C++ may become necessary. To make use of OpenCV with Python, we need to import cv2. The code below will simply load in the RGB figure above and print out the pixel values in each of the 4-quadrants. + +First we need to import the OpenCV Python library, `cv2`: + + +```python +import cv2 +``` + +Then we can load the image: + + +```python +image = cv2.imread("RGB_Tuple.JPG") +``` + +The `shape` characteristic of the image returns a tuple of the number of rows, columns, and channels (if the image is color): + + +```python +print("width: %d pixels" %(image.shape[1])) +print("height: %d pixels" % (image.shape[0])) +print("color channels: %d" % (image.shape[2])) +``` + +You an also access specific pixels within the image (the `image` variable is really just an array of pixel values) by the row and column coordinates. Each pixel values is an array of Blue, Green, and Red values. + + +```python +# print the BGR values of a pixel in the upper left of the image +print(image[10, 10, :]) + +# print the red value of a pixel in the bottom left of the image +print(image[700, 100, 2]) +``` + +Fill in the code to do the following: + + +```python +# TODO: print BGR values of a pixel in the upper right of the image +print(image[ , , :]) + +# TODO: print BGR values of a pixel in the lower left of the image +print(image[ , , :]) + +# TODO: print blue value of a pixel in the lower right of the image +print(image[ , , ]) +``` + +We can display the image as well. +> ⚠️ **WARNING:** To exit the image just press any key. **DO NOT** press the 'X' in the corner. If you do press the 'X' (smh) you will have to Restart & Clear the Kernel: in the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". + + +```python +cv2.imshow("Loaded image", image) +cv2.waitKey(0) +cv2.destroyAllWindows() # close the image window +``` + +When executing the code above, there were two minor surprises. What do you think they were? Now lets take a look at additional functionality embedded within OpenCV. + +Convert image to RGB and print the same pixel values. Remember that the image is already loaded within the `image` variable. + + +```python +# TODO: Convert image to RGB + + +# TODO: print the RGB values of a pixel in the upper left of the image + + +# TODO: print the red value of a pixel in the bottom left of the image + + +# TODO: print RGB values of a pixel in the upper right of the image + + +# TODO: print RGB values of a pixel in the lower left of the image + + +# TODO: print blue value of a pixel in the lower right of the image + +``` + +Modify the code to convert to grayscale and print the same pixel values. + + +```python +# TODO: Convert image to Grayscale + + +# TODO: print the Grayscale values of a pixel in the upper left of the image + + +# TODO: print Grayscale values of a pixel in the upper right of the image + + +# TODO: print Grayscale values of a pixel in the lower left of the image + +``` + +### Summary +These examples barely scratch the surface of what is possible with OpenCV. In the upcoming lessons we will learn a few more ways to manipulate images, but if you want to learn more you can either explore the [OpenCV-Python Source Documentation](https://docs.opencv.org/3.4/index.html) or the [OpenCV-Python Tutorial](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html). + +### Assignment +Scan the article on the [Histogram of Oriented Gradients (HOG)](https://arxiv.org/pdf/1406.2419.pdf) feature descriptor and be prepared to discuss. I don't need you to understand the math, but you should be able to understand the advantages of the technique. + +### Cleanup +In the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. + +## Part 3: Gradients + +The objective of this portion of the lesson is for you to start the process of learning how to create custom object detectors in an image. There are many techniques, but the one technique I am interested in applying first is what is known as Histogram of Oriented Gradients. Before we can dig into the technique, we should first understand a bit about image gradients and contours. + +By the end of today's lesson you will be able to: +- Define what an image gradient is +- Compute changes in direction of an input image +- Define both gradient magnitude and gradient orientation +- Use OpenCV to approximate image gradients + +The image gradient is one of the fundamental building blocks in computer vision image processing. + +We use gradients for detecting edges in images, which allows us to find contours and outlines of objects in images. We use them as inputs for quantifying images through feature extraction — in fact, highly successful and well-known image descriptors such as Histogram of Oriented Gradients and SIFT are built upon image gradient representations. Gradient images are even used to construct saliency maps, which highlight the subjects of an image. We use gradients all the time in computer vision and image processing. I would go as far as to say they are one of the most important building blocks you will learn about in this module. While they are not often discussed in detail since other more powerful and interesting methods build on top of them, we are going to take the time and discuss them in detail. + +As I mentioned in the introduction, image gradients are used as the basic building blocks in many computer vision and image processing applications. However, the main application of image gradients lies within edge detection. As the name suggests, edge detection is the process of finding edges in an image, which reveals structural information regarding the objects in an image. Edges could therefore correspond to: + +- Boundaries of an object in an image. +- Boundaries of shadowing or lighting conditions in an image. +- Boundaries of “parts” within an object. + +As we mentioned in the previous portion of the lab, we will often work with grayscale images, because of the massive reduction in images. OpenCV will convert to grayscale using the following conversion formula: + +$Y = 0.299 R + 0.587 G + 0.114 B$ + +Let's see if that matches our expectations in the figure below: + +![logo](Figures/RGB_Gray.png) + +The below figure is an image of edges being detected simply by looking for the contours in an image: + +![logo](Figures/EdgeDet.png) + +As you can see, all of the edges (or changes in contrast are clearly identified), but how did we do it? Lets look at the math below, and then we will look at how simple the code is by taking advantage of OpenCV. + +Formally, an image gradient is defined as a directional change in image intensity. Or put more simply, at each pixel of the input (grayscale) image, a gradient measures the change in pixel intensity in a given direction. By estimating the direction or orientation along with the magnitude (i.e. how strong the change in direction is), we are able to detect regions of an image that look like edges. + +Lets look at a blown up version of a basic pixel map. Our goal here is to establish the basic framework for how we will eventually compute the gradient: + +![logo](Figures/PixelCont.png) + +In the image above we essentially wish to examine the \(3 \times 3\) neighborhood surrounding the central pixel. Our x values run from left to right, and our y values from top to bottom. In order to compute any changes in direction we will need the north, south, east, and west pixels, which are marked on the above figure. + +If we denote our input image as *I*, then we define the north, south, east, and west pixels using the following notation: + +- North: $I(x, y - 1)$ +- South: $I(x, y + 1)$ +- East: $I(x + 1, y)$ +- West: $I(x - 1, y)$ + +Again, these four values are critical in computing the changes in image intensity in both the x and y direction. + +To demonstrate this, let us compute the vertical change or the y-change by taking the difference between the south and north pixels: + +$G_{y} = I(x, y + 1) - I(x, y - 1)$ + +Similarly, we can compute the horizontal change or the x-change by taking the difference between the east and west pixels: + +$G_{x} = I(x + 1, y) - I(x - 1, y)$ + +Awesome — so now we have $G_{x}$ and $G_{y}$, which represent the change in image intensity for the central pixel in both the x and y direction. Lets look at a relatively intuitive example at first without all the math. + +![logo](Figures/GradientEx.png) + +On the left we have a $3 \times 3$ region of an image where the top half of the image is white and the bottom half of the image is black. The gradient orientation is thus equal to $\theta=-90^{\circ}$ + +And on the right we have another $3 \times 3$ neighborhood of an image, where the upper triangular region is white and the lower triangular region is black. Here we can see the change in direction is equal to $\theta=-45^{\circ}$. While these two examples are both relatively easy to understand, lets use our knowledge of the Pythagorean theorem to actually compute the magnitude and orientation of the gradient with actual values now. + +![logo](Figures/GradTrig.png) + +Inspecting the triangle in the above figure you can see that the gradient magnitude G is the hypotenuse of the triangle. Therefore, all we need to do is apply the Pythagorean theorem and we will end up with the gradient magnitude: + +$G = \sqrt{G_{x}^{2} + G_{y}^{2}}$ + +The gradient orientation can then be given as the ratio of $G_{y}$ to $G_{x}$. We can use the $tan^{-1}$ to compute the gradient orientation, + +$\theta = tan^{-1}(\frac{G_{y}}{G_{x}}) \times (\frac{180}{\pi})$ + +We converted to degrees by multiplying by the ratio of $180/\pi$. Lets now add pixel intensity values and put this to the test. + +![logo](Figures/GradEx2.png) + +In the above image we have an image where the upper-third is white and the bottom two-thirds is black. Using the equations for $G_{x}$ and $G_{y}$, we arrive at: + +$G_{x} = $ + +and + +$G_{y} = $ + +Plugging these values into our gradient magnitude equation we get: + +$G = $ + +As for our gradient orientation: + +$\theta = $ + +Now you try with the following example: + +![logo](Figures/GradEx3.png) + +$G_{x} = $ + +and + +$G_{y} = $ + +Plugging these values into our gradient magnitude equation we get: + +$G = $ + +As for our gradient orientation: + +$\theta = $ + +Now that you know how to compute both the orientation and the magnitude of the gradients, you essentially have the most basic building block established for computing the necessary information for HOG w/ SVM. Additionally, you can use the following code to compute very effective contours in images. + +Fortunately, in practice we don't need to do any of the math above. Instead we can use what is known as the Sobel Kernel to compute the values for $G_{x}$ and $G_{y}$. OpenCV and numpy have functionality built in that allow us to do all of this very quickly. + +> To exit the image just press any key. **DO NOT** press the 'X' in the corner. If you do press the 'X' (smh) you will have to Restart & Clear the Kernel: in the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". + + +```python +import cv2 +import numpy as np +``` + + +```python +#load the image +image=cv2.imread("RGB_Tuple.JPG") +gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) +``` + + +```python +#Show the original image along with the grayscale image +cv2.imshow("Original image", image) +cv2.imshow("Grayscale Image", gray) + +# Lets now compute the gradients along the X and Y axis, respectively +gX = cv2.Sobel(gray,cv2.CV_64F,1,0) +gY = cv2.Sobel(gray,cv2.CV_64F,0,1) + +# the `gX` and `gY` images are now of the floating point data type, +# so we need to take care to convert them back to an unsigned 8-bit +# integer representation so other OpenCV functions can utilize them +gX = cv2.convertScaleAbs(gX) +gY = cv2.convertScaleAbs(gY) + +# combine the sobel X and Y representations into a single image +sobelCombined = cv2.addWeighted(gX, 0.5, gY, 0.5, 0) +cv2.imshow("Gradient Image", sobelCombined) +cv2.waitKey(0) +cv2.destroyAllWindows() # close the image window +``` + +### Summary +Gradients are one important tool used in object detection. Next lesson we will learn how to apply gradients using the Histogram of Oriented Gradients to train an object detector. + +### Assignment +Watch the following video on [Histogram of Oriented Gradients](https://youtube.com/watch?v=4ESLTAd3IOM). + +### Cleanup +In the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. + +## Part 4: Histogram of Oriented Gradients (HOG) Features + +The objective of this portion of the lesson is to demonstrate the functionality of the HOG with SVM (Support Vector Machine) algorithm for object detection. By this point, we should all be well aware of what a histogram is. The application of the histogram for the HOG feature extraction is to further simplify the tested image to enable our computer to rapidly and accurately identify the presence of an object within the image. +Instead of using each individual gradient direction of each individual pixel of an image, we group the pixels into small cells. For each cell, we compute all the gradient directions and group them into a number of orientation bins. We sum up the gradient magnitude in each sample. So stronger gradients contribute more weight to their bins, and effects of small random orientations due to noise is reduced. Doing this for all cells gives us a representation of the structure of the image. The HOG features keep the representation of an object distinct but also allow for some variations in shape. For example, lets consider an object detector for a car, see the below figure. + +![logo](Figures/HOG_Features.JPG) + +Comparing each individual pixel of this training image with another test image would not only be time consuming, but it would also be highly subject to noise. As previously mentioned, the HOG feature will consider a block of pixels. The size of this block is variable and will naturally impact both accuracy and speed of execution for the algorithm. Once the block size is determined, the gradient for each pixel within the block is computed. Once the gradients are computed for a block, the entire cell can then be represented by this histogram. Not only does this reduce the amount of data to compare with a test image, but it also reduces the impacts of noise in the image and measurements. + +![logo](Figures/HOG_Histogram.JPG) + +Now that we have an understanding of the HOG features, lets use tools embedded within OpenCV and Dlib to build our first detector for a stop sign. But first we need to download a pre-created repository of test and training data. Remember, we won't use our training data to test the effectiveness of the algorithm. Of course the algorithm will work effectively on the training data. Our hope is that we can create a large enough sampling of test data that we can have a highly effective detector that is robust against new images. + +(CV:HOG)= +### Building a detector using HOG features +Download the example demo into the `my_scripts` folder you created earlier in the semester. It should be located under `~/master_ws/src/ece387_curriculum/`. + +```bash +roscd ece387_curriculum/my_scripts +git clone git@github.com:ECE495/HOG_Demo.git +cd HOG_Demo +``` + +Take a look at what is contained within the repo. Essentially you have both a training data folder and a test folder. We will now use a tool called **imglab** to annotate the images for building our detector. + +Browse to the [imglab tool](https://imglab.in/) and select **"UMM, MAYBE NEXT TIME!"**. + +In the bottom left of the site, select the load button and browse to the training folder: + +![logo](Figures/load.png) + +Select the first stop sign and the **"Rectangle"** tool. + +![logo](Figures/rectangle.png) + +Highlight the border of the stop sign: drag-and-draw a bounding rectangle, ensuring to **only** select the stop sign and to select all examples of the object in the image. + +> 📝️ **NOTE:** It is important to label all examples of objects in an image; otherwise, Dlib will implicitly assume that regions not labeled are regions that should not be detected (i.e., hard-negative mining applied during extraction time). + +You can select a bounding box and hit the delete key to remove it. + +If you press `alt+left/right arrow` you can navigate through images in the slider and repeat highlighting the objects. + +Once all stop signs are complete hit `ctrl+e` to save the annotations (bounding box information) as a **"Dlib XML"** file within the `training` folder using a descriptive name such as `stop_annotations.xml`. + +![logo](Figures/xml.png) + +We now need to create the code to build the detector based on our annotated training data. + +```bash +cd ~/master_ws/src/ece387_curriculum/my_scripts/HOG_Demo +touch trainDetector.py +``` + +Now open this in your favorite editor to add the following code. I have built into the code the ability to provide command line arguments. This will make the code a bit more flexible such that you don't need to recreate it in the future if you want to reuse if for another project. You will provide two arguments at runtime. First you need to tell the program where the .xml file is. Second, you will state where you want to put the detector that you create... the detector should have a .svm extension. + +```python +# import the necessary packages +from __future__ import print_function +import argparse +import dlib + +# construct the argument parser and parse the arguments +ap = argparse.ArgumentParser() +ap.add_argument("-x", "--xml", required=True, help="path to input XML file") +ap.add_argument("-d", "--detector", required=True, help="path to output detector") +args = vars(ap.parse_args()) + +# grab the default training options for the HOG + Linear SVM detector, then +# train the detector -- in practice, the `C` parameter can be adjusted... +# feel free to research and see if you can improve +print("[INFO] training detector...") +options = dlib.simple_object_detector_training_options() +options.C = 1.0 +options.num_threads = 4 +options.be_verbose = True +dlib.train_simple_object_detector(args["xml"], args["detector"], options) + +# show the training accuracy +print("[INFO] training accuracy: {}".format( + dlib.test_simple_object_detector(args["xml"], args["detector"]))) + +# load the detector and visualize the HOG filter +detector = dlib.simple_object_detector(args["detector"]) +win = dlib.image_window() +win.set_image(detector) +dlib.hit_enter_to_continue() +``` + +Once you have the code entered, you can run it with the following command. Remember, you need to provide two command line arguments: + +```bash +roscd ece387_curriculum/my_scripts/HOG_Demo +python3 trainDetector.py --xml training/stop_annotations.xml --detector training/stop_detector.svm +``` + +You may get a few errors pop up during execution based on your choice for bounding boxes. Make sure you address those errors before continuing. If everything executed correctly, you should ultimately see a picture of the HOG feature you designed. + +### Testing a detector +Now it is time to build our code to test the detector. The following code will make use of the imutils library as well. + +You may get a few errors pop up during execution based on your choice for bounding boxes. Make sure you address those errors before continuing. If everything executed correctly, you should ultimately see a picture of the HOG feature you designed. + +Now it is time to build our code to test the detector. + +```bash +roscd ece387_curriculum/my_scripts/HOG_Demo +touch testDetector.py +``` + +Again, use your preferred editor to enter the code below: + +```python +# import the necessary packages +from imutils import paths +import argparse +import dlib +import cv2 + +# construct the argument parser and parse the arguments +ap = argparse.ArgumentParser() +ap.add_argument("-d", "--detector", required=True, help="Path to trained object detector") +ap.add_argument("-t", "--testing", required=True, help="Path to directory of testing images") +args = vars(ap.parse_args()) + +# load the detector +detector = dlib.simple_object_detector(args["detector"]) + +# loop over the testing images +for testingPath in paths.list_images(args["testing"]): + # load the image and make predictions + image = cv2.imread(testingPath) + boxes = detector(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + + # loop over the bounding boxes and draw them + for b in boxes: + (x, y, w, h) = (b.left(), b.top(), b.right(), b.bottom()) + cv2.rectangle(image, (x, y), (w, h), (0, 255, 0), 2) + + # show the image + cv2.imshow("Image", image) + cv2.waitKey(0) +``` + +Run the test detector: + +```bash +roscd ece387_curriculum/my_scripts/HOG_Demo +python3 testDetector.py --detector training/stop_detector.svm --testing test +``` + +OK, so how did you do? What surprises did you have? What might you consider to improve the detector? + +### Summary +You have now trained and tested your first detector! In the future you will train a new detector using the camera on your robot and a real stop sign. This will be used in your final project to detect and react to stop signs in the wild! + +### Assignment +Research Dlib's simple object detector, and see how you might want to tune the options to improve the performance. + +### Cleanup +In the Jupyter Notebook at the top menu bar select "Kernel" and "Restart & Clear Output". Shutdown the notebook server by typing `ctrl+c` within the terminal you ran `jupyter-notebook` in. Select 'y'. + +## Part 5: ROS and Image Capture +ROS provides a number of tools to interact with a commercial-off-the-shelf camera such as the USB camera connected to your robot. The primary tool we will use is the [usb_cam](http://wiki.ros.org/usb_cam) package which is already installed on your robot. + +Let's create a **lab4** package on the **Master** we can use to start developing a launch file to run our computer vision tools. + +In a terminal create a **lab4** package, `launch` folder, and `lab4.launch` file: + +```bash +cd ~/master_ws/src/ece387_robot_spring202X-USERNAME/ +catkin_create_pkg lab4 rospy sensor_msgs std_msgs cv_bridge apriltag_ros +cd lab4 +mkdir launch +cd launch +touch lab4.launch +``` + +Make and source your workspace. + +### Launch File - USB Cam + +Edit the `lab4.launch` file to call the **usb_cam_node** on the robot which will automatically connect to the camera and publish the video over a topic. + +```xml + + + + + + + + + + + + + + + +``` + +Save and exit. + +Ensure **roscore** is running on the **Master**. + +Run the **usb_cam** node on the **Robot** using the **lab4** launch file. + +Open a terminal on the **Master** and view the topics created by the node. + +The primary topic we will look at is */usb_cam/image_raw*. What type of message is sent over this topic? Take note as we will use this in the lab! + +Let's display the video using the **image_view** tool on the **Master**. + +```bash +rostopic list +rosrun rqt_image_view rqt_image_view +``` +Ensure the `/usb_cam/image_raw` topic is selected. + +### Calibrate USB Camera + +A camera must first be calibrated to utilize computer vision based tasks. Otherwise, there is no reference for how large objects are in regards to the camera frame. The [ROS Calibration Tool](http://wiki.ros.org/camera_calibration) creates a calibration file that is then used by other ROS packages to enable size and distance calculations. The **camera_calibration** package utilizes OpenCV camera calibration to allow easy calibration of monocular or stereo cameras using a checkerboard calibration target. The complete guide can be found on the [Camera Calibration Tutorial](http://wiki.ros.org/camera_calibration/Tutorials/MonocularCalibration). + +Connect to the camera using the **usb_cam** node: + +```bash +roslaunch lab4 lab4.launch +``` + +Run the camera calibrate package with the correct parameters (even though the checkerboard says it is a 9x6 board with 3.0 cm squares it is actually a 8x5 board with 2.7 cm squares - the size the calibration tool uses is actually the interior vertex points, not the squares). + +Open a new terminal on the **Master** and run the folowing: + +```bash +rosrun camera_calibration cameracalibrator.py --size 8x5 --square 0.027 image:=/usb_cam/image_raw camera:=/usb_cam +``` +You should see a window open that looks like this: +![logo](Figures/callibration.png) + +In order to get a good calibration you will need to move the checkerboard around in the camera frame such that: + +- checkerboard on the camera's left, right, top and bottom of field of view + - X bar - left/right in field of view + - Y bar - top/bottom in field of view + - Size bar - toward/away and tilt from the +- checkerboard filling the whole field of view +- checkerboard tilted to the left, right, top and bottom (Skew) + + +As you move the checkerboard around you will see three bars on the calibration sidebar increase in length. + +When the CALIBRATE button lights, you have enough data for calibration and can click CALIBRATE to see the results. Calibration can take a couple minutes. The windows might be greyed out but just wait, it is working. + +![logo](Figures/callibrate.png) + +When complete, select the save button and then commit. + +Browse to the location of the calibration data, extract, and move to the appropriate ROS folder on the robot: + +```bash +cd /tmp +tar xf calibrationdata.tar.gz +scp ost.yaml pi@robotX:/home/pi/.ros/camera_info/head_camera.yaml +``` + +Kill the `lab4.launch`. + +Create a secure shell to the robot and edit the calibration data and replace "narrow\_stero" with "head\_camera": + +```bash +ssh pi@robotX +nano /home/pi/.ros/camera_info/head_camera.yaml +``` + +Rerun the `lab4.launch` file on the robot. You should see the camera feed reopen and see no errors in the command line (you may need to unplug and plug your camera back in). + +### Checkpoint +Show an instructor the working camera feed and that the **usb_cam** node was able to open the camera calibration file. + +### Summary +You now are able to connect to a USB camera using ROS, display the image provided by the node, and have a calibration file that ROS can use to identify the size of objects in the frame. + +### Cleanup +Kill all rosnodes and roscore! + +## Part 6: Fiducial Markers + +In this lesson we will learn how fiducial markers are used in image processing. Specifically, we will utilize ROS tools to identify different [April Tags](https://april.eecs.umich.edu/papers/details.php?name=olson2011tags) and use the 3D position and orientation to determine the robot's distance from an object. + +A fiducial marker is an artificial feature used in creating controllable experiments, ground truthing, and in simplifying the development of systems where perception is not the central objective. A few examples of fiducial markers include ArUco Markers, AprilTags, and QR codes. Each of these different tags hold information such as an ID or, in the case of QR codes, websites, messages, and etc. We will primarily be focusing on AprilTags as there is a very robust ROS package already built. This library identifies AprilTags and will provide information about the tags size, distance, and orientation. + +### AprilTag ROS +Browse to the AprilTag_ROS package on the **Master** and edit the config file: + +```bash +roscd apriltag_ros/config +sudo nano tags.yaml +``` + +This is where you provide the package with information about the tags it should identify. You should have gotten tags 0-3. Each of these tags is $.165 m$ wide and should have a corresponding name: "tag_0" (in the final project, you might want to change these names as we will be providing you commands that correspond to each tag). In the `tags.yaml` file, add a line for each tag under "standalone tags" (replace ... with last two tags): + +```python +standalone_tags: + [ + {id: 0, size: .165, name: tag_0}, + {id: 1, size: .165, name: tag_1}, + ... + ] +``` + +Repeat these steps on the **Robot**. + +### Launch File - Apriltag_Ros + +Edit the `lab4.launch` file on the **Master**, calling the `continuous_detection` node provided by the **apriltag_ros** package. We need to set the arguments to the values provided by the `usb_cam` node: + +Add the following arguments and parameters to the top of the launch file: + +```xml + + + + + + + + +``` + +Add the apriltag node in the remote section: + + +```xml + + + + + + + + + +``` + +Save and exit. + +Launch the `lab4.launch` file. + +In a terminal on the master open the **rqt_image_view** node (`rosrun rqt_image_view rqt_image_view`) and select the *tag_detections_image* topic. If you hold up each tag, you should see a yellow box highlight the tag with an id in the middle of the tag. + +In another terminal on the master echo the topic `tag_detections`. What information do you see? Will the apriltag_ros node identify only one tag at a time? Which value do you think we would use to determine distance from the tag? What kind of message is this? What package does this message come from? + +### Checkpoint +Show an instructor that the **apriltag_ros** can identify tags and provides position data. + +### Summary +You now have the ability to identify AprilTags and because you have a calibrated camera, you can detect the size, orientation, and distance of a tag. + +### Cleanup +Kill all rosnodes and roscore! diff --git a/_sources/faq.md b/_sources/faq.md old mode 100755 new mode 100644 index 9069dba..fcb4df7 --- a/_sources/faq.md +++ b/_sources/faq.md @@ -1,29 +1,29 @@ -# 🙋 FAQ - -(faq-general)= -## General - -### _/usr/bin/env: ‘python3\r’: No such file or directory_ - -$ rosrun lab1 mouse_client.py -/usr/bin/env: ‘python3\r’: No such file or directory - - -The problem are your line ending characters. Your file was created or edited on a Windows system and uses Windows/DOS-style line endings (CR+LF), whereas Linux systems like Ubuntu require Unix-style line endings (LF). - -- Sublime: Open the desired file with Sublime and from the top menu select View -> Line Endings and then the Windows(CRLF) or Unix(LF). That’s it. -- VS Code: At the bottom right of the screen in VS Code there is a little button that says `LF` or `CRLF`: Click that button and change it to your preference. - -
    - - - - - - - - - - - - +# 🙋 FAQ + +(faq-general)= +## General + +### _/usr/bin/env: ‘python3\r’: No such file or directory_ + +$ rosrun lab1 mouse_client.py +/usr/bin/env: ‘python3\r’: No such file or directory + + +The problem are your line ending characters. Your file was created or edited on a Windows system and uses Windows/DOS-style line endings (CR+LF), whereas Linux systems like Ubuntu require Unix-style line endings (LF). + +- Sublime: Open the desired file with Sublime and from the top menu select View -> Line Endings and then the Windows(CRLF) or Unix(LF). That’s it. +- VS Code: At the bottom right of the screen in VS Code there is a little button that says `LF` or `CRLF`: Click that button and change it to your preference. + +
    + + + + + + + + + + + + diff --git a/_sources/intro.md b/_sources/intro.md old mode 100755 new mode 100644 index 5233334..2731146 --- a/_sources/intro.md +++ b/_sources/intro.md @@ -1,40 +1,40 @@ -# ECE 387 Introduction to Robotic Systems - -## 👨‍🏫 Instructors -- ![neff](https://img.shields.io/badge/Col%20Brian%20Neff-6xxx-yellow) -- [![baek](https://img.shields.io/badge/Dr.%20Stan%20Baek-2E38-red)](https://stanbaek.github.io) - - - -## 📝 Course information -- **Course Goal**: Cadets shall design, implement, test, and debug robotics-based systems by developing Python programs incorporating built-in Robotics Operating System (ROS) functions and successfully interfacing the microcomputer with the external world. - -- **Course Text**: There is no printed textbook for ECE 387. Reading materials/labs are posted on this Course Web. - -- **Syllabus**: Posted [here](syllabus.md). - -- **Course Schedule**: Posted [here](schedule.md) and subject to change. - -## 📡 Communication and Control (C2) -- All communication and announcement 📣 will be provided through MS Teams -- All lecture 📓 materials will be provided through MS Teams. -- Laboratory 🔬 work will be posted in this course web. -- All assignments must be submitted in [Gradescope](https://www.gradecope.com) -- [GitHub](https://www.github.com) will be used for students to provide their source code 📄 for homework and laboratory assignments. - -## 🤝 Collaboration Policy: - -Unless specifically directed otherwise, the collaboration policy for this course is: - -Authorized resources: Any material from the ECE 382 course site and online sources regarding C programming syntax only. This does not include any solutions or solution stubs for challenges similar to those asked in any assignments. - -- For all assignments in this course, unless otherwise noted on the assignment, you may work with anyone. We expect all graded work, including code and written reports, to be in your own work. Copying another person’s work, with or without documentation, will result in **NO** 🚫 academic credit. Furthermore, copying without attribution is dishonorable and will be dealt with as an honor code violation. -- All help received on work submitted for grading must be documented in accordance with the course documentation 📝 policy. -- GRs are individual efforts. No collaboration is allowed while taking these exams. All electronic devices (phones, smartwatches, computers, tablets, etc.) must be placed out of sight for the duration of the event. If any electronic device is seen during the event, the student will receive a zero for that effort. - - - \ No newline at end of file diff --git a/_sources/python.md b/_sources/python.md old mode 100755 new mode 100644 index 69f4535..583f93f --- a/_sources/python.md +++ b/_sources/python.md @@ -1,13 +1,13 @@ -# 🐍 Python Tutorials - -## Online Tutorials - -- [Python in easy steps](https://ineasysteps.com/wp-content/uploads/2018/07/Python-in-easy-steps-2nd-Ed-TOCCh1.pdf) - -## NumPy -- [NumPy user guide](https://numpy.org/doc/stable/user/index.html) -- [NumPy for MATLAB users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) - -## Python Style Guide -- [PEP8](https://www.python.org/dev/peps/pep-0008/) - +# 🐍 Python Tutorials + +## Online Tutorials + +- [Python in easy steps](https://ineasysteps.com/wp-content/uploads/2018/07/Python-in-easy-steps-2nd-Ed-TOCCh1.pdf) + +## NumPy +- [NumPy user guide](https://numpy.org/doc/stable/user/index.html) +- [NumPy for MATLAB users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) + +## Python Style Guide +- [PEP8](https://www.python.org/dev/peps/pep-0008/) + diff --git a/_sources/resources.md b/_sources/resources.md deleted file mode 100755 index 42d7753..0000000 --- a/_sources/resources.md +++ /dev/null @@ -1,24 +0,0 @@ -# 💎 Resources - -## Recommended Textbooks 📚 -- Peter Corke, [**Robotics, Vision, and Control**](https://petercorke.com/rvc/home/), Springer, 1st & 2nd ed., free 1st ed [pdf](https://link.springer.com/content/pdf/10.1007%2F978-3-642-20144-8.pdf); easy to read but sometimes skips details. -- Craig, **Introduction to Robotics: Mehcanics and Control**. Pearson, 4th ed.; most popular textbook and easy to read. -- Spong, Hutchinson, & Viidyasagar, **Robot Modeling and Control**; alternative to Craig's book, more in-depth math, covers a great deal of contents not covered by Craig. -- Murray, Li, & Sastry, **A Mathematical Introduction to Robotics Manipualtion**, free [pdf](http://www.cds.caltech.edu/~murray/mlswiki/?title=First_edition); must-have book for roboticists, extremeley hard to read. don't read it if you are a biginner - you will hate robotics, UC Berkeley [EE106A/206A](https://ucb-ee106.github.io/106a-fa20site/) - - -## Online 💻 Lecture Series by K. Lynch (Northwestern Univ.) -- Modern Robotics [YouTube Channel](https://www.youtube.com/watch?v=jVu-Hijns70&list=PLggLP4f-rq02vX0OQQ5vrCxbJrzamYDfx&ab_channel=NorthwesternRobotics) -- Textbook: Modern Robotics: mechanics, planning and control -- Textbook [Wiki](http://hades.mech.northwestern.edu/index.php/Modern_Robotics) -- Textbook [pdf](http://hades.mech.northwestern.edu/images/2/25/MR-v2.pdf) -- Coursera [course](https://www.coursera.org/specializations/modernrobotics) (free to enroll) -- Lynch's course [website](http://hades.mech.northwestern.edu/index.php/ME_449_Robotic_Manipulation) - - -## Linear Algebra ➕✖️ -**The best Linear Algebra textbook in the world (I think)** -- **textbook**: Gilbert Strang, [**Introductioin to Linear Algebra**](https://math.mit.edu/~gs/linearalgebra/) -- G. Strang's phenomenal [lecture videos at MIT](https://ocw.mit.edu/courses/mathematics/18-06-linear-algebra-spring-2010/video-lectures/) -- You can also find many of his lecture videos in **YouTube**. -- If you want to go to **graduate school** and study robotics, computer vision, information system, machine learning, artificial intelligence, or data science, you should have a strong background in linear algebra. \ No newline at end of file diff --git a/_sources/schedule.md b/_sources/schedule.md old mode 100755 new mode 100644 index 3486ad0..a5dac75 --- a/_sources/schedule.md +++ b/_sources/schedule.md @@ -1,50 +1,50 @@ -# 📆 Course Schedule - -```{note} -This schedule is subject to change as appropriate. -``` - -**Last Updated: 21 Dec 2023** - -| Lesson | Topic | Due (Before Class)| -|:------:|----------------------------------------------|:-----------------:| -| 1 | Course Intro, Master & Robot, Git | | -| 2 | Module 1: ROS | | -| 3 | Quiz 1 & ICE 1 | | -| 4 | Module 2: Linux for Robotics | | -| 5 | Quiz 2 & ICE 2 | | -| 6 | Module 3: Python 3 for Robotics | | -| 7 | Quiz 3 & ICE3 | | -| 8 | ICE 3 | | -| 9 | Module 4: Driving the Robot, ICE 4 | | -| 10 | Module 5: Custom Messages, ICE5 | | -| 11 | Lab 1 | | -| 12 | Lab 1 | | -| 13 | Module 6: IMU, ICE 6 | Lab 1 | -| 14 | Lab2 | | -| 15 | Lab2 | | -| 16 | Module 7: Launch File, ICE 7 | Lab 2 | -| 17 | GR 1 | | -| 18 | Module 8: LIDAR, ICE 8 | | -| 19 | Lab 3 | | -| 20 | Lab 3 | | -| 21 | Module 9: Computer Vision, OpenCV, ICE 9 | Lab 3 | -| 22 | Lab 4 | | -| 23 | Lab 4 | | -| 24 | Lab 4 | | -| 25 | Module 10: Computer Vision, AprilTags, ICE 10| Lab 4 | -| 26 | Lab 5 | | -| 27 | Lab 5 | | -| 28 | Lab 5 | | -| 29 | Final Project - Requirements | Lab 5 | -| 30 | Final Project - Design Briefs | Briefings | -| 31 | Final Project - Implementation | | -| 32 | Final Project | | -| 33 | Final Project | | -| 34 | Final Project | | -| 35 | Final Project | | -| 36 | Final Project | | -| 37 | Final Project | | -| 38 | Final Project | | -| 39 | Final Project | | +# 📆 Course Schedule + +```{note} +This schedule is subject to change as appropriate. +``` + +**Last Updated: 21 Dec 2023** + +| Lesson | Topic | Due (Before Class)| +|:------:|----------------------------------------------|:-----------------:| +| 1 | Course Intro, Master & Robot, Git | | +| 2 | Module 1: ROS | | +| 3 | Quiz 1 & ICE 1 | | +| 4 | Module 2: Linux for Robotics | | +| 5 | Quiz 2 & ICE 2 | | +| 6 | Module 3: Python 3 for Robotics | | +| 7 | Quiz 3 & ICE3 | | +| 8 | ICE 3 | | +| 9 | Module 4: Driving the Robot, ICE 4 | | +| 10 | Module 5: Custom Messages, ICE5 | | +| 11 | Lab 1 | | +| 12 | Lab 1 | | +| 13 | Module 6: IMU, ICE 6 | Lab 1 | +| 14 | Lab2 | | +| 15 | Lab2 | | +| 16 | Module 7: Launch File, ICE 7 | Lab 2 | +| 17 | GR 1 | | +| 18 | Module 8: LIDAR, ICE 8 | | +| 19 | Lab 3 | | +| 20 | Lab 3 | | +| 21 | Module 9: Computer Vision, OpenCV, ICE 9 | Lab 3 | +| 22 | Lab 4 | | +| 23 | Lab 4 | | +| 24 | Lab 4 | | +| 25 | Module 10: Computer Vision, AprilTags, ICE 10| Lab 4 | +| 26 | Lab 5 | | +| 27 | Lab 5 | | +| 28 | Lab 5 | | +| 29 | Final Project - Requirements | Lab 5 | +| 30 | Final Project - Design Briefs | Briefings | +| 31 | Final Project - Implementation | | +| 32 | Final Project | | +| 33 | Final Project | | +| 34 | Final Project | | +| 35 | Final Project | | +| 36 | Final Project | | +| 37 | Final Project | | +| 38 | Final Project | | +| 39 | Final Project | | | 40 | Final Project - Demo | Demo | \ No newline at end of file diff --git a/_sources/syllabus.md b/_sources/syllabus.md old mode 100755 new mode 100644 index dcb9419..3f10157 --- a/_sources/syllabus.md +++ b/_sources/syllabus.md @@ -1,94 +1,94 @@ -# 📌 Syllabus - -## Course Goals -Cadets shall design, implement, test, and debug robotics-based systems by developing Python programs incorporating built-in Robotics Operating System (ROS) functions and successfully interfacing the microcomputer with the external world. - -## Course Objectives -Cadets shall be able to: -- Demonstrate a clear understanding of basic concepts of robotic systems. -- Utilize the Linux operating system to develop software systems for robotic applications. -- Write, compile, and run Robotics Operating System (ROS) code in Python. -- Interface sensors and actuators with a microcomputer. -- Implement control algorithms to accomplish robotic tasks. -- Evaluate and analyze in writing the outcomes of laboratory work. - -## Course Prerequisites -CS206, CS210, CS211, CS212, or department approval. - -## Course Schedule -The course schedule is [here](schedule.md) - -## Grade Distribution and Policy - -The overall **weighting** of the graded items is: - -| Prog | | Final | | -|------------------------|-------------|--------------------|-------------| -| GRs | 20% | GRs | 20% | -| Labs | 75% | Labs | 50% | -| Quizzes | 5% | Quizzes | 5% | -| | | Final Project | 25% | -| | | | | -| Total | 100% | Total | 100% | - -The **Grade distribution** for this course is shown in the chart below. - -| Grade | Grade | -|-----------------------|---------------------| -| 93 <= A <= 100 | 77 <= C+ < 80 | -| 90 <= A- < 93 | 73 <= C < 77 | -| 87 <= B+ < 90 | 70 <= C- < 73 | -| 83 <= B < 87 | 60 <= D < 70 | -| 80 <= B- < 83 | 0 <= F < 60 | - -_**You must complete all minimum functionalities on labs in order to complete the course.** Even if an assignment is so late that no credit will be received, the assignment must be completed to the satisfaction of the instructor to prevent a grade of “Incomplete.”_ - -## Primary Communication and Control (C2) -All communication and course material will be provided through a course/section Team. A Blockboard course will be used to provide grades. Lastly, GitHub will be used for cadets to provide their source code for laboratories. - -## Textbooks -There is no printed textbook for ECE 387. Reading materials/labs are posted on this Course Web. - -## Collaboration Policy - -Unless specifically directed otherwise, the collaboration policy for this course is: - -Authorized resources: Any material from the ECE 387 course site and online sources regarding C programming syntax only. This does not include any solutions or solution stubs for challenges similar to those asked in any assignments. - -- For all assignments in this course, unless otherwise noted on the assignment, you may work with anyone. We expect all graded work, including code and written reports, to be in your own work. Copying another person’s work, with or without documentation, will result in NO academic credit. Furthermore, copying without attribution is dishonorable and will be dealt with as an honor code violation. -- All help received on work submitted for grading must be documented in accordance with the course documentation policy. -- GRs are individual efforts. No collaboration is allowed while taking these exams. All electronic devices (phones, smartwatches, computers, tablets, etc.) must be placed out of sight for the duration of the event. If any electronic device is seen during the event, the student will receive a zero for that effort. - -## EI Policy - -Schedule EI with an instructor if you are having difficulty with the course material. You must have read the assignment and attempted the homework before requesting EI. Note: You are responsible for material if you miss class, so get notes from someone in your section. For example, you miss the lesson where the instructor announces a quiz for the next lesson or the instructor assigns homework due next lesson. Even though you missed the lesson, you are still responsible for the quiz, homework, or any other assignments made. It is in your best interest to check with your classmates after an absence. After you’ve read the assignment, attempted the homework, and checked with your classmates, you may then schedule EI if you have difficulty with the material—not to make up a class you missed. - -## CAS Policy -For CAS notification, email your instructor prior to your absence and include the lesson number, the date, and the reason (descriptive reason — don’t just send a CAS code or SCA number) as soon as possible, preferably before the absence occurs. It is your responsibility to check your SCA to see if instructor permission is required. If it is, you must make the request prior to your absence. If you miss class, you are responsible for all material (e.g. assignments, notes, announcements, handouts, etc.) covered in class. Please check with another cadet in your section to find out what you missed. - -When a cadet is absent on the day that an assignment is due, or on the date of a quiz or GR, the cadet is responsible for meeting the following standards: -- Scheduled Absence: If a cadet will miss any graded event due to a scheduled absence such as an SCA, sport team trip, or scheduled lasik surgery, the cadet is expected to complete all work BEFORE the absence. -- Unscheduled Absence: If a cadet misses a graded event for an unscheduled reason such as AOC approved bedrest or a family emergency, the cadet must complete all work on the first full class day that they return to duty in order to avoid a late penalty. For example, if a cadet is on AOC bedrest for a GR on M17 and can return to duty on T17 or M18, the cadet is expected to make up the work by M18. -- Unique Circumstances: For circumstances that do not fall under either of these broad categories (e.g. concussion protocol), the cadet is expected to communicate early and often with the instructor. The instructor and course director will work with the cadet on a course of action. - -## Late Work Policy -All work is due as shown on the [schedule](schedule.md) or in the assignment. If problems arise with graded assignments, see your instructor in advance. Assignments turned in later than the due date **without prior permission** from the instructor will be penalized (with instructor discretion) 25% per **calendar** day. - -## Assignments -Assignments and due dates are included on the [schedule](schedule.md). - -## Exams and Quizzes -All exams and quizzes are closed textbook and notes. Quizzes will be given at instructor discretion. Testable material includes any concepts from the labs, lectures, exercises, homework, and assigned readings. **Not all testable concepts will necessarily be covered in class (e.g., readings).** - -For missed GRs, the following policies are outlined in USAFA FOI 537-3: -- Scheduled Absence - If you know that you will be unable to take the GR during the scheduled GR period, you are required to inform your instructor as soon as possible before the GR and to schedule a make-up exam. -- Unscheduled Absence - If you miss the GR for reasons beyond your control (e.g. hospitalization, emergency leave, delayed field trip return, etc.), you must contact DFEC (x3190) within two working days to schedule a makeup. Exceptions can only be granted by the Department Head. - -## Laboratories -Labs are held in 2E48 but may include a prelab assignment that must be done before coming to class. The labs tend to be very hardware intensive and will probably require debugging to isolate and fix problems. In-class time is your primary chance to get active help for these problems so the more you prepare outside of class, the more successful you’ll be. The 53 minutes go by extremely fast - don’t waste them! - -## Final Project -The final project will be a culmination of the learned material and will include a robot maze and competition. The final project will include a formal laboratory write-up and seven-minute presentation describing your design, solution, and results. The final project is worth 25% of your final grade. - -## Miscellaneous -This course is designed to help in your development as an engineer or cyber scientist. Feel free to provide feedback on the lessons and labs at any time. If you have ideas to improve or enhance the course, please let me know. The class builds on concepts from the prerequisites, so it is important for you to seek help as soon as you need it. Procrastination is truly the enemy in a hardware design course. A little foresight and planning and a lot of effort will result in an extremely rewarding experience serving as the basis for future microprocessor design work. +# 📌 Syllabus + +## Course Goals +Cadets shall design, implement, test, and debug robotics-based systems by developing Python programs incorporating built-in Robotics Operating System (ROS) functions and successfully interfacing the microcomputer with the external world. + +## Course Objectives +Cadets shall be able to: +- Demonstrate a clear understanding of basic concepts of robotic systems. +- Utilize the Linux operating system to develop software systems for robotic applications. +- Write, compile, and run Robotics Operating System (ROS) code in Python. +- Interface sensors and actuators with a microcomputer. +- Implement control algorithms to accomplish robotic tasks. +- Evaluate and analyze in writing the outcomes of laboratory work. + +## Course Prerequisites +CS206, CS210, CS211, CS212, or department approval. + +## Course Schedule +The course schedule is [here](schedule.md) + +## Grade Distribution and Policy + +The overall **weighting** of the graded items is: + +| Prog | | Final | | +|------------------------|-------------|--------------------|-------------| +| GRs | 20% | GRs | 20% | +| Labs | 75% | Labs | 50% | +| Quizzes | 5% | Quizzes | 5% | +| | | Final Project | 25% | +| | | | | +| Total | 100% | Total | 100% | + +The **Grade distribution** for this course is shown in the chart below. + +| Grade | Grade | +|-----------------------|---------------------| +| 93 <= A <= 100 | 77 <= C+ < 80 | +| 90 <= A- < 93 | 73 <= C < 77 | +| 87 <= B+ < 90 | 70 <= C- < 73 | +| 83 <= B < 87 | 60 <= D < 70 | +| 80 <= B- < 83 | 0 <= F < 60 | + +_**You must complete all minimum functionalities on labs in order to complete the course.** Even if an assignment is so late that no credit will be received, the assignment must be completed to the satisfaction of the instructor to prevent a grade of “Incomplete.”_ + +## Primary Communication and Control (C2) +All communication and course material will be provided through a course/section Team. A Blockboard course will be used to provide grades. Lastly, GitHub will be used for cadets to provide their source code for laboratories. + +## Textbooks +There is no printed textbook for ECE 387. Reading materials/labs are posted on this Course Web. + +## Collaboration Policy + +Unless specifically directed otherwise, the collaboration policy for this course is: + +Authorized resources: Any material from the ECE 387 course site and online sources regarding C programming syntax only. This does not include any solutions or solution stubs for challenges similar to those asked in any assignments. + +- For all assignments in this course, unless otherwise noted on the assignment, you may work with anyone. We expect all graded work, including code and written reports, to be in your own work. Copying another person’s work, with or without documentation, will result in NO academic credit. Furthermore, copying without attribution is dishonorable and will be dealt with as an honor code violation. +- All help received on work submitted for grading must be documented in accordance with the course documentation policy. +- GRs are individual efforts. No collaboration is allowed while taking these exams. All electronic devices (phones, smartwatches, computers, tablets, etc.) must be placed out of sight for the duration of the event. If any electronic device is seen during the event, the student will receive a zero for that effort. + +## EI Policy + +Schedule EI with an instructor if you are having difficulty with the course material. You must have read the assignment and attempted the homework before requesting EI. Note: You are responsible for material if you miss class, so get notes from someone in your section. For example, you miss the lesson where the instructor announces a quiz for the next lesson or the instructor assigns homework due next lesson. Even though you missed the lesson, you are still responsible for the quiz, homework, or any other assignments made. It is in your best interest to check with your classmates after an absence. After you’ve read the assignment, attempted the homework, and checked with your classmates, you may then schedule EI if you have difficulty with the material—not to make up a class you missed. + +## CAS Policy +For CAS notification, email your instructor prior to your absence and include the lesson number, the date, and the reason (descriptive reason — don’t just send a CAS code or SCA number) as soon as possible, preferably before the absence occurs. It is your responsibility to check your SCA to see if instructor permission is required. If it is, you must make the request prior to your absence. If you miss class, you are responsible for all material (e.g. assignments, notes, announcements, handouts, etc.) covered in class. Please check with another cadet in your section to find out what you missed. + +When a cadet is absent on the day that an assignment is due, or on the date of a quiz or GR, the cadet is responsible for meeting the following standards: +- Scheduled Absence: If a cadet will miss any graded event due to a scheduled absence such as an SCA, sport team trip, or scheduled lasik surgery, the cadet is expected to complete all work BEFORE the absence. +- Unscheduled Absence: If a cadet misses a graded event for an unscheduled reason such as AOC approved bedrest or a family emergency, the cadet must complete all work on the first full class day that they return to duty in order to avoid a late penalty. For example, if a cadet is on AOC bedrest for a GR on M17 and can return to duty on T17 or M18, the cadet is expected to make up the work by M18. +- Unique Circumstances: For circumstances that do not fall under either of these broad categories (e.g. concussion protocol), the cadet is expected to communicate early and often with the instructor. The instructor and course director will work with the cadet on a course of action. + +## Late Work Policy +All work is due as shown on the [schedule](schedule.md) or in the assignment. If problems arise with graded assignments, see your instructor in advance. Assignments turned in later than the due date **without prior permission** from the instructor will be penalized (with instructor discretion) 25% per **calendar** day. + +## Assignments +Assignments and due dates are included on the [schedule](schedule.md). + +## Exams and Quizzes +All exams and quizzes are closed textbook and notes. Quizzes will be given at instructor discretion. Testable material includes any concepts from the labs, lectures, exercises, homework, and assigned readings. **Not all testable concepts will necessarily be covered in class (e.g., readings).** + +For missed GRs, the following policies are outlined in USAFA FOI 537-3: +- Scheduled Absence - If you know that you will be unable to take the GR during the scheduled GR period, you are required to inform your instructor as soon as possible before the GR and to schedule a make-up exam. +- Unscheduled Absence - If you miss the GR for reasons beyond your control (e.g. hospitalization, emergency leave, delayed field trip return, etc.), you must contact DFEC (x3190) within two working days to schedule a makeup. Exceptions can only be granted by the Department Head. + +## Laboratories +Labs are held in 2E48 but may include a prelab assignment that must be done before coming to class. The labs tend to be very hardware intensive and will probably require debugging to isolate and fix problems. In-class time is your primary chance to get active help for these problems so the more you prepare outside of class, the more successful you’ll be. The 53 minutes go by extremely fast - don’t waste them! + +## Final Project +The final project will be a culmination of the learned material and will include a robot maze and competition. The final project will include a formal laboratory write-up and seven-minute presentation describing your design, solution, and results. The final project is worth 25% of your final grade. + +## Miscellaneous +This course is designed to help in your development as an engineer or cyber scientist. Feel free to provide feedback on the lessons and labs at any time. If you have ideas to improve or enhance the course, please let me know. The class builds on concepts from the prerequisites, so it is important for you to seek help as soon as you need it. Procrastination is truly the enemy in a hardware design course. A little foresight and planning and a lot of effort will result in an extremely rewarding experience serving as the basis for future microprocessor design work. diff --git a/_sphinx_design_static/design-style.4045f2051d55cab465a707391d5b2007.min.css b/_sphinx_design_static/design-style.4045f2051d55cab465a707391d5b2007.min.css old mode 100755 new mode 100644 index 57bec30..3225661 --- a/_sphinx_design_static/design-style.4045f2051d55cab465a707391d5b2007.min.css +++ b/_sphinx_design_static/design-style.4045f2051d55cab465a707391d5b2007.min.css @@ -1 +1 @@ -.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #007bff;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0069d9;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #007bff;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0069d9;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/_sphinx_design_static/design-tabs.js b/_sphinx_design_static/design-tabs.js old mode 100755 new mode 100644 index a869cf5..36b38cf --- a/_sphinx_design_static/design-tabs.js +++ b/_sphinx_design_static/design-tabs.js @@ -1,27 +1,27 @@ -var sd_labels_by_text = {}; - -function ready() { - const li = document.getElementsByClassName("sd-tab-label"); - for (const label of li) { - syncId = label.getAttribute("data-sync-id"); - if (syncId) { - label.onclick = onLabelClick; - if (!sd_labels_by_text[syncId]) { - sd_labels_by_text[syncId] = []; - } - sd_labels_by_text[syncId].push(label); - } - } -} - -function onLabelClick() { - // Activate other inputs with the same sync id. - syncId = this.getAttribute("data-sync-id"); - for (label of sd_labels_by_text[syncId]) { - if (label === this) continue; - label.previousElementSibling.checked = true; - } - window.localStorage.setItem("sphinx-design-last-tab", syncId); -} - -document.addEventListener("DOMContentLoaded", ready, false); +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_static/_sphinx_javascript_frameworks_compat.js b/_static/_sphinx_javascript_frameworks_compat.js old mode 100755 new mode 100644 diff --git a/_static/basic.css b/_static/basic.css old mode 100755 new mode 100644 index d613287..9e364ed --- a/_static/basic.css +++ b/_static/basic.css @@ -1,930 +1,930 @@ -/* - * basic.css - * ~~~~~~~~~ - * - * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -div.section::after { - display: block; - content: ''; - clear: left; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 270px; - margin-left: -100%; - font-size: 90%; - word-wrap: break-word; - overflow-wrap : break-word; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox form.search { - overflow: hidden; -} - -div.sphinxsidebar #searchbox input[type="text"] { - float: left; - width: 80%; - padding: 0.25em; - box-sizing: border-box; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - float: left; - width: 20%; - border-left: none; - padding: 0.25em; - box-sizing: border-box; -} - - -img { - border: 0; - max-width: 100%; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li p.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; - margin-left: auto; - margin-right: auto; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable ul { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -table.indextable > tbody > tr > td > ul { - padding-left: 0em; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- domain module index --------------------------------------------------- */ - -table.modindextable td { - padding: 2px; - border-collapse: collapse; -} - -/* -- general body styles --------------------------------------------------- */ - -div.body { - min-width: 360px; - max-width: 800px; -} - -div.body p, div.body dd, div.body li, div.body blockquote { - -moz-hyphens: auto; - -ms-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; -} - -a.headerlink { - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -caption:hover > a.headerlink, -p.caption:hover > a.headerlink, -div.code-block-caption:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, figure.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, figure.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, figure.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -img.align-default, figure.align-default, .figure.align-default { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-default { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar, -aside.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px; - background-color: #ffe; - width: 40%; - float: right; - clear: right; - overflow-x: auto; -} - -p.sidebar-title { - font-weight: bold; -} -nav.contents, -aside.topic, - -div.admonition, div.topic, blockquote { - clear: left; -} - -/* -- topics ---------------------------------------------------------------- */ -nav.contents, -aside.topic, - -div.topic { - border: 1px solid #ccc; - padding: 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- content of sidebars/topics/admonitions -------------------------------- */ - -div.sidebar > :last-child, -aside.sidebar > :last-child, -nav.contents > :last-child, -aside.topic > :last-child, - -div.topic > :last-child, -div.admonition > :last-child { - margin-bottom: 0; -} - -div.sidebar::after, -aside.sidebar::after, -nav.contents::after, -aside.topic::after, - -div.topic::after, -div.admonition::after, -blockquote::after { - display: block; - content: ''; - clear: both; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - margin-top: 10px; - margin-bottom: 10px; - border: 0; - border-collapse: collapse; -} - -table.align-center { - margin-left: auto; - margin-right: auto; -} - -table.align-default { - margin-left: auto; - margin-right: auto; -} - -table caption span.caption-number { - font-style: italic; -} - -table caption span.caption-text { -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -th > :first-child, -td > :first-child { - margin-top: 0px; -} - -th > :last-child, -td > :last-child { - margin-bottom: 0px; -} - -/* -- figures --------------------------------------------------------------- */ - -div.figure, figure { - margin: 0.5em; - padding: 0.5em; -} - -div.figure p.caption, figcaption { - padding: 0.3em; -} - -div.figure p.caption span.caption-number, -figcaption span.caption-number { - font-style: italic; -} - -div.figure p.caption span.caption-text, -figcaption span.caption-text { -} - -/* -- field list styles ----------------------------------------------------- */ - -table.field-list td, table.field-list th { - border: 0 !important; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.field-name { - -moz-hyphens: manual; - -ms-hyphens: manual; - -webkit-hyphens: manual; - hyphens: manual; -} - -/* -- hlist styles ---------------------------------------------------------- */ - -table.hlist { - margin: 1em 0; -} - -table.hlist td { - vertical-align: top; -} - -/* -- object description styles --------------------------------------------- */ - -.sig { - font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; -} - -.sig-name, code.descname { - background-color: transparent; - font-weight: bold; -} - -.sig-name { - font-size: 1.1em; -} - -code.descname { - font-size: 1.2em; -} - -.sig-prename, code.descclassname { - background-color: transparent; -} - -.optional { - font-size: 1.3em; -} - -.sig-paren { - font-size: larger; -} - -.sig-param.n { - font-style: italic; -} - -/* C++ specific styling */ - -.sig-inline.c-texpr, -.sig-inline.cpp-texpr { - font-family: unset; -} - -.sig.c .k, .sig.c .kt, -.sig.cpp .k, .sig.cpp .kt { - color: #0033B3; -} - -.sig.c .m, -.sig.cpp .m { - color: #1750EB; -} - -.sig.c .s, .sig.c .sc, -.sig.cpp .s, .sig.cpp .sc { - color: #067D17; -} - - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -:not(li) > ol > li:first-child > :first-child, -:not(li) > ul > li:first-child > :first-child { - margin-top: 0px; -} - -:not(li) > ol > li:last-child > :last-child, -:not(li) > ul > li:last-child > :last-child { - margin-bottom: 0px; -} - -ol.simple ol p, -ol.simple ul p, -ul.simple ol p, -ul.simple ul p { - margin-top: 0; -} - -ol.simple > li:not(:first-child) > p, -ul.simple > li:not(:first-child) > p { - margin-top: 0; -} - -ol.simple p, -ul.simple p { - margin-bottom: 0; -} - -/* Docutils 0.17 and older (footnotes & citations) */ -dl.footnote > dt, -dl.citation > dt { - float: left; - margin-right: 0.5em; -} - -dl.footnote > dd, -dl.citation > dd { - margin-bottom: 0em; -} - -dl.footnote > dd:after, -dl.citation > dd:after { - content: ""; - clear: both; -} - -/* Docutils 0.18+ (footnotes & citations) */ -aside.footnote > span, -div.citation > span { - float: left; -} -aside.footnote > span:last-of-type, -div.citation > span:last-of-type { - padding-right: 0.5em; -} -aside.footnote > p { - margin-left: 2em; -} -div.citation > p { - margin-left: 4em; -} -aside.footnote > p:last-of-type, -div.citation > p:last-of-type { - margin-bottom: 0em; -} -aside.footnote > p:last-of-type:after, -div.citation > p:last-of-type:after { - content: ""; - clear: both; -} - -/* Footnotes & citations ends */ - -dl.field-list { - display: grid; - grid-template-columns: fit-content(30%) auto; -} - -dl.field-list > dt { - font-weight: bold; - word-break: break-word; - padding-left: 0.5em; - padding-right: 5px; -} - -dl.field-list > dt:after { - content: ":"; -} - -dl.field-list > dd { - padding-left: 0.5em; - margin-top: 0em; - margin-left: 0em; - margin-bottom: 0em; -} - -dl { - margin-bottom: 15px; -} - -dd > :first-child { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dl > dd:last-child, -dl > dd:last-child > :last-child { - margin-bottom: 0; -} - -dt:target, span.highlighted { - background-color: #fbe54e; -} - -rect.highlighted { - fill: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -.classifier:before { - font-style: normal; - margin: 0 0.5em; - content: ":"; - display: inline-block; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -pre, div[class*="highlight-"] { - clear: both; -} - -span.pre { - -moz-hyphens: none; - -ms-hyphens: none; - -webkit-hyphens: none; - hyphens: none; - white-space: nowrap; -} - -div[class*="highlight-"] { - margin: 1em 0; -} - -td.linenos pre { - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - display: block; -} - -table.highlighttable tbody { - display: block; -} - -table.highlighttable tr { - display: flex; -} - -table.highlighttable td { - margin: 0; - padding: 0; -} - -table.highlighttable td.linenos { - padding-right: 0.5em; -} - -table.highlighttable td.code { - flex: 1; - overflow: hidden; -} - -.highlight .hll { - display: block; -} - -div.highlight pre, -table.highlighttable pre { - margin: 0; -} - -div.code-block-caption + div { - margin-top: 0; -} - -div.code-block-caption { - margin-top: 1em; - padding: 2px 5px; - font-size: small; -} - -div.code-block-caption code { - background-color: transparent; -} - -table.highlighttable td.linenos, -span.linenos, -div.highlight span.gp { /* gp: Generic.Prompt */ - user-select: none; - -webkit-user-select: text; /* Safari fallback only */ - -webkit-user-select: none; /* Chrome/Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+ */ -} - -div.code-block-caption span.caption-number { - padding: 0.1em 0.3em; - font-style: italic; -} - -div.code-block-caption span.caption-text { -} - -div.literal-block-wrapper { - margin: 1em 0; -} - -code.xref, a code { - background-color: transparent; - font-weight: bold; -} - -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -span.eqno a.headerlink { - position: absolute; - z-index: 1; -} - -div.math:hover a.headerlink { - visibility: visible; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 270px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} +nav.contents, +aside.topic, + +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ +nav.contents, +aside.topic, + +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, + +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, + +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +/* Docutils 0.17 and older (footnotes & citations) */ +dl.footnote > dt, +dl.citation > dt { + float: left; + margin-right: 0.5em; +} + +dl.footnote > dd, +dl.citation > dd { + margin-bottom: 0em; +} + +dl.footnote > dd:after, +dl.citation > dd:after { + content: ""; + clear: both; +} + +/* Docutils 0.18+ (footnotes & citations) */ +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +/* Footnotes & citations ends */ + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dt:after { + content: ":"; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } } \ No newline at end of file diff --git a/_static/check-solid.svg b/_static/check-solid.svg old mode 100755 new mode 100644 diff --git a/_static/clipboard.min.js b/_static/clipboard.min.js old mode 100755 new mode 100644 diff --git a/_static/copy-button.svg b/_static/copy-button.svg old mode 100755 new mode 100644 diff --git a/_static/copybutton.css b/_static/copybutton.css old mode 100755 new mode 100644 diff --git a/_static/copybutton.js b/_static/copybutton.js old mode 100755 new mode 100644 index cd25be7..2ea7ff3 --- a/_static/copybutton.js +++ b/_static/copybutton.js @@ -1,248 +1,248 @@ -// Localization support -const messages = { - 'en': { - 'copy': 'Copy', - 'copy_to_clipboard': 'Copy to clipboard', - 'copy_success': 'Copied!', - 'copy_failure': 'Failed to copy', - }, - 'es' : { - 'copy': 'Copiar', - 'copy_to_clipboard': 'Copiar al portapapeles', - 'copy_success': '¡Copiado!', - 'copy_failure': 'Error al copiar', - }, - 'de' : { - 'copy': 'Kopieren', - 'copy_to_clipboard': 'In die Zwischenablage kopieren', - 'copy_success': 'Kopiert!', - 'copy_failure': 'Fehler beim Kopieren', - }, - 'fr' : { - 'copy': 'Copier', - 'copy_to_clipboard': 'Copié dans le presse-papier', - 'copy_success': 'Copié !', - 'copy_failure': 'Échec de la copie', - }, - 'ru': { - 'copy': 'Скопировать', - 'copy_to_clipboard': 'Скопировать в буфер', - 'copy_success': 'Скопировано!', - 'copy_failure': 'Не удалось скопировать', - }, - 'zh-CN': { - 'copy': '复制', - 'copy_to_clipboard': '复制到剪贴板', - 'copy_success': '复制成功!', - 'copy_failure': '复制失败', - }, - 'it' : { - 'copy': 'Copiare', - 'copy_to_clipboard': 'Copiato negli appunti', - 'copy_success': 'Copiato!', - 'copy_failure': 'Errore durante la copia', - } -} - -let locale = 'en' -if( document.documentElement.lang !== undefined - && messages[document.documentElement.lang] !== undefined ) { - locale = document.documentElement.lang -} - -let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; -if (doc_url_root == '#') { - doc_url_root = ''; -} - -/** - * SVG files for our copy buttons - */ -let iconCheck = ` - ${messages[locale]['copy_success']} - - -` - -// If the user specified their own SVG use that, otherwise use the default -let iconCopy = ``; -if (!iconCopy) { - iconCopy = ` - ${messages[locale]['copy_to_clipboard']} - - - -` -} - -/** - * Set up copy/paste for code blocks - */ - -const runWhenDOMLoaded = cb => { - if (document.readyState != 'loading') { - cb() - } else if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', cb) - } else { - document.attachEvent('onreadystatechange', function() { - if (document.readyState == 'complete') cb() - }) - } -} - -const codeCellId = index => `codecell${index}` - -// Clears selected text since ClipboardJS will select the text when copying -const clearSelection = () => { - if (window.getSelection) { - window.getSelection().removeAllRanges() - } else if (document.selection) { - document.selection.empty() - } -} - -// Changes tooltip text for a moment, then changes it back -// We want the timeout of our `success` class to be a bit shorter than the -// tooltip and icon change, so that we can hide the icon before changing back. -var timeoutIcon = 2000; -var timeoutSuccessClass = 1500; - -const temporarilyChangeTooltip = (el, oldText, newText) => { - el.setAttribute('data-tooltip', newText) - el.classList.add('success') - // Remove success a little bit sooner than we change the tooltip - // So that we can use CSS to hide the copybutton first - setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) - setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) -} - -// Changes the copy button icon for two seconds, then changes it back -const temporarilyChangeIcon = (el) => { - el.innerHTML = iconCheck; - setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) -} - -const addCopyButtonToCodeCells = () => { - // If ClipboardJS hasn't loaded, wait a bit and try again. This - // happens because we load ClipboardJS asynchronously. - if (window.ClipboardJS === undefined) { - setTimeout(addCopyButtonToCodeCells, 250) - return - } - - // Add copybuttons to all of our code cells - const COPYBUTTON_SELECTOR = 'div.highlight pre'; - const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) - codeCells.forEach((codeCell, index) => { - const id = codeCellId(index) - codeCell.setAttribute('id', id) - - const clipboardButton = id => - `` - codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) - }) - -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -/** - * Removes excluded text from a Node. - * - * @param {Node} target Node to filter. - * @param {string} exclude CSS selector of nodes to exclude. - * @returns {DOMString} Text from `target` with text removed. - */ -function filterText(target, exclude) { - const clone = target.cloneNode(true); // clone as to not modify the live DOM - if (exclude) { - // remove excluded nodes - clone.querySelectorAll(exclude).forEach(node => node.remove()); - } - return clone.innerText; -} - -// Callback when a copy button is clicked. Will be passed the node that was clicked -// should then grab the text and replace pieces of text that shouldn't be used in output -function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { - var regexp; - var match; - - // Do we check for line continuation characters and "HERE-documents"? - var useLineCont = !!lineContinuationChar - var useHereDoc = !!hereDocDelim - - // create regexp to capture prompt and remaining line - if (isRegexp) { - regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') - } else { - regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') - } - - const outputLines = []; - var promptFound = false; - var gotLineCont = false; - var gotHereDoc = false; - const lineGotPrompt = []; - for (const line of textContent.split('\n')) { - match = line.match(regexp) - if (match || gotLineCont || gotHereDoc) { - promptFound = regexp.test(line) - lineGotPrompt.push(promptFound) - if (removePrompts && promptFound) { - outputLines.push(match[2]) - } else { - outputLines.push(line) - } - gotLineCont = line.endsWith(lineContinuationChar) & useLineCont - if (line.includes(hereDocDelim) & useHereDoc) - gotHereDoc = !gotHereDoc - } else if (!onlyCopyPromptLines) { - outputLines.push(line) - } else if (copyEmptyLines && line.trim() === '') { - outputLines.push(line) - } - } - - // If no lines with the prompt were found then just use original lines - if (lineGotPrompt.some(v => v === true)) { - textContent = outputLines.join('\n'); - } - - // Remove a trailing newline to avoid auto-running when pasting - if (textContent.endsWith("\n")) { - textContent = textContent.slice(0, -1) - } - return textContent -} - - -var copyTargetText = (trigger) => { - var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); - - // get filtered text - let exclude = '.linenos, .gp'; - - let text = filterText(target, exclude); - return formatCopyText(text, '', false, true, true, true, '', '') -} - - // Initialize with a callback so we can modify the text before copy - const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) - - // Update UI with error/success messages - clipboard.on('success', event => { - clearSelection() - temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) - temporarilyChangeIcon(event.trigger) - }) - - clipboard.on('error', event => { - temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) - }) -} - +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/_static/copybutton_funcs.js b/_static/copybutton_funcs.js old mode 100755 new mode 100644 diff --git a/_static/design-style.4045f2051d55cab465a707391d5b2007.min.css b/_static/design-style.4045f2051d55cab465a707391d5b2007.min.css old mode 100755 new mode 100644 index 57bec30..3225661 --- a/_static/design-style.4045f2051d55cab465a707391d5b2007.min.css +++ b/_static/design-style.4045f2051d55cab465a707391d5b2007.min.css @@ -1 +1 @@ -.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #007bff;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0069d9;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #007bff;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0069d9;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/_static/design-tabs.js b/_static/design-tabs.js old mode 100755 new mode 100644 index a869cf5..36b38cf --- a/_static/design-tabs.js +++ b/_static/design-tabs.js @@ -1,27 +1,27 @@ -var sd_labels_by_text = {}; - -function ready() { - const li = document.getElementsByClassName("sd-tab-label"); - for (const label of li) { - syncId = label.getAttribute("data-sync-id"); - if (syncId) { - label.onclick = onLabelClick; - if (!sd_labels_by_text[syncId]) { - sd_labels_by_text[syncId] = []; - } - sd_labels_by_text[syncId].push(label); - } - } -} - -function onLabelClick() { - // Activate other inputs with the same sync id. - syncId = this.getAttribute("data-sync-id"); - for (label of sd_labels_by_text[syncId]) { - if (label === this) continue; - label.previousElementSibling.checked = true; - } - window.localStorage.setItem("sphinx-design-last-tab", syncId); -} - -document.addEventListener("DOMContentLoaded", ready, false); +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_static/doctools.js b/_static/doctools.js old mode 100755 new mode 100644 diff --git a/_static/documentation_options.js b/_static/documentation_options.js old mode 100755 new mode 100644 index 828e1d2..3063782 --- a/_static/documentation_options.js +++ b/_static/documentation_options.js @@ -1,14 +1,14 @@ -var DOCUMENTATION_OPTIONS = { - URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '', - LANGUAGE: 'en', - COLLAPSE_INDEX: false, - BUILDER: 'html', - FILE_SUFFIX: '.html', - LINK_SUFFIX: '.html', - HAS_SOURCE: true, - SOURCELINK_SUFFIX: '', - NAVIGATION_WITH_KEYS: true, - SHOW_SEARCH_SUMMARY: true, - ENABLE_SEARCH_SHORTCUTS: false, +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '', + NAVIGATION_WITH_KEYS: true, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: false, }; \ No newline at end of file diff --git a/_static/ece387.png b/_static/ece387.png old mode 100755 new mode 100644 diff --git a/_static/favicon.ico b/_static/favicon.ico old mode 100755 new mode 100644 diff --git a/_static/file.png b/_static/file.png old mode 100755 new mode 100644 diff --git a/_static/images/logo_binder.svg b/_static/images/logo_binder.svg old mode 100755 new mode 100644 diff --git a/_static/images/logo_colab.png b/_static/images/logo_colab.png old mode 100755 new mode 100644 diff --git a/_static/images/logo_deepnote.svg b/_static/images/logo_deepnote.svg old mode 100755 new mode 100644 diff --git a/_static/images/logo_jupyterhub.svg b/_static/images/logo_jupyterhub.svg old mode 100755 new mode 100644 diff --git a/_static/jquery-3.6.0.js b/_static/jquery-3.6.0.js old mode 100755 new mode 100644 diff --git a/_static/jquery.js b/_static/jquery.js old mode 100755 new mode 100644 diff --git a/_static/language_data.js b/_static/language_data.js old mode 100755 new mode 100644 index 29455f0..2e22b06 --- a/_static/language_data.js +++ b/_static/language_data.js @@ -1,199 +1,199 @@ -/* - * language_data.js - * ~~~~~~~~~~~~~~~~ - * - * This script contains the language-specific data used by searchtools.js, - * namely the list of stopwords, stemmer, scorer and splitter. - * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; - - -/* Non-minified version is copied as a separate JS file, is available */ - -/** - * Porter Stemmer - */ -var Stemmer = function() { - - var step2list = { - ational: 'ate', - tional: 'tion', - enci: 'ence', - anci: 'ance', - izer: 'ize', - bli: 'ble', - alli: 'al', - entli: 'ent', - eli: 'e', - ousli: 'ous', - ization: 'ize', - ation: 'ate', - ator: 'ate', - alism: 'al', - iveness: 'ive', - fulness: 'ful', - ousness: 'ous', - aliti: 'al', - iviti: 'ive', - biliti: 'ble', - logi: 'log' - }; - - var step3list = { - icate: 'ic', - ative: '', - alize: 'al', - iciti: 'ic', - ical: 'ic', - ful: '', - ness: '' - }; - - var c = "[^aeiou]"; // consonant - var v = "[aeiouy]"; // vowel - var C = c + "[^aeiouy]*"; // consonant sequence - var V = v + "[aeiou]*"; // vowel sequence - - var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 - var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 - var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 - var s_v = "^(" + C + ")?" + v; // vowel in stem - - this.stemWord = function (w) { - var stem; - var suffix; - var firstch; - var origword = w; - - if (w.length < 3) - return w; - - var re; - var re2; - var re3; - var re4; - - firstch = w.substr(0,1); - if (firstch == "y") - w = firstch.toUpperCase() + w.substr(1); - - // Step 1a - re = /^(.+?)(ss|i)es$/; - re2 = /^(.+?)([^s])s$/; - - if (re.test(w)) - w = w.replace(re,"$1$2"); - else if (re2.test(w)) - w = w.replace(re2,"$1$2"); - - // Step 1b - re = /^(.+?)eed$/; - re2 = /^(.+?)(ed|ing)$/; - if (re.test(w)) { - var fp = re.exec(w); - re = new RegExp(mgr0); - if (re.test(fp[1])) { - re = /.$/; - w = w.replace(re,""); - } - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = new RegExp(s_v); - if (re2.test(stem)) { - w = stem; - re2 = /(at|bl|iz)$/; - re3 = new RegExp("([^aeiouylsz])\\1$"); - re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re2.test(w)) - w = w + "e"; - else if (re3.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - else if (re4.test(w)) - w = w + "e"; - } - } - - // Step 1c - re = /^(.+?)y$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(s_v); - if (re.test(stem)) - w = stem + "i"; - } - - // Step 2 - re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step2list[suffix]; - } - - // Step 3 - re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step3list[suffix]; - } - - // Step 4 - re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - re2 = /^(.+?)(s|t)(ion)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - if (re.test(stem)) - w = stem; - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = new RegExp(mgr1); - if (re2.test(stem)) - w = stem; - } - - // Step 5 - re = /^(.+?)e$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - re2 = new RegExp(meq1); - re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) - w = stem; - } - re = /ll$/; - re2 = new RegExp(mgr1); - if (re.test(w) && re2.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - - // and turn initial Y back to y - if (firstch == "y") - w = firstch.toLowerCase() + w.substr(1); - return w; - } -} - +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, is available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/locales/ar/LC_MESSAGES/booktheme.mo b/_static/locales/ar/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ar/LC_MESSAGES/booktheme.po b/_static/locales/ar/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/bg/LC_MESSAGES/booktheme.mo b/_static/locales/bg/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/bg/LC_MESSAGES/booktheme.po b/_static/locales/bg/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/bn/LC_MESSAGES/booktheme.mo b/_static/locales/bn/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/bn/LC_MESSAGES/booktheme.po b/_static/locales/bn/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ca/LC_MESSAGES/booktheme.mo b/_static/locales/ca/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ca/LC_MESSAGES/booktheme.po b/_static/locales/ca/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/cs/LC_MESSAGES/booktheme.mo b/_static/locales/cs/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/cs/LC_MESSAGES/booktheme.po b/_static/locales/cs/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/da/LC_MESSAGES/booktheme.mo b/_static/locales/da/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/da/LC_MESSAGES/booktheme.po b/_static/locales/da/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/de/LC_MESSAGES/booktheme.mo b/_static/locales/de/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/de/LC_MESSAGES/booktheme.po b/_static/locales/de/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/el/LC_MESSAGES/booktheme.mo b/_static/locales/el/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/el/LC_MESSAGES/booktheme.po b/_static/locales/el/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/eo/LC_MESSAGES/booktheme.mo b/_static/locales/eo/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/eo/LC_MESSAGES/booktheme.po b/_static/locales/eo/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/es/LC_MESSAGES/booktheme.mo b/_static/locales/es/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/es/LC_MESSAGES/booktheme.po b/_static/locales/es/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/et/LC_MESSAGES/booktheme.mo b/_static/locales/et/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/et/LC_MESSAGES/booktheme.po b/_static/locales/et/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/fi/LC_MESSAGES/booktheme.mo b/_static/locales/fi/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/fi/LC_MESSAGES/booktheme.po b/_static/locales/fi/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/fr/LC_MESSAGES/booktheme.mo b/_static/locales/fr/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/fr/LC_MESSAGES/booktheme.po b/_static/locales/fr/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/hr/LC_MESSAGES/booktheme.mo b/_static/locales/hr/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/hr/LC_MESSAGES/booktheme.po b/_static/locales/hr/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/id/LC_MESSAGES/booktheme.mo b/_static/locales/id/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/id/LC_MESSAGES/booktheme.po b/_static/locales/id/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/it/LC_MESSAGES/booktheme.mo b/_static/locales/it/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/it/LC_MESSAGES/booktheme.po b/_static/locales/it/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/iw/LC_MESSAGES/booktheme.mo b/_static/locales/iw/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/iw/LC_MESSAGES/booktheme.po b/_static/locales/iw/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ja/LC_MESSAGES/booktheme.mo b/_static/locales/ja/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ja/LC_MESSAGES/booktheme.po b/_static/locales/ja/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ko/LC_MESSAGES/booktheme.mo b/_static/locales/ko/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ko/LC_MESSAGES/booktheme.po b/_static/locales/ko/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/lt/LC_MESSAGES/booktheme.mo b/_static/locales/lt/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/lt/LC_MESSAGES/booktheme.po b/_static/locales/lt/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/lv/LC_MESSAGES/booktheme.mo b/_static/locales/lv/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/lv/LC_MESSAGES/booktheme.po b/_static/locales/lv/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ml/LC_MESSAGES/booktheme.mo b/_static/locales/ml/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ml/LC_MESSAGES/booktheme.po b/_static/locales/ml/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/mr/LC_MESSAGES/booktheme.mo b/_static/locales/mr/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/mr/LC_MESSAGES/booktheme.po b/_static/locales/mr/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ms/LC_MESSAGES/booktheme.mo b/_static/locales/ms/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ms/LC_MESSAGES/booktheme.po b/_static/locales/ms/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/nl/LC_MESSAGES/booktheme.mo b/_static/locales/nl/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/nl/LC_MESSAGES/booktheme.po b/_static/locales/nl/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/no/LC_MESSAGES/booktheme.mo b/_static/locales/no/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/no/LC_MESSAGES/booktheme.po b/_static/locales/no/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/pl/LC_MESSAGES/booktheme.mo b/_static/locales/pl/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/pl/LC_MESSAGES/booktheme.po b/_static/locales/pl/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/pt/LC_MESSAGES/booktheme.mo b/_static/locales/pt/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/pt/LC_MESSAGES/booktheme.po b/_static/locales/pt/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ro/LC_MESSAGES/booktheme.mo b/_static/locales/ro/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ro/LC_MESSAGES/booktheme.po b/_static/locales/ro/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ru/LC_MESSAGES/booktheme.mo b/_static/locales/ru/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ru/LC_MESSAGES/booktheme.po b/_static/locales/ru/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/sk/LC_MESSAGES/booktheme.mo b/_static/locales/sk/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/sk/LC_MESSAGES/booktheme.po b/_static/locales/sk/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/sl/LC_MESSAGES/booktheme.mo b/_static/locales/sl/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/sl/LC_MESSAGES/booktheme.po b/_static/locales/sl/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/sr/LC_MESSAGES/booktheme.mo b/_static/locales/sr/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/sr/LC_MESSAGES/booktheme.po b/_static/locales/sr/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/sv/LC_MESSAGES/booktheme.mo b/_static/locales/sv/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/sv/LC_MESSAGES/booktheme.po b/_static/locales/sv/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ta/LC_MESSAGES/booktheme.mo b/_static/locales/ta/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ta/LC_MESSAGES/booktheme.po b/_static/locales/ta/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/te/LC_MESSAGES/booktheme.mo b/_static/locales/te/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/te/LC_MESSAGES/booktheme.po b/_static/locales/te/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/tg/LC_MESSAGES/booktheme.mo b/_static/locales/tg/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/tg/LC_MESSAGES/booktheme.po b/_static/locales/tg/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/th/LC_MESSAGES/booktheme.mo b/_static/locales/th/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/th/LC_MESSAGES/booktheme.po b/_static/locales/th/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/tl/LC_MESSAGES/booktheme.mo b/_static/locales/tl/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/tl/LC_MESSAGES/booktheme.po b/_static/locales/tl/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/tr/LC_MESSAGES/booktheme.mo b/_static/locales/tr/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/tr/LC_MESSAGES/booktheme.po b/_static/locales/tr/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/uk/LC_MESSAGES/booktheme.mo b/_static/locales/uk/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/uk/LC_MESSAGES/booktheme.po b/_static/locales/uk/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/ur/LC_MESSAGES/booktheme.mo b/_static/locales/ur/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/ur/LC_MESSAGES/booktheme.po b/_static/locales/ur/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/vi/LC_MESSAGES/booktheme.mo b/_static/locales/vi/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/vi/LC_MESSAGES/booktheme.po b/_static/locales/vi/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo b/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/zh_CN/LC_MESSAGES/booktheme.po b/_static/locales/zh_CN/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo b/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo old mode 100755 new mode 100644 diff --git a/_static/locales/zh_TW/LC_MESSAGES/booktheme.po b/_static/locales/zh_TW/LC_MESSAGES/booktheme.po old mode 100755 new mode 100644 diff --git a/_static/minus.png b/_static/minus.png old mode 100755 new mode 100644 diff --git a/_static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css b/_static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css old mode 100755 new mode 100644 diff --git a/_static/plus.png b/_static/plus.png old mode 100755 new mode 100644 diff --git a/_static/pygments.css b/_static/pygments.css old mode 100755 new mode 100644 index 4d9ab4a..997797f --- a/_static/pygments.css +++ b/_static/pygments.css @@ -1,152 +1,152 @@ -html[data-theme="light"] .highlight pre { line-height: 125%; } -html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -html[data-theme="light"] .highlight .hll { background-color: #7971292e } -html[data-theme="light"] .highlight { background: #fefefe; color: #545454 } -html[data-theme="light"] .highlight .c { color: #797129 } /* Comment */ -html[data-theme="light"] .highlight .err { color: #d91e18 } /* Error */ -html[data-theme="light"] .highlight .k { color: #7928a1 } /* Keyword */ -html[data-theme="light"] .highlight .l { color: #797129 } /* Literal */ -html[data-theme="light"] .highlight .n { color: #545454 } /* Name */ -html[data-theme="light"] .highlight .o { color: #008000 } /* Operator */ -html[data-theme="light"] .highlight .p { color: #545454 } /* Punctuation */ -html[data-theme="light"] .highlight .ch { color: #797129 } /* Comment.Hashbang */ -html[data-theme="light"] .highlight .cm { color: #797129 } /* Comment.Multiline */ -html[data-theme="light"] .highlight .cp { color: #797129 } /* Comment.Preproc */ -html[data-theme="light"] .highlight .cpf { color: #797129 } /* Comment.PreprocFile */ -html[data-theme="light"] .highlight .c1 { color: #797129 } /* Comment.Single */ -html[data-theme="light"] .highlight .cs { color: #797129 } /* Comment.Special */ -html[data-theme="light"] .highlight .gd { color: #007faa } /* Generic.Deleted */ -html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ -html[data-theme="light"] .highlight .gh { color: #007faa } /* Generic.Heading */ -html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ -html[data-theme="light"] .highlight .gu { color: #007faa } /* Generic.Subheading */ -html[data-theme="light"] .highlight .kc { color: #7928a1 } /* Keyword.Constant */ -html[data-theme="light"] .highlight .kd { color: #7928a1 } /* Keyword.Declaration */ -html[data-theme="light"] .highlight .kn { color: #7928a1 } /* Keyword.Namespace */ -html[data-theme="light"] .highlight .kp { color: #7928a1 } /* Keyword.Pseudo */ -html[data-theme="light"] .highlight .kr { color: #7928a1 } /* Keyword.Reserved */ -html[data-theme="light"] .highlight .kt { color: #797129 } /* Keyword.Type */ -html[data-theme="light"] .highlight .ld { color: #797129 } /* Literal.Date */ -html[data-theme="light"] .highlight .m { color: #797129 } /* Literal.Number */ -html[data-theme="light"] .highlight .s { color: #008000 } /* Literal.String */ -html[data-theme="light"] .highlight .na { color: #797129 } /* Name.Attribute */ -html[data-theme="light"] .highlight .nb { color: #797129 } /* Name.Builtin */ -html[data-theme="light"] .highlight .nc { color: #007faa } /* Name.Class */ -html[data-theme="light"] .highlight .no { color: #007faa } /* Name.Constant */ -html[data-theme="light"] .highlight .nd { color: #797129 } /* Name.Decorator */ -html[data-theme="light"] .highlight .ni { color: #008000 } /* Name.Entity */ -html[data-theme="light"] .highlight .ne { color: #7928a1 } /* Name.Exception */ -html[data-theme="light"] .highlight .nf { color: #007faa } /* Name.Function */ -html[data-theme="light"] .highlight .nl { color: #797129 } /* Name.Label */ -html[data-theme="light"] .highlight .nn { color: #545454 } /* Name.Namespace */ -html[data-theme="light"] .highlight .nx { color: #545454 } /* Name.Other */ -html[data-theme="light"] .highlight .py { color: #007faa } /* Name.Property */ -html[data-theme="light"] .highlight .nt { color: #007faa } /* Name.Tag */ -html[data-theme="light"] .highlight .nv { color: #d91e18 } /* Name.Variable */ -html[data-theme="light"] .highlight .ow { color: #7928a1 } /* Operator.Word */ -html[data-theme="light"] .highlight .pm { color: #545454 } /* Punctuation.Marker */ -html[data-theme="light"] .highlight .w { color: #545454 } /* Text.Whitespace */ -html[data-theme="light"] .highlight .mb { color: #797129 } /* Literal.Number.Bin */ -html[data-theme="light"] .highlight .mf { color: #797129 } /* Literal.Number.Float */ -html[data-theme="light"] .highlight .mh { color: #797129 } /* Literal.Number.Hex */ -html[data-theme="light"] .highlight .mi { color: #797129 } /* Literal.Number.Integer */ -html[data-theme="light"] .highlight .mo { color: #797129 } /* Literal.Number.Oct */ -html[data-theme="light"] .highlight .sa { color: #008000 } /* Literal.String.Affix */ -html[data-theme="light"] .highlight .sb { color: #008000 } /* Literal.String.Backtick */ -html[data-theme="light"] .highlight .sc { color: #008000 } /* Literal.String.Char */ -html[data-theme="light"] .highlight .dl { color: #008000 } /* Literal.String.Delimiter */ -html[data-theme="light"] .highlight .sd { color: #008000 } /* Literal.String.Doc */ -html[data-theme="light"] .highlight .s2 { color: #008000 } /* Literal.String.Double */ -html[data-theme="light"] .highlight .se { color: #008000 } /* Literal.String.Escape */ -html[data-theme="light"] .highlight .sh { color: #008000 } /* Literal.String.Heredoc */ -html[data-theme="light"] .highlight .si { color: #008000 } /* Literal.String.Interpol */ -html[data-theme="light"] .highlight .sx { color: #008000 } /* Literal.String.Other */ -html[data-theme="light"] .highlight .sr { color: #d91e18 } /* Literal.String.Regex */ -html[data-theme="light"] .highlight .s1 { color: #008000 } /* Literal.String.Single */ -html[data-theme="light"] .highlight .ss { color: #007faa } /* Literal.String.Symbol */ -html[data-theme="light"] .highlight .bp { color: #797129 } /* Name.Builtin.Pseudo */ -html[data-theme="light"] .highlight .fm { color: #007faa } /* Name.Function.Magic */ -html[data-theme="light"] .highlight .vc { color: #d91e18 } /* Name.Variable.Class */ -html[data-theme="light"] .highlight .vg { color: #d91e18 } /* Name.Variable.Global */ -html[data-theme="light"] .highlight .vi { color: #d91e18 } /* Name.Variable.Instance */ -html[data-theme="light"] .highlight .vm { color: #797129 } /* Name.Variable.Magic */ -html[data-theme="light"] .highlight .il { color: #797129 } /* Literal.Number.Integer.Long */ -html[data-theme="dark"] .highlight pre { line-height: 125%; } -html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } -html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } -html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ -html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ -html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ -html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ -html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ -html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ -html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ -html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ -html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ -html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ -html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ -html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ -html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ -html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ -html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ -html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ -html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ -html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ -html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ -html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ -html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ -html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ -html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ -html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ -html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ -html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ -html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ -html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ -html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ -html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ -html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ -html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ -html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ -html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ -html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ -html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ -html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ -html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ -html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ -html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ -html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ -html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ -html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ -html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ -html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ -html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ -html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ -html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ -html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ -html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ -html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ -html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ -html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ -html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ -html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ -html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ -html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ -html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ -html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ -html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ -html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ -html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ -html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ -html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ -html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ -html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ -html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ -html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #7971292e } +html[data-theme="light"] .highlight { background: #fefefe; color: #545454 } +html[data-theme="light"] .highlight .c { color: #797129 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #d91e18 } /* Error */ +html[data-theme="light"] .highlight .k { color: #7928a1 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #797129 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #545454 } /* Name */ +html[data-theme="light"] .highlight .o { color: #008000 } /* Operator */ +html[data-theme="light"] .highlight .p { color: #545454 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #797129 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #797129 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #797129 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #797129 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #797129 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #797129 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #007faa } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #007faa } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #007faa } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #7928a1 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #7928a1 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #7928a1 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #7928a1 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #7928a1 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #797129 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #797129 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #797129 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #008000 } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #797129 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #797129 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #007faa } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #007faa } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #797129 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #008000 } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #7928a1 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #007faa } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #797129 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #545454 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #545454 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #007faa } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #007faa } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #d91e18 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #7928a1 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #545454 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #545454 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #797129 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #797129 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #797129 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #797129 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #797129 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #008000 } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #008000 } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #008000 } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #008000 } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #008000 } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #008000 } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #008000 } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #008000 } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #008000 } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #008000 } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #d91e18 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #008000 } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #007faa } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #797129 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #007faa } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #d91e18 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #d91e18 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #d91e18 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #797129 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #797129 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/sbt-webpack-macros.html b/_static/sbt-webpack-macros.html old mode 100755 new mode 100644 diff --git a/_static/scripts/bootstrap.js b/_static/scripts/bootstrap.js old mode 100755 new mode 100644 diff --git a/_static/scripts/bootstrap.js.LICENSE.txt b/_static/scripts/bootstrap.js.LICENSE.txt old mode 100755 new mode 100644 diff --git a/_static/scripts/bootstrap.js.map b/_static/scripts/bootstrap.js.map old mode 100755 new mode 100644 diff --git a/_static/scripts/pydata-sphinx-theme.js b/_static/scripts/pydata-sphinx-theme.js old mode 100755 new mode 100644 diff --git a/_static/scripts/pydata-sphinx-theme.js.map b/_static/scripts/pydata-sphinx-theme.js.map old mode 100755 new mode 100644 diff --git a/_static/scripts/sphinx-book-theme.js b/_static/scripts/sphinx-book-theme.js old mode 100755 new mode 100644 diff --git a/_static/scripts/sphinx-book-theme.js.map b/_static/scripts/sphinx-book-theme.js.map old mode 100755 new mode 100644 diff --git a/_static/searchtools.js b/_static/searchtools.js old mode 100755 new mode 100644 diff --git a/_static/sphinx-thebe.css b/_static/sphinx-thebe.css old mode 100755 new mode 100644 diff --git a/_static/sphinx-thebe.js b/_static/sphinx-thebe.js old mode 100755 new mode 100644 diff --git a/_static/styles/bootstrap.css b/_static/styles/bootstrap.css old mode 100755 new mode 100644 diff --git a/_static/styles/pydata-sphinx-theme.css b/_static/styles/pydata-sphinx-theme.css old mode 100755 new mode 100644 diff --git a/_static/styles/sphinx-book-theme.css b/_static/styles/sphinx-book-theme.css old mode 100755 new mode 100644 diff --git a/_static/styles/theme.css b/_static/styles/theme.css old mode 100755 new mode 100644 diff --git a/_static/tabs.css b/_static/tabs.css old mode 100755 new mode 100644 diff --git a/_static/tabs.js b/_static/tabs.js old mode 100755 new mode 100644 index d15e045..58d2cdd --- a/_static/tabs.js +++ b/_static/tabs.js @@ -2,6 +2,9 @@ var labels_by_text = {}; function ready() { var li = document.getElementsByClassName("tab-label"); + const urlParams = new URLSearchParams(window.location.search); + const tabs = urlParams.getAll("tabs"); + for (const label of li) { label.onclick = onLabelClick; const text = label.textContent; @@ -10,6 +13,12 @@ function ready() { } labels_by_text[text].push(label); } + + for (const tab of tabs) { + for (label of labels_by_text[tab]) { + label.previousSibling.checked = true; + } + } } function onLabelClick() { diff --git a/_static/togglebutton.css b/_static/togglebutton.css old mode 100755 new mode 100644 diff --git a/_static/togglebutton.js b/_static/togglebutton.js old mode 100755 new mode 100644 diff --git a/_static/underscore-1.13.1.js b/_static/underscore-1.13.1.js old mode 100755 new mode 100644 diff --git a/_static/underscore.js b/_static/underscore.js old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/LICENSE.txt b/_static/vendor/fontawesome/6.1.2/LICENSE.txt old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/css/all.min.css b/_static/vendor/fontawesome/6.1.2/css/all.min.css old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/webfonts/fa-brands-400.ttf b/_static/vendor/fontawesome/6.1.2/webfonts/fa-brands-400.ttf old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/webfonts/fa-brands-400.woff2 b/_static/vendor/fontawesome/6.1.2/webfonts/fa-brands-400.woff2 old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/webfonts/fa-regular-400.ttf b/_static/vendor/fontawesome/6.1.2/webfonts/fa-regular-400.ttf old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/webfonts/fa-regular-400.woff2 b/_static/vendor/fontawesome/6.1.2/webfonts/fa-regular-400.woff2 old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/webfonts/fa-solid-900.ttf b/_static/vendor/fontawesome/6.1.2/webfonts/fa-solid-900.ttf old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/webfonts/fa-solid-900.woff2 b/_static/vendor/fontawesome/6.1.2/webfonts/fa-solid-900.woff2 old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/webfonts/fa-v4compatibility.ttf b/_static/vendor/fontawesome/6.1.2/webfonts/fa-v4compatibility.ttf old mode 100755 new mode 100644 diff --git a/_static/vendor/fontawesome/6.1.2/webfonts/fa-v4compatibility.woff2 b/_static/vendor/fontawesome/6.1.2/webfonts/fa-v4compatibility.woff2 old mode 100755 new mode 100644 diff --git a/_static/webpack-macros.html b/_static/webpack-macros.html old mode 100755 new mode 100644 diff --git a/faq.html b/faq.html old mode 100755 new mode 100644 index c21de82..4fb0e80 --- a/faq.html +++ b/faq.html @@ -1,573 +1,573 @@ - - - - - - - - - - - - 🙋 FAQ — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - - -

    -
    -
    -
    -
    - - - -
    -
    - - - -
    - - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -

    🙋 FAQ

    - -
    - -
    -
    - - - - -
    - -
    -

    🙋 FAQ#

    -
    -

    General#

    -
    -

    /usr/bin/env: ‘python3\r’: No such file or directory#

    -

    $ rosrun lab1 mouse_client.py -/usr/bin/env: ‘python3\r’: No such file or directory

    -

    The problem are your line ending characters. Your file was created or edited on a Windows system and uses Windows/DOS-style line endings (CR+LF), whereas Linux systems like Ubuntu require Unix-style line endings (LF).

    -
      -
    • Sublime: Open the desired file with Sublime and from the top menu select View -> Line Endings and then the Windows(CRLF) or Unix(LF). That’s it.

    • -
    • VS Code: At the bottom right of the screen in VS Code there is a little button that says LF or CRLF: Click that button and change it to your preference.

    • -
    -
    -
    -
    -
    - - - - -
    - - - - - - -
    - - - - - - -
    -
    - - -
    - - -
    -
    -
    - - - - - -
    -
    - + + + + + + + + + + + + 🙋 FAQ — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    + + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +

    🙋 FAQ

    + +
    + +
    +
    + + + + +
    + +
    +

    🙋 FAQ#

    +
    +

    General#

    +
    +

    /usr/bin/env: ‘python3\r’: No such file or directory#

    +

    $ rosrun lab1 mouse_client.py +/usr/bin/env: ‘python3\r’: No such file or directory

    +

    The problem are your line ending characters. Your file was created or edited on a Windows system and uses Windows/DOS-style line endings (CR+LF), whereas Linux systems like Ubuntu require Unix-style line endings (LF).

    +
      +
    • Sublime: Open the desired file with Sublime and from the top menu select View -> Line Endings and then the Windows(CRLF) or Unix(LF). That’s it.

    • +
    • VS Code: At the bottom right of the screen in VS Code there is a little button that says LF or CRLF: Click that button and change it to your preference.

    • +
    +
    +
    +
    +
    + + + + +
    + + + + + + +
    + + + + + + +
    +
    + + +
    + + +
    +
    +
    + + + + + +
    +
    + \ No newline at end of file diff --git a/genindex.html b/genindex.html old mode 100755 new mode 100644 index 0f84f66..f5769e5 --- a/genindex.html +++ b/genindex.html @@ -1,444 +1,444 @@ - - - - - - - - - - - Index — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    - - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -

    - -
    -
    - -
    -
    -
    - - - - -
    - - -

    Index

    - -
    - -
    - - -
    - - - - -
    - -
    - -
    -
    -
    - -
    - -
    - -
    - - - - -
    -
    - - -
    - - -
    -
    -
    - - - - - -
    -
    - + + + + + + + + + + + Index — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    + + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +

    + +
    +
    + +
    +
    +
    + + + + +
    + + +

    Index

    + +
    + +
    + + +
    + + + + +
    + +
    + +
    +
    +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    + + +
    +
    +
    + + + + + +
    +
    + \ No newline at end of file diff --git a/index.html b/index.html old mode 100755 new mode 100644 index d09c1e8..3157386 --- a/index.html +++ b/index.html @@ -1 +1 @@ - + diff --git a/intro.html b/intro.html old mode 100755 new mode 100644 index 5f7e30f..587d590 --- a/intro.html +++ b/intro.html @@ -1,613 +1,613 @@ - - - - - - - - - - - - ECE 387 Introduction to Robotic Systems — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    - - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -

    ECE 387 Introduction to Robotic Systems

    - -
    - -
    -
    - - - - -
    - -
    -

    ECE 387 Introduction to Robotic Systems#

    -
    -

    👨‍🏫 Instructors#

    -
      -
    • neff

    • -
    • baek

    • -
    -
    -
    -

    📝 Course information#

    -
      -
    • Course Goal: Cadets shall design, implement, test, and debug robotics-based systems by developing Python programs incorporating built-in Robotics Operating System (ROS) functions and successfully interfacing the microcomputer with the external world.

    • -
    • Course Text: There is no printed textbook for ECE 387. Reading materials/labs are posted on this Course Web.

    • -
    • Syllabus: Posted here.

    • -
    • Course Schedule: Posted here and subject to change.

    • -
    -
    -
    -

    📡 Communication and Control (C2)#

    -
      -
    • All communication and announcement 📣 will be provided through MS Teams

    • -
    • All lecture 📓 materials will be provided through MS Teams.

    • -
    • Laboratory 🔬 work will be posted in this course web.

    • -
    • All assignments must be submitted in Gradescope

    • -
    • GitHub will be used for students to provide their source code 📄 for homework and laboratory assignments.

    • -
    -
    -
    -

    🤝 Collaboration Policy:#

    -

    Unless specifically directed otherwise, the collaboration policy for this course is:

    -

    Authorized resources: Any material from the ECE 382 course site and online sources regarding C programming syntax only. This does not include any solutions or solution stubs for challenges similar to those asked in any assignments.

    -
      -
    • For all assignments in this course, unless otherwise noted on the assignment, you may work with anyone. We expect all graded work, including code and written reports, to be in your own work. Copying another person’s work, with or without documentation, will result in NO 🚫 academic credit. Furthermore, copying without attribution is dishonorable and will be dealt with as an honor code violation.

    • -
    • All help received on work submitted for grading must be documented in accordance with the course documentation 📝 policy.

    • -
    • GRs are individual efforts. No collaboration is allowed while taking these exams. All electronic devices (phones, smartwatches, computers, tablets, etc.) must be placed out of sight for the duration of the event. If any electronic device is seen during the event, the student will receive a zero for that effort.

    • -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - -
    - - - - - - -
    - - - - - - -
    -
    - - -
    - - -
    -
    -
    - - - - - -
    -
    - + + + + + + + + + + + + ECE 387 Introduction to Robotic Systems — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    + + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +

    ECE 387 Introduction to Robotic Systems

    + +
    + +
    +
    + + + + +
    + +
    +

    ECE 387 Introduction to Robotic Systems#

    +
    +

    👨‍🏫 Instructors#

    +
      +
    • neff

    • +
    • baek

    • +
    +
    +
    +

    📝 Course information#

    +
      +
    • Course Goal: Cadets shall design, implement, test, and debug robotics-based systems by developing Python programs incorporating built-in Robotics Operating System (ROS) functions and successfully interfacing the microcomputer with the external world.

    • +
    • Course Text: There is no printed textbook for ECE 387. Reading materials/labs are posted on this Course Web.

    • +
    • Syllabus: Posted here.

    • +
    • Course Schedule: Posted here and subject to change.

    • +
    +
    +
    +

    📡 Communication and Control (C2)#

    +
      +
    • All communication and announcement 📣 will be provided through MS Teams

    • +
    • All lecture 📓 materials will be provided through MS Teams.

    • +
    • Laboratory 🔬 work will be posted in this course web.

    • +
    • All assignments must be submitted in Gradescope

    • +
    • GitHub will be used for students to provide their source code 📄 for homework and laboratory assignments.

    • +
    +
    +
    +

    🤝 Collaboration Policy:#

    +

    Unless specifically directed otherwise, the collaboration policy for this course is:

    +

    Authorized resources: Any material from the ECE 382 course site and online sources regarding C programming syntax only. This does not include any solutions or solution stubs for challenges similar to those asked in any assignments.

    +
      +
    • For all assignments in this course, unless otherwise noted on the assignment, you may work with anyone. We expect all graded work, including code and written reports, to be in your own work. Copying another person’s work, with or without documentation, will result in NO 🚫 academic credit. Furthermore, copying without attribution is dishonorable and will be dealt with as an honor code violation.

    • +
    • All help received on work submitted for grading must be documented in accordance with the course documentation 📝 policy.

    • +
    • GRs are individual efforts. No collaboration is allowed while taking these exams. All electronic devices (phones, smartwatches, computers, tablets, etc.) must be placed out of sight for the duration of the event. If any electronic device is seen during the event, the student will receive a zero for that effort.

    • +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + +
    + + + + + + +
    + + + + + + +
    +
    + + +
    + + +
    +
    +
    + + + + + +
    +
    + \ No newline at end of file diff --git a/objects.inv b/objects.inv old mode 100755 new mode 100644 diff --git a/python.html b/python.html old mode 100755 new mode 100644 index d15431d..afa3a0a --- a/python.html +++ b/python.html @@ -1,576 +1,576 @@ - - - - - - - - - - - - 🐍 Python Tutorials — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    - - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -

    🐍 Python Tutorials

    - -
    -
    - -
    -

    Contents

    -
    - -
    -
    -
    - - - - -
    - -
    -

    🐍 Python Tutorials#

    -
    -

    Online Tutorials#

    - -
    -
    -

    NumPy#

    - -
    -
    -

    Python Style Guide#

    - -
    -
    - - - - -
    - - - - - - -
    - - - -
    - - -
    -
    - - -
    - - -
    -
    -
    - - - - - -
    -
    - + + + + + + + + + + + + 🐍 Python Tutorials — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    + + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +

    🐍 Python Tutorials

    + +
    +
    + +
    +

    Contents

    +
    + +
    +
    +
    + + + + +
    + +
    +

    🐍 Python Tutorials#

    +
    +

    Online Tutorials#

    + +
    +
    +

    NumPy#

    + +
    +
    +

    Python Style Guide#

    + +
    +
    + + + + +
    + + + + + + +
    + + + +
    + + +
    +
    + + +
    + + +
    +
    +
    + + + + + +
    +
    + \ No newline at end of file diff --git a/resources.html b/resources.html deleted file mode 100755 index cbf9906..0000000 --- a/resources.html +++ /dev/null @@ -1,586 +0,0 @@ - - - - - - - - - - - - 💎 Resources — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    - - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - - - - - - -
    - -
    -

    💎 Resources#

    - -
    -

    Online 💻 Lecture Series by K. Lynch (Northwestern Univ.)#

    -
      -
    • Modern Robotics YouTube Channel

    • -
    • Textbook: Modern Robotics: mechanics, planning and control

    • -
    • Textbook Wiki

    • -
    • Textbook pdf

    • -
    • Coursera course (free to enroll)

    • -
    • Lynch’s course website

    • -
    -
    -
    -

    Linear Algebra ➕✖️#

    -

    The best Linear Algebra textbook in the world (I think)

    -
      -
    • textbook: Gilbert Strang, Introductioin to Linear Algebra

    • -
    • G. Strang’s phenomenal lecture videos at MIT

    • -
    • You can also find many of his lecture videos in YouTube.

    • -
    • If you want to go to graduate school and study robotics, computer vision, information system, machine learning, artificial intelligence, or data science, you should have a strong background in linear algebra.

    • -
    -
    -
    - - - - -
    - - - - - - -
    - - - - - - -
    -
    - - -
    - - -
    -
    -
    - - - - - -
    -
    - - \ No newline at end of file diff --git a/schedule.html b/schedule.html old mode 100755 new mode 100644 index 53c3586..30ad75c --- a/schedule.html +++ b/schedule.html @@ -1,704 +1,704 @@ - - - - - - - - - - - - 📆 Course Schedule — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    - - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -

    📆 Course Schedule

    - -
    -
    - -
    -
    -
    - - - - -
    - -
    -

    📆 Course Schedule#

    -
    -

    Note

    -

    This schedule is subject to change as appropriate.

    -
    -

    Last Updated: 21 Dec 2023

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Lesson

    Topic

    Due (Before Class)

    1

    Course Intro, Master & Robot, Git

    2

    Module 1: ROS

    3

    Quiz 1 & ICE 1

    4

    Module 2: Linux for Robotics

    5

    Quiz 2 & ICE 2

    6

    Module 3: Python 3 for Robotics

    7

    Quiz 3 & ICE3

    8

    ICE 3

    9

    Module 4: Driving the Robot, ICE 4

    10

    Module 5: Custom Messages, ICE5

    11

    Lab 1

    12

    Lab 1

    13

    Module 6: IMU, ICE 6

    Lab 1

    14

    Lab2

    15

    Lab2

    16

    Module 7: Launch File, ICE 7

    Lab 2

    17

    GR 1

    18

    Module 8: LIDAR, ICE 8

    19

    Lab 3

    20

    Lab 3

    21

    Module 9: Computer Vision, OpenCV, ICE 9

    Lab 3

    22

    Lab 4

    23

    Lab 4

    24

    Lab 4

    25

    Module 10: Computer Vision, AprilTags, ICE 10

    Lab 4

    26

    Lab 5

    27

    Lab 5

    28

    Lab 5

    29

    Final Project - Requirements

    Lab 5

    30

    Final Project - Design Briefs

    Briefings

    31

    Final Project - Implementation

    32

    Final Project

    33

    Final Project

    34

    Final Project

    35

    Final Project

    36

    Final Project

    37

    Final Project

    38

    Final Project

    39

    Final Project

    40

    Final Project - Demo

    Demo

    -
    - - - - -
    - - - - - - -
    - - - - -
    -
    - - -
    - - -
    -
    -
    - - - - - -
    -
    - + + + + + + + + + + + + 📆 Course Schedule — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    + + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +

    📆 Course Schedule

    + +
    +
    + +
    +
    +
    + + + + +
    + +
    +

    📆 Course Schedule#

    +
    +

    Note

    +

    This schedule is subject to change as appropriate.

    +
    +

    Last Updated: 21 Dec 2023

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    Lesson

    Topic

    Due (Before Class)

    1

    Course Intro, Master & Robot, Git

    2

    Module 1: ROS

    3

    Quiz 1 & ICE 1

    4

    Module 2: Linux for Robotics

    5

    Quiz 2 & ICE 2

    6

    Module 3: Python 3 for Robotics

    7

    Quiz 3 & ICE3

    8

    ICE 3

    9

    Module 4: Driving the Robot, ICE 4

    10

    Module 5: Custom Messages, ICE5

    11

    Lab 1

    12

    Lab 1

    13

    Module 6: IMU, ICE 6

    Lab 1

    14

    Lab2

    15

    Lab2

    16

    Module 7: Launch File, ICE 7

    Lab 2

    17

    GR 1

    18

    Module 8: LIDAR, ICE 8

    19

    Lab 3

    20

    Lab 3

    21

    Module 9: Computer Vision, OpenCV, ICE 9

    Lab 3

    22

    Lab 4

    23

    Lab 4

    24

    Lab 4

    25

    Module 10: Computer Vision, AprilTags, ICE 10

    Lab 4

    26

    Lab 5

    27

    Lab 5

    28

    Lab 5

    29

    Final Project - Requirements

    Lab 5

    30

    Final Project - Design Briefs

    Briefings

    31

    Final Project - Implementation

    32

    Final Project

    33

    Final Project

    34

    Final Project

    35

    Final Project

    36

    Final Project

    37

    Final Project

    38

    Final Project

    39

    Final Project

    40

    Final Project - Demo

    Demo

    +
    + + + + +
    + + + + + + +
    + + + + +
    +
    + + +
    + + +
    +
    +
    + + + + + +
    +
    + \ No newline at end of file diff --git a/search.html b/search.html old mode 100755 new mode 100644 index d85716b..ffff605 --- a/search.html +++ b/search.html @@ -1,456 +1,456 @@ - - - - - - - - - - Search - Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    - - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - -
    -

    Search

    - - - -
    -
    - - - - -
    - -
    - -
    -
    -
    - -
    - -
    - -
    - - - - -
    -
    - - -
    - - -
    -
    -
    - - - - - -
    -
    - + + + + + + + + + + Search - Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    + + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    + + +
    +

    Search

    + + + +
    +
    + + + + +
    + +
    + +
    +
    +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    + + +
    +
    +
    + + + + + +
    +
    + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js old mode 100755 new mode 100644 index 1efafc2..458169b --- a/searchindex.js +++ b/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["Appendix/GitSetup", "Appendix/MasterSetup", "Appendix/RobotSetup", "Module10_FinalProject/FinalProject", "Module1_ROS/ICE1_ListenerTalker", "Module1_ROS/ROS", "Module2_Linux/Linux", "Module3_Python3/ICE3_ClientServer", "Module3_Python3/Python3", "Module3_Python3/ROS", "Module3_Python3/RPSComputer", "Module4_DrivingTheRobot/DrivingTheRobot", "Module4_DrivingTheRobot/ICE4_DrivingTheRobot", "Module5_CustomMessages/CustomMessages", "Module5_CustomMessages/Lab1_CustomMessages", "Module6_IMU/IMU", "Module6_IMU/Lab2_IMU", "Module7_LaunchFile/LaunchFile", "Module8_LIDAR/LIDAR", "Module8_LIDAR/Lab3_LIDAR", "Module9_CV/Lab4_ComputerVision", "Module9_CV/computer_vision", "faq", "intro", "python", "schedule", "syllabus"], "filenames": ["Appendix\\GitSetup.md", "Appendix\\MasterSetup.md", "Appendix\\RobotSetup.md", "Module10_FinalProject\\FinalProject.md", "Module1_ROS\\ICE1_ListenerTalker.md", "Module1_ROS\\ROS.md", "Module2_Linux\\Linux.md", "Module3_Python3\\ICE3_ClientServer.md", "Module3_Python3\\Python3.md", "Module3_Python3\\ROS.md", "Module3_Python3\\RPSComputer.md", "Module4_DrivingTheRobot\\DrivingTheRobot.md", "Module4_DrivingTheRobot\\ICE4_DrivingTheRobot.md", "Module5_CustomMessages\\CustomMessages.md", "Module5_CustomMessages\\Lab1_CustomMessages.md", "Module6_IMU\\IMU.md", "Module6_IMU\\Lab2_IMU.md", "Module7_LaunchFile\\LaunchFile.md", "Module8_LIDAR\\LIDAR.md", "Module8_LIDAR\\Lab3_LIDAR.md", "Module9_CV\\Lab4_ComputerVision.md", "Module9_CV\\computer_vision.md", "faq.md", "intro.md", "python.md", "schedule.md", "syllabus.md"], "titles": ["Module 0: Setting Up GIT Repositories", "Master Setup", "Robot Setup", "Module 10: Final Project", "ICE1: Talker and Listener", "Robotics Operating System (ROS)", "Module 2: Linux for Robotics", "ICE3 - Client and Server", "Python3 for Robotics", "ICE3: ROS", "Module 3: Python3 for Robotics", "Driving the Robot", "ICE4: Driving the Robot", "Custom Messages", "Lab 1: Custom Messages", "Inertial Measurement Unit", "Lab 2: Inertial Measurement Unit", "Launch Files", "LIDAR", "Lab 3: LIDAR", "Lab 4: Computer Vision", "Computer Vision", "\ud83d\ude4b FAQ", "ECE 387 Introduction to Robotic Systems", "\ud83d\udc0d Python Tutorials", "\ud83d\udcc6 Course Schedule", "\ud83d\udccc Syllabus"], "terms": {"setup": [0, 5, 6, 17], "access": [0, 2, 4, 5, 6, 7, 8, 13, 21], "brows": [0, 4, 5, 7, 13, 15, 17, 18, 20, 21], "com": [0, 1, 2, 21], "you": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 26], "do": [0, 2, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16, 17, 19, 20, 21, 22, 26], "alreadi": [0, 1, 2, 4, 6, 7, 8, 11, 15, 20, 21], "have": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 26], "one": [0, 2, 4, 5, 6, 7, 8, 11, 12, 13, 17, 21], "It": [0, 2, 3, 4, 5, 6, 8, 11, 12, 18, 21, 26], "us": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19, 20, 22, 23, 26], "usernam": [0, 6, 13, 15, 16, 18, 19, 20, 21], "someth": [0, 8], "identifi": [0, 3, 20, 21], "e": [0, 1, 2, 3, 6, 7, 13, 15, 17, 20, 21, 26], "g": [0, 2, 3, 6, 13, 21, 26], "bneff1013": 0, "One": [0, 4, 6, 8], "student": [0, 2, 3, 6, 12, 23, 26], "per": [0, 4, 8, 13, 18, 21, 26], "group": [0, 2, 5, 6, 14, 21], "follow": [0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 26], "person": [0, 13, 14, 16, 19, 23, 26], "comput": [0, 1, 2, 4, 5, 6, 9, 11, 15, 23, 25, 26], "first": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 21, 26], "we": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 23, 26], "ar": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 21, 22, 23, 26], "go": [0, 4, 5, 6, 8, 11, 14, 17, 18, 21, 26], "thi": [0, 1, 2, 3, 5, 6, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 25, 26], "allow": [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 18, 20, 21, 23, 26], "instructor": [0, 4, 6, 13, 15, 17, 18, 21, 26], "see": [0, 2, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, 20, 21, 26], "all": [0, 1, 2, 3, 4, 5, 6, 8, 9, 11, 12, 13, 14, 15, 17, 18, 20, 21, 23, 26], "commit": [0, 11, 21], "throughout": [0, 2, 3, 4, 5, 8, 12, 21], "semest": [0, 21], "http": [0, 1, 2, 5, 6, 17], "ooh0u_xw": 0, "select": [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 17, 18, 21, 22], "accept": [0, 1, 2, 8, 14], "assign": [0, 3, 6, 8, 13, 23], "mai": [0, 1, 2, 4, 6, 7, 8, 11, 12, 13, 21, 23, 26], "need": [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "hit": [0, 1, 2, 4, 5, 6, 7, 12, 13, 19, 20, 21], "refresh": [0, 4, 12, 13, 20], "eventu": [0, 5, 13, 21], "provid": [0, 1, 2, 3, 4, 5, 8, 12, 13, 14, 15, 17, 18, 20, 21, 23, 26], "link": [0, 1, 2], "note": [0, 1, 2, 3, 6, 12, 13, 15, 17, 19, 20, 21, 23, 26], "url": 0, "save": [0, 1, 2, 5, 6, 13, 15, 17, 18, 19, 21], "best": [0, 5, 6, 12, 15, 26], "wai": [0, 2, 3, 4, 5, 6, 8, 18, 21], "check": [0, 2, 4, 6, 7, 8, 12, 13, 15, 16, 17, 26], "updat": [0, 6, 25], "manag": 0, "invit": 0, "team": [0, 3, 14, 16, 19, 20, 23, 26], "peopl": [0, 8], "member": 0, "user": [0, 1, 2, 4, 5, 6, 7, 9, 10, 12, 16, 17, 19, 20, 21, 24], "name": [0, 1, 2, 4, 6, 8, 12, 13, 17, 20, 21], "now": [0, 1, 2, 5, 6, 8, 9, 10, 11, 12, 15, 17, 21], "exact": [0, 8], "same": [0, 1, 2, 4, 5, 6, 8, 12, 13, 17, 21], "thing": [0, 2, 5, 6, 8, 17], "0mpq4tne": 0, "repeat": [0, 6, 12, 13, 20, 21], "step": [0, 1, 2, 3, 4, 7, 8, 17, 19, 20, 21, 24], "c": [0, 1, 2, 4, 5, 6, 7, 9, 12, 13, 15, 17, 18, 20, 21, 23, 26], "h": [0, 2, 21], "open": [0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22], "termin": [0, 2, 4, 6, 7, 8, 9, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21], "ctrl": [0, 4, 5, 6, 7, 8, 9, 12, 13, 15, 17, 18, 20, 21], "alt": [0, 4, 5, 6, 7, 8, 21], "t": [0, 1, 2, 4, 5, 6, 7, 8, 12, 13, 17, 18, 21, 26], "The": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 26], "1": [0, 1, 2, 3, 4, 6, 7, 8, 10, 12, 13, 16, 17, 18, 25], "2": [0, 2, 3, 4, 5, 7, 8, 12, 14, 17, 19, 25], "gener": [0, 1, 5, 13, 15, 17, 21], "new": [0, 1, 4, 5, 6, 7, 8, 12, 13, 15, 17, 18, 20, 21], "kei": [0, 5, 8, 17, 21], "substitut": [0, 11], "email": [0, 1, 2, 26], "address": [0, 1, 2, 6, 17, 21], "keygen": [0, 1, 2, 17], "ed25519": [0, 1, 2], "your_email": 0, "exampl": [0, 2, 5, 6, 8, 13, 14, 16, 17, 19, 20, 21, 26], "when": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 17, 19, 20, 21, 26], "re": [0, 8], "prompt": [0, 1, 2, 6, 17, 20], "enter": [0, 1, 2, 4, 6, 7, 8, 10, 17, 20, 21], "file": [0, 1, 2, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 18, 25], "which": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 13, 15, 16, 17, 18, 19, 21], "click": [0, 1, 2, 5, 6, 13, 15, 18, 21, 22], "At": [0, 1, 2, 4, 6, 7, 9, 11, 17, 18, 22], "type": [0, 1, 2, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16, 17, 18, 20, 21], "secur": [0, 1, 2, 6, 12, 15, 16, 17, 18, 21], "passphras": [0, 17], "start": [0, 1, 2, 3, 4, 5, 12, 14, 15, 17, 19, 21], "agent": [0, 1, 2], "background": [0, 1, 2, 4], "add": [0, 1, 4, 5, 6, 13, 14, 16, 17, 19, 20, 21], "privat": [0, 1, 2, 17], "eval": [0, 1, 2], "s": [0, 1, 2, 4, 5, 6, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 26], "id_ed25519": [0, 1, 2], "public": [0, 1, 2, 17], "nano": [0, 1, 2, 5, 6, 12, 13, 17, 18, 21], "pub": [0, 1, 2, 6, 8, 10, 13], "content": [0, 1, 2, 5, 6, 8, 13, 17], "maxim": [0, 1, 2], "window": [0, 1, 2, 4, 5, 6, 7, 8, 11, 12, 13, 15, 17, 18, 21, 22], "ensur": [0, 1, 2, 3, 4, 5, 8, 9, 12, 13, 15, 17, 18, 21], "ha": [0, 2, 3, 4, 5, 7, 8, 12, 18, 21], "end": [0, 3, 8, 14, 16, 19, 20, 21, 22], "right": [0, 1, 2, 3, 5, 8, 12, 14, 16, 17, 19, 21, 22], "copi": [0, 1, 2, 4, 6, 7, 8, 13, 15, 16, 17, 18, 19, 20, 23, 26], "web": [0, 1, 2, 23, 26], "browser": [0, 1, 2, 12, 13], "sign": [0, 1, 2, 3, 21], "In": [0, 1, 2, 4, 5, 6, 8, 9, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 26], "upper": [0, 1, 2, 21], "corner": [0, 1, 2, 12, 21], "ani": [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 18, 20, 21, 23, 26], "page": [0, 1, 2, 12, 14, 16, 19, 20], "profil": [0, 1, 2], "photo": [0, 1, 2], "sidebar": [0, 1, 2, 21], "gpg": [0, 1, 2], "titl": [0, 1, 2, 13], "field": [0, 1, 2, 6, 12, 14, 15, 21, 26], "descript": [0, 1, 2, 8, 13, 21, 26], "label": [0, 1, 2, 20, 21], "masterx": [0, 6, 17], "past": [0, 1, 2, 6], "If": [0, 2, 3, 4, 6, 8, 12, 13, 14, 17, 18, 19, 20, 21, 23, 26], "confirm": [0, 2, 8], "password": [0, 1, 6, 17], "shell": [0, 2, 6, 12, 15, 16, 17, 18, 21], "dfec3141": 0, "pi": [0, 6, 17, 18, 20, 21], "robotx": [0, 6, 17, 20, 21], "f": [0, 15, 20, 26], "j": [0, 5], "n": [0, 2, 8], "On": [0, 2, 11, 12, 16, 17, 19, 21], "mode": [0, 2], "workspac": [0, 1, 2, 5, 6, 15, 17, 19, 20, 21], "sourc": [0, 1, 2, 5, 6, 12, 13, 15, 17, 18, 19, 20, 21, 23, 26], "folder": [0, 5, 6, 9, 12, 13, 14, 16, 17, 19, 20, 21], "cd": [0, 1, 2, 5, 6, 13, 15, 17, 18, 20, 21], "master_w": [0, 1, 5, 6, 13, 15, 16, 18, 19, 20, 21], "src": [0, 1, 2, 5, 6, 13, 15, 16, 17, 18, 19, 20, 21], "replac": [0, 1, 2, 4, 5, 6, 8, 12, 13, 18, 21], "ece387": [0, 14], "ece387_master_spring202x": [0, 19, 20], "last": [0, 1, 2, 4, 5, 6, 7, 8, 13, 18, 21, 25], "mate": 0, "config": [0, 2, 21], "global": 0, "lastname1": 0, "lastname2": 0, "ro": [0, 3, 10, 11, 14, 15, 17, 18, 23, 25, 26], "directori": [0, 5, 6, 13, 17, 19, 21], "robot_w": [0, 2, 6, 17, 20], "ece387_robot_spring202x": [0, 20, 21], "guid": [1, 2, 4, 9, 11, 21], "walk": [1, 2], "through": [1, 2, 4, 5, 6, 8, 11, 12, 21, 23, 26], "server": [1, 2, 4, 6, 9, 11, 13, 15, 17, 18, 21], "22": [2, 25], "04": [1, 2], "lt": [1, 2], "ros2": [], "humbl": [], "4b": [], "embed": [1, 2, 5, 15, 17, 21], "within": [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "roboti": [1, 2, 11, 18], "burger": [1, 2, 5, 17], "along": [2, 12, 21], "usb": [2, 20], "system": [1, 2, 3, 4, 6, 11, 12, 13, 17, 18, 19, 21, 22, 26], "util": [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16, 17, 19, 20, 21, 26], "unit": [1, 2], "state": [1, 2, 3, 4, 8, 15, 21], "air": [1, 2], "forc": [1, 2, 11], "academi": [1, 2], "electr": [1, 2], "engin": [1, 2, 26], "depart": [1, 2, 26], "teach": [2, 6], "undergradu": 2, "adapt": [1, 2], "from": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 26], "manual": [1, 2, 5, 6], "below": [2, 3, 4, 5, 6, 8, 9, 13, 14, 15, 16, 18, 20, 21, 26], "list": [1, 2, 4, 5, 6, 9, 10, 12, 13, 17, 18, 21], "recommend": [2, 6, 11, 17], "other": [2, 4, 5, 6, 8, 11, 12, 13, 15, 17, 18, 19, 20, 21, 26], "off": [2, 4, 5, 6, 8, 12, 13, 15, 17, 18, 19, 20, 21], "shelf": [2, 21], "compon": [2, 8, 12, 13, 15, 18, 19, 20, 21], "can": [1, 2, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "ones": [2, 12], "cam": 2, "work": [1, 2, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 21, 23], "128": [2, 21], "gb": 2, "high": [2, 18], "speed": [2, 5, 8, 13, 18, 19, 20, 21], "monitor": [2, 6], "mous": [2, 5, 16], "keyboard": [1, 2, 5, 6, 11, 12, 13, 16], "an": [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 26], "older": [2, 18], "version": [1, 2, 8, 17, 21], "jetson": 2, "3b": [], "ot": [], "purchas": 2, "4": [2, 3, 4, 5, 7, 14, 19, 25], "model": [2, 4, 17], "b": [1, 2, 7, 21, 26], "prefer": [2, 14, 16, 19, 21, 22, 26], "8": [2, 4, 9, 21, 25], "ram": 2, "stop": [2, 3, 6, 13, 14, 15, 18, 19, 21], "after": [2, 3, 4, 5, 6, 7, 8, 12, 13, 20, 26], "A": [2, 3, 5, 6, 13, 16, 17, 19, 21, 26], "heat": 2, "sink": 2, "propertli": 2, "canakit": 2, "also": [2, 4, 8, 12, 13, 14, 15, 16, 19, 20, 21], "small": [2, 5, 15, 21], "fan": 2, "help": [2, 3, 4, 5, 6, 8, 13, 15, 16, 17, 18, 20, 21, 23, 26], "cool": 2, "3d": [1, 2, 11, 21], "print": [2, 4, 5, 6, 7, 8, 13, 15, 16, 18, 19, 21, 23, 26], "bracket": [2, 4, 7, 8], "mount": 2, "level": [2, 3, 5, 6, 8, 19], "prior": [2, 5, 26], "finish": [2, 8], "build": [1, 2, 3, 5, 9, 11, 14, 18, 20, 26], "cours": [2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 21], "found": [2, 14, 16, 19, 20, 21], "materi": [2, 23, 26], "two": [2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 18, 20, 21, 26], "front": [2, 4, 5, 6, 9], "standoff": 2, "There": [1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 17, 21, 23, 26], "multipl": [2, 8, 13, 17, 20], "imag": [1, 2], "easiest": [2, 6], "instruct": [1, 2, 4, 7, 8, 11, 12], "your": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 17, 18, 21, 22, 23, 26], "oper": [1, 2, 3, 4, 6, 8, 11, 12, 17, 23, 26], "os": [2, 6], "onc": [1, 2, 4, 5, 6, 8, 9, 12, 13, 15, 17, 18, 21], "choos": [2, 8], "button": [2, 12, 14, 20, 21, 22], "scroll": [2, 5, 6, 14], "down": [2, 6, 7, 13, 14, 15, 18, 20], "menu": [2, 4, 5, 9, 18, 21, 22], "purpos": 2, "next": [2, 4, 6, 7, 8, 15, 17, 20, 21, 26], "lastli": [2, 4, 6, 8, 18, 20, 26], "latest": [1, 2], "64": 2, "bit": [2, 5, 8, 21], "20": [1, 2, 11, 25, 26], "correct": [2, 8, 15, 18, 21], "storag": 2, "devic": [2, 15, 23, 26], "correspond": [2, 13, 15, 18, 21], "process": [2, 6, 8, 9, 11, 18, 20, 21], "overwrit": 2, "drive": [2, 3, 5, 6, 13, 14, 17, 19, 25], "so": [1, 2, 3, 4, 5, 6, 8, 13, 14, 15, 16, 17, 20, 21, 26], "befor": [2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, 21, 25, 26], "insert": [2, 15, 17, 18], "pop": [2, 21], "sure": [2, 3, 8, 9, 21], "write": [2, 3, 4, 5, 6, 8, 17, 20, 26], "complet": [2, 3, 4, 6, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "should": [2, 3, 4, 5, 6, 7, 8, 9, 12, 14, 15, 16, 17, 18, 19, 20, 21], "sd": 2, "power": [2, 6, 21], "connect": [1, 2, 6, 11, 12, 13, 15, 18, 21], "boot": [1, 2], "take": [1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 20, 21, 23, 26], "few": [2, 5, 6, 7, 8, 12, 13, 17, 21], "minut": [2, 3, 21, 26], "configur": [2, 17], "default": [1, 2, 8, 17, 21], "fail": [2, 3, 6, 8], "try": [2, 3, 4, 6, 7, 8, 20, 21], "again": [2, 4, 6, 8, 13, 21], "current": [2, 3, 5, 6, 12, 15, 16], "twice": [2, 17], "i": [2, 4, 5, 6, 8, 11, 14, 17, 18, 21], "like": [2, 5, 6, 8, 13, 18, 21, 22], "rememb": [2, 8, 12, 13, 14, 17, 21], "machin": [1, 2, 3, 17, 19, 20, 21], "anyth": [2, 4, 5, 13, 17], "temp": 2, "sudo": [1, 2, 5, 12, 18, 21], "addus": 2, "easi": [2, 4, 6, 7, 8, 10, 21, 24], "until": [2, 3, 4, 6, 7, 8, 11, 13, 14, 16], "back": [2, 4, 5, 6, 7, 21], "log": [2, 4], "out": [2, 3, 5, 6, 8, 21, 23, 26], "exit": [2, 4, 5, 6, 9, 12, 13, 15, 17, 18, 19, 20, 21], "account": [1, 2], "usermod": 2, "l": [2, 5, 16], "newusernam": 2, "d": [1, 2, 6, 7, 8, 13, 20, 21, 26], "home": [2, 5, 6, 17, 20, 21], "newhomedir": 2, "m": [2, 5, 12, 13, 18, 19, 21], "For": [1, 2, 3, 5, 6, 8, 11, 13, 17, 20, 21, 23, 26], "suermod": 2, "still": [2, 4, 5, 6, 7, 8, 12, 17, 26], "delet": [2, 6, 21], "delus": 2, "rm": [2, 6], "r": [1, 2, 6, 16, 21], "pwd": [2, 5], "chose": [2, 8], "good": [2, 6, 8, 12, 17, 21], "give": [2, 6, 8, 11, 17, 21], "each": [2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 15, 17, 18, 20, 21], "uniqu": [2, 8, 15, 26], "number": [1, 2, 3, 4, 5, 6, 7, 8, 13, 15, 21, 26], "0": [2, 3, 5, 6, 8, 10, 12, 13, 15, 16, 17, 18, 19, 20, 21, 26], "robot0": [2, 6, 17, 21], "command": [1, 2, 3, 9, 11, 12, 17, 18, 20, 21], "line": [1, 2, 4, 5, 6, 8, 9, 12, 13, 17, 18, 20, 21, 22], "editor": [1, 2, 5, 6, 12, 13, 15, 16, 17, 18, 19, 21], "choic": [1, 2, 8, 10, 21], "etc": [1, 2, 18, 21, 23, 26], "effect": [2, 11, 21], "reboot": 2, "don": [2, 4, 5, 6, 8, 12, 13, 14, 18, 21, 26], "yet": [2, 4, 5, 6], "though": [2, 4, 21, 26], "coupl": [2, 5, 14, 21], "more": [2, 3, 4, 5, 6, 8, 9, 12, 13, 14, 15, 17, 18, 21, 26], "accomplish": [1, 2, 3, 4, 6, 7, 8, 11, 12, 17, 18, 19, 26], "gui": [1, 2, 6, 13, 15, 18], "most": [2, 5, 6, 14, 21], "reliabl": 2, "method": [2, 4, 7, 13, 20, 21], "determin": [2, 4, 5, 6, 8, 12, 15, 16, 18, 21], "ip": [1, 2, 6], "typic": [2, 6, 8, 12, 17], "wlan0": [2, 6], "netplan": 2, "50": [2, 14, 16, 19, 20, 26], "cloud": 2, "init": [1, 2], "yaml": [2, 21], "favorit": [1, 2, 12, 21], "edit": [2, 6, 13, 14, 16, 17, 19, 21, 22], "look": [2, 5, 6, 8, 13, 14, 15, 17, 18, 21], "tab": [2, 4, 5, 6, 7, 8, 12, 17], "wireless": [2, 6], "interfac": [2, 6, 23, 26], "ssid": 2, "appli": [2, 5, 6, 15, 18, 20, 21], "benefici": 2, "static": 2, "To": [2, 4, 5, 6, 8, 9, 12, 13, 15, 21], "subnet": 2, "gatewai": 2, "rout": 2, "via": [1, 2, 11, 13, 14, 16, 17, 19, 20], "192": [2, 6], "168": [2, 6], "dev": [2, 17, 21], "proto": 2, "24": [2, 21, 25], "kernel": [2, 4, 5, 6, 9, 21], "scope": [2, 8], "201": 2, "rang": [2, 8, 18, 21], "inform": [2, 4, 5, 6, 7, 8, 9, 12, 13, 15, 21, 26], "datasourc": 2, "persist": 2, "across": [2, 5, 14], "instanc": [2, 5, 7, 8, 10, 13, 16, 17, 19], "capabl": [2, 11, 14, 18], "cfg": 2, "99": 2, "ethernet": [2, 6], "eth0": [2, 6], "dhcp4": 2, "true": [2, 6, 7, 8, 13, 15, 16, 17, 18, 20, 21], "wifi": 2, "point": [1, 2, 3, 4, 7, 11, 14, 16, 17, 18, 19, 20, 21], "nameserv": 2, "attempt": [2, 6, 26], "caus": [2, 4, 14], "issu": [2, 6, 15], "control": [2, 6, 8, 11, 12, 13, 15, 17, 21], "while": [1, 2, 4, 5, 6, 7, 8, 11, 15, 21, 23, 26], "habit": 2, "fine": 2, "applic": [1, 2, 3, 8, 17, 21, 26], "period": [2, 6, 8, 26], "auto": 2, "sudoedit": 2, "apt": [1, 2, 12, 18], "conf": 2, "20auto": 2, "packag": [1, 2, 4, 5, 6, 8, 10, 12, 14, 15, 16, 17, 18, 19, 21], "unattend": 2, "autocleaninterv": 2, "systemctl": 2, "our": [1, 2, 4, 5, 6, 7, 8, 11, 12, 13, 15, 17, 18, 20, 21], "might": [2, 4, 5, 6, 8, 13, 21], "necessari": [2, 5, 6, 7, 9, 17, 19, 21], "larger": 2, "activ": [2, 4, 5, 6, 9, 11, 12, 13, 14, 26], "free": [2, 6, 11, 21, 26], "falloc": 2, "program": [2, 11, 21, 23, 26], "creat": [1, 2, 5, 6, 7, 9, 12, 14, 15, 16, 17, 18, 21, 22], "2g": 2, "swapfil": 2, "wa": [2, 5, 6, 21, 22], "correctli": [2, 21], "logo": [], "figur": [6, 21], "swap2": [], "png": [], "make": [1, 2, 3, 5, 6, 7, 8, 9, 12, 13, 15, 17, 18, 19, 20, 21, 26], "onli": [2, 3, 5, 6, 8, 12, 14, 17, 18, 21, 23, 26], "root": [2, 5, 17], "chmod": [2, 6, 17], "600": 2, "permiss": [2, 6, 26], "read": [2, 4, 6, 7, 8, 10, 14, 15, 16, 17, 18, 19, 23, 26], "flag": 2, "Then": [2, 6, 8, 15, 21], "turn": [2, 5, 6, 12, 15, 18, 26], "swapon": 2, "perman": 2, "fstab": 2, "echo": [1, 2, 4, 5, 6, 9, 15, 18, 21], "none": [2, 8], "sw": 2, "tee": 2, "time": [1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 15, 17, 18, 20, 21, 26], "addition": [2, 6, 8, 11, 21], "observ": [2, 6, 12, 13, 15, 18], "output": [2, 3, 4, 5, 6, 8, 9, 12, 13, 15, 17, 18, 20, 21], "remot": [2, 21], "either": [2, 6, 8, 14, 21, 26], "dynam": 2, "dn": 2, "anoth": [2, 3, 4, 5, 8, 14, 16, 18, 21, 23, 26], "ip_address": 2, "sinc": [2, 5, 13, 21], "singl": [2, 5, 8, 21], "both": [2, 3, 4, 5, 8, 11, 13, 15, 17, 18, 21], "y": [1, 2, 4, 5, 6, 9, 13, 15, 17, 18, 21], "usafabot": 2, "about": [2, 4, 5, 6, 8, 9, 11, 12, 13, 17, 21], "run": [1, 2, 6, 9, 10, 12, 15, 18, 20, 21, 26], "includ": [1, 2, 3, 4, 5, 6, 8, 13, 14, 15, 16, 17, 19, 20, 21, 23, 26], "direct": [2, 5, 16, 21, 23, 26], "gnome": 2, "3": [2, 3, 4, 5, 7, 8, 11, 14, 15, 16, 17, 18, 25, 26], "environ": [1, 2, 5, 6, 17], "flexibl": [2, 5, 14, 21], "want": [2, 6, 8, 11, 13, 14, 20, 21], "remov": [2, 6, 13, 16, 19, 20, 21], "origin": [2, 11, 21], "top": [2, 4, 5, 6, 9, 12, 21, 22], "screen": [2, 4, 6, 9, 11, 14, 16, 17, 19, 20, 21, 22], "assum": [1, 2, 4, 21], "easier": [1, 2, 8, 13, 15], "entir": [1, 2, 5, 19, 20, 21], "python3": [1, 2, 6, 7, 13, 15, 18, 20, 21], "ubuntu20": [1, 2], "some": [1, 2, 4, 5, 6, 7, 8, 9, 12, 14, 16, 17, 21], "python": [1, 2, 4, 9, 10, 11, 13, 14, 15, 18, 20, 23, 25, 26], "instead": [1, 2, 6, 8, 12, 17, 18, 20, 21], "execut": [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 18, 19, 20, 21], "usr": [1, 2, 6, 13, 15, 18, 20], "bin": [1, 2, 6, 13, 15, 17, 18, 20, 21], "call": [1, 2, 4, 5, 6, 7, 11, 13, 14, 15, 17, 20, 21], "basic": [1, 2, 4, 6, 8, 13, 14, 26], "10": [1, 2, 4, 6, 11, 13, 16, 21, 25], "requir": [1, 2, 4, 5, 7, 8, 10, 11, 15, 17, 21, 22, 25, 26], "wiki": [1, 2, 4, 5, 12], "support": [1, 2, 21], "focal": [1, 2, 20], "org": [1, 2, 5], "sh": [1, 2, 6, 17], "deb": [1, 2], "lsb_releas": [1, 2], "sc": [1, 2], "main": [1, 2, 9, 15, 21], "curl": [1, 2], "haven": [1, 2, 5], "raw": [1, 2, 14], "githubusercont": [1, 2], "rosdistro": [1, 2], "master": [2, 5, 6, 9, 11, 12, 13, 14, 15, 18, 20, 21, 25], "asc": [1, 2], "base": [1, 2, 3, 6, 13, 14, 18, 21, 23, 26], "bare": [2, 21], "bone": 2, "minimum": [2, 11, 26], "commun": [1, 2, 4, 5, 6, 8, 9, 12], "librari": [1, 2, 5, 14, 15, 16, 21], "No": [2, 23, 26], "tool": [1, 2, 3, 4, 5, 6, 8, 11, 12, 13, 15, 17, 18, 21], "As": [2, 4, 5, 6, 8, 12, 15, 17, 21], "ideal": 2, "keep": [2, 13, 15, 17, 18, 21], "overhead": 2, "low": 2, "possibl": [2, 8, 21, 26], "mani": [2, 4, 5, 8, 11, 17, 18, 20, 21], "ran": [2, 4, 5, 6, 7, 9, 13, 15, 17, 18, 21], "rosdep": [1, 2], "rosinstal": [1, 2], "wstool": [1, 2], "pip": [1, 2, 14], "essenti": [1, 2, 14, 21], "initi": [1, 2, 5, 10, 15, 16, 19], "opt": [1, 2, 5, 12, 17, 18], "bash": [1, 2, 4, 5, 6, 9, 17], "mkdir": [1, 2, 6, 13, 17, 21], "p": [1, 2, 20], "catkin_mak": [1, 2, 6, 13, 15, 18], "variabl": [1, 2, 4, 7, 10, 12, 13, 16, 17, 19, 20, 21], "script": [1, 2, 4, 5, 6, 8, 13, 17, 20], "bashrc": [1, 2, 5, 12, 13, 15, 17, 18], "bottom": [1, 2, 5, 13, 21, 22], "rose": [1, 2, 12, 13, 17, 18], "commandbash": [], "devel": [1, 2, 5, 17], "export": [1, 2, 5, 17, 18], "ros_package_path": [1, 2, 5, 17], "share": [1, 2, 5, 12, 17, 18], "ros_hostnam": [1, 2, 17], "backtick": [1, 2], "apostroph": [1, 2], "ros_master_uri": [1, 2, 5, 17], "master_ip": [1, 2], "11311": [1, 2, 5, 17], "w": [1, 2, 5, 6, 12, 15, 17, 20, 21], "turtlebot3_model": [1, 2, 17, 18], "lds_model": [1, 2, 17, 18], "ld": [1, 2, 17, 18], "01": [1, 2, 17, 18], "02": [1, 2, 18], "lidar": [1, 2, 3, 5, 13, 25], "must": [1, 2, 6, 7, 8, 13, 14, 20, 21, 23, 26], "get": [1, 2, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "laser": [1, 2, 18, 19], "proc": [1, 2], "hl": [2, 18], "lfcd": [2, 18], "driver": [2, 18], "rgbd": [1, 2], "launch": [1, 2, 4, 5, 6, 7, 8, 11, 12, 15, 16, 18, 25], "rosseri": [1, 2], "arduino": [1, 2], "client": [1, 2, 9, 14], "msg": [1, 2, 4, 6, 7, 8, 10, 20], "amcl": [1, 2], "map": [1, 2, 5, 7, 18, 21], "move": [1, 2, 4, 5, 6, 7, 8, 9, 11, 13, 14, 15, 17, 18, 21], "urdf": [1, 2], "xacro": [1, 2], "compress": [1, 2], "transport": [1, 2], "rqt": [1, 2], "rviz": [1, 2, 5, 11, 17, 18], "gmap": [1, 2], "navig": [1, 2, 3, 5, 15, 17, 21], "interact": [1, 2, 5, 6, 11, 12, 21], "marker": [1, 2], "libudev": 2, "git": [1, 2, 21, 25], "clone": [1, 2, 21], "develop": [2, 5, 7, 11, 13, 14, 21, 23, 26], "ld08_driver": 2, "turtlebot3_simul": [1, 2], "af": [1, 2], "ece387_curriculum": [1, 2, 11, 21], "node": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 17, 18, 21], "path": [1, 2, 5, 6, 12, 18, 20, 21], "ignor": [1, 2, 3, 4, 5, 8, 12], "These": [1, 2, 5, 8, 12, 15, 17, 18, 21], "pip3": [1, 2], "roscd": [1, 2, 6, 11, 13, 15, 17, 18, 20, 21], "txt": [1, 2], "dlib": [1, 2, 20, 21], "quit": [1, 2, 8], "board": [2, 15, 16, 21], "dpkg": 2, "architectur": [1, 2], "armhf": 2, "libc6": 2, "opencr_port": 2, "ttyacm0": [2, 17], "opencr_model": 2, "burger_noet": 2, "rf": 2, "opencr_upd": 2, "tar": [2, 21], "bz2": 2, "loader": [2, 17], "extract": [2, 21], "wget": 2, "binari": 2, "ros1": 2, "xvf": 2, "upload": [2, 9], "success": [2, 3, 6, 21, 26], "debug": [2, 4, 23, 26], "steve": [], "beyer": [2, 13], "2022": [], "stan": [], "baek": [], "2023": 25, "desktop": 1, "network": [1, 4, 5, 6, 21], "ground": [1, 5, 21], "robot": [1, 3, 13, 14, 15, 16, 18, 19, 21, 25, 26], "host": [1, 5, 11], "roscor": [1, 4, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16, 17, 18, 21], "intel": 1, "nuc": 1, "kit": 1, "amd64": 1, "bootabl": 1, "stick": [1, 8], "maco": 1, "well": [1, 5, 6, 8, 12, 16, 21], "master0": [1, 6], "press": [1, 4, 5, 6, 7, 8, 10, 14, 17, 20, 21], "hold": [1, 4, 5, 8, 21], "f10": 1, "startup": [1, 2], "login": 1, "up": [1, 3, 4, 6, 7, 11, 12, 14, 18, 21, 26], "set": [1, 6, 7, 8, 12, 13, 15, 16, 18, 19, 20, 21], "jupyt": [1, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 18, 21], "notebook": [1, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 18, 21], "contrib": 1, "nbextens": 1, "full": [1, 26], "theminimum": 1, "2d": 1, "percept": [1, 11, 21], "xterm": [1, 17], "hostnam": [1, 6, 17], "chang": [1, 4, 5, 6, 7, 8, 13, 15, 18, 21, 22, 23, 25], "joi": 1, "teleop": [1, 12], "twist": [1, 5, 6, 12, 13, 14, 16, 19], "dynamixel": 1, "sdk": 1, "automat": [1, 6, 17, 20, 21], "previou": [3, 5, 6, 12, 13, 21], "dfec": [3, 5, 6, 11, 14, 20, 26], "hall": [3, 5], "center": [3, 14, 15], "between": [3, 4, 5, 6, 8, 9, 11, 12, 13, 18, 20, 21], "wall": [3, 19], "imu": [3, 5, 13, 16, 17, 25], "90": [3, 6, 15, 16, 19, 21, 26], "180": [3, 15, 16, 18, 21], "360": [3, 15, 16, 18], "deg": [3, 16, 18], "opencv": [3, 20, 25], "randomli": [3, 8], "place": [3, 6, 8, 11, 15, 20, 21, 23, 26], "act": 3, "accordingli": [3, 6, 7], "apriltag_ro": [3, 20], "certain": [3, 13], "distanc": [3, 18, 19, 21], "apriltag": [3, 20, 25], "left": [3, 4, 5, 7, 8, 12, 14, 16, 18, 19, 21], "around": [3, 5, 6, 8, 20, 21], "reach": [3, 8, 16], "goal": [3, 16, 21, 23], "depend": [3, 8, 13, 15, 16, 19], "tag": [3, 6, 13, 15, 17, 18, 19, 21], "id": [3, 6, 17, 20, 21], "complex": [3, 5, 17], "task": [3, 5, 8, 12, 21, 26], "plan": [3, 26], "just": [3, 4, 6, 8, 12, 13, 15, 18, 21, 26], "code": [3, 4, 5, 6, 7, 9, 10, 11, 14, 15, 16, 18, 19, 20, 22, 23, 26], "solut": [3, 18, 23, 26], "therefor": [3, 15, 17, 21], "phase": 3, "focu": [3, 4], "graph": [3, 14], "finit": 3, "notion": 3, "powerpoint": 3, "digit": [3, 8, 15], "lot": [3, 4, 5, 6, 13, 17, 19, 26], "practic": [3, 6, 7, 11, 12, 13, 14, 15, 21], "rqt_graph": [3, 4, 5, 6, 9, 12, 13, 15, 16, 17, 18], "topic": [3, 4, 5, 6, 7, 9, 10, 12, 13, 14, 15, 16, 18, 19, 20, 21, 25], "forese": 3, "great": [3, 8], "logic": [3, 8], "flow": [3, 12], "problem": [3, 5, 17, 22, 26], "visual": [3, 5, 11, 13, 17, 18, 21], "how": [3, 4, 7, 8, 9, 11, 12, 13, 15, 17, 18, 19, 20, 21], "transit": [3, 17], "condit": [3, 21], "forward": [3, 5, 6, 7, 14, 19], "input": [3, 4, 5, 7, 10, 16, 19, 20, 21], "receiv": [3, 4, 7, 8, 10, 13, 18, 23, 26], "sensor": [3, 5, 11, 15, 17, 18, 26], "rest": [3, 4, 5], "simplic": [3, 21], "moor": 3, "reli": 3, "quick": [3, 6], "intro": [3, 25], "ece382": 3, "here": [3, 4, 5, 8, 10, 14, 17, 21, 23, 26], "7": [2, 3, 4, 21, 25], "part": [3, 13, 14, 15], "fsm": 3, "report": [3, 4, 6, 23, 26], "section": [3, 7, 14, 17, 18, 19, 20, 21, 26], "under": [3, 6, 14, 16, 19, 20, 21, 26], "class": [3, 4, 5, 6, 9, 10, 11, 12, 15, 16, 17, 18, 19, 20, 25, 26], "than": [3, 4, 8, 12, 16, 18, 26], "undisclos": 3, "locat": [3, 5, 6, 15, 20, 21], "larg": [3, 17, 21], "travel": [3, 15], "approxim": [3, 21], "meter": 3, "wide": [3, 5, 13, 21], "stai": [3, 8], "middl": [3, 8, 21], "halfwai": 3, "intersect": 3, "wait": [2, 3, 4, 5, 7, 8, 21], "five": [3, 5], "second": [3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 18, 21], "continu": [3, 6, 8, 12, 21], "door": 3, "discov": [3, 5], "5": [3, 4, 5, 6, 11, 12, 14, 16, 18, 25, 26], "lesson": [3, 6, 13, 15, 17, 18, 21, 25, 26], "40": [3, 25], "random": [3, 8, 10, 21], "deduct": 3, "checkpoint": 3, "doe": [3, 4, 6, 13, 14, 21, 23, 26], "rubric": 3, "item": [3, 8, 9, 10, 26], "worth": [3, 26], "6": [2, 3, 4, 15, 25], "total": [2, 3, 8, 21, 26], "60": [2, 3, 26], "apriltag0": 3, "apriltag1": 3, "apriltag2": 3, "apriltag3": 3, "intersectiom": 3, "detect": [3, 5, 14, 18, 20, 21], "30": [2, 3, 18, 25], "warn": [3, 16, 21], "25": [3, 5, 13, 14, 16, 19, 20, 25, 26], "grade": [3, 19, 20, 23], "known": [4, 6, 7, 8, 10, 20, 21], "academia": [4, 6, 7, 8, 10], "industri": [4, 6, 7, 8, 10], "text": [4, 6, 7, 8, 10, 13, 14, 23], "coexist": [4, 6, 7, 8, 10, 16], "veri": [4, 6, 7, 8, 10, 11, 15, 18, 19, 21, 26], "format": [4, 6, 7, 8, 10, 14, 16, 17, 19, 20, 21], "block": [4, 5, 6, 7, 8, 9, 10, 13, 21], "contain": [4, 6, 7, 8, 10, 13, 21], "shift": [4, 5, 6, 7, 8, 10], "earlier": [4, 5, 6, 7, 8, 10, 17, 21], "later": [4, 5, 6, 7, 8, 10, 26], "know": [4, 7, 8, 13, 17, 21, 26], "cell": [4, 7, 8, 21], "charact": [4, 5, 6, 7, 8, 9, 22], "gone": [4, 7, 8], "rospi": [4, 6, 7, 8, 10, 13, 15, 16, 18, 19, 20, 21], "std_msg": [4, 6, 7, 8, 10, 13, 14, 15, 16, 19, 20, 21], "string": [4, 7, 8, 10, 13], "messag": [4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 18, 19, 20, 21, 25], "def": [4, 6, 7, 8, 10, 13, 15, 16, 18, 20], "callback_chat": 4, "abov": [4, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21], "everi": [4, 5, 7, 8, 13, 17], "sent": [4, 5, 6, 7, 8, 9, 12, 13, 15, 16, 19, 21], "over": [4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 18, 20, 21], "interrupt": [4, 8], "spin": [4, 7, 10, 13, 15, 18, 20], "thread": 4, "displai": [4, 6, 8, 9, 12, 18, 20, 21], "what": [4, 5, 6, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21, 26], "loginfo": [4, 6], "get_caller_id": 4, "heard": 4, "data": [4, 5, 8, 10, 13, 15, 17, 18, 20, 21], "paramet": [4, 12, 17, 21], "meaning": 4, "actual": [4, 8, 11, 14, 16, 19, 20, 21], "attribut": [4, 5, 8, 23, 26], "mesag": 4, "why": [4, 5], "init_nod": [4, 6, 8, 10, 13, 15, 18, 20], "except": [4, 6, 7, 20, 26], "rosinterruptexcept": [4, 6], "pass": [4, 6, 8, 13, 14, 20], "similar": [4, 7, 12, 13, 17, 18, 19, 20, 23, 26], "infinit": 4, "loop": [4, 20, 21], "__name__": [4, 6, 8, 13, 15, 18, 20], "__main__": [4, 6, 8, 13, 15, 18, 20], "info": [4, 5, 9, 12, 13, 14, 18, 21], "1666756858": 4, "595079": 4, "308": 4, "564000": 4, "hello": [4, 7], "send": [4, 5, 7, 8, 9, 10, 12, 13, 14, 17, 18, 26], "show": [2, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, 20, 21], "delv": 4, "littl": [4, 22, 26], "deeper": 4, "explicitli": [4, 7, 8], "dure": [4, 6, 7, 8, 11, 12, 18, 21, 23, 26], "portion": [4, 7, 8, 15, 21], "advantag": [4, 6, 7, 8, 21], "shortcut": [4, 6, 7, 8], "ic": [4, 5, 9, 13, 25], "could": [4, 5, 6, 8, 21], "put": [4, 8, 21], "would": [4, 5, 6, 8, 9, 11, 12, 13, 21], "knowledg": [4, 5, 6, 11, 15, 18, 21], "regard": [4, 21, 23, 26], "standard": [4, 9, 26], "common": [4, 17, 21], "find": [4, 8, 11, 12, 13, 17, 18, 20, 21, 26], "chat_pub": 4, "queue_siz": [4, 6, 8, 10, 13], "rate": [4, 6, 12, 19], "hz": [4, 5, 6, 13], "is_shutdown": 4, "chat_str": [4, 7], "sleep": [2, 4, 6], "drop": 4, "old": [4, 13], "faster": [4, 8], "handl": [4, 7, 13, 14, 15], "With": [4, 8, 9], "previous": [4, 6, 13, 21], "desir": [4, 12, 19, 22], "statement": [4, 16, 19, 20], "realli": [4, 11, 21], "cleanli": 4, "side": [4, 13, 14, 21], "That": [4, 5, 6, 8, 17, 22], "due": [4, 21, 25, 26], "mean": [4, 5, 6, 8], "those": [4, 6, 8, 9, 13, 21, 23, 26], "investig": [4, 18], "equival": 4, "linux": [4, 5, 11, 12, 22, 25, 26], "ice1_ro": [], "lab": [4, 6, 11, 12, 13, 15, 17, 18, 21, 23, 25, 26], "NOT": [4, 5, 6, 9, 14, 21], "rosnod": [4, 5, 9, 13, 21], "rosout": [4, 12, 13, 17], "facilit": 4, "rostop": [4, 5, 6, 9, 12, 13, 18, 21], "expect": [4, 8, 12, 16, 21, 23, 26], "them": [4, 6, 7, 8, 9, 11, 12, 17, 21, 26], "close": [4, 5, 6, 8, 9, 11, 12, 13, 15, 17, 18, 21, 26], "rosmsg": [4, 5, 9, 12, 13, 14, 18], "saw": [4, 5, 8], "ice1_talk": 4, "reset": [4, 6, 9], "clear": [4, 5, 6, 9, 21, 26], "bar": [4, 5, 9, 21], "restart": [4, 5, 9, 20, 21], "shutdown": [4, 6, 7, 9, 13, 15, 17, 18, 21], "accompani": [5, 6], "introduct": [6, 21], "notetak": [5, 6], "learn": [5, 6, 7, 9, 12, 13, 14, 15, 18, 21, 26], "turltebot3": 5, "argument": [5, 6, 19, 21], "ls": [2, 5, 6], "notic": [5, 13, 14, 17], "priveleg": 5, "indic": [5, 6, 8, 15, 17, 18, 21], "anywher": 5, "absolut": [5, 6, 11, 12, 18], "noetic": [5, 12, 17, 18], "three": [5, 6, 8, 21], "built": [5, 8, 12, 13, 14, 17, 18, 21, 23, 26], "establish": [5, 17, 21], "x": [5, 6, 12, 13, 15, 16, 17, 18, 19, 20, 21], "implement": [5, 8, 10, 23, 25, 26], "howev": [5, 6, 11, 14, 15, 17, 21], "let": [5, 6, 12, 17, 21, 26], "plai": [5, 8], "virtual": [5, 11], "integr": [5, 15, 16, 18, 19, 20], "ultim": [5, 21], "perform": [5, 8, 15, 21], "specif": [5, 8, 13, 14, 17, 21, 23, 26], "dedic": 5, "But": [5, 21], "pro": [5, 12], "tip": [5, 6, 11, 12, 15, 18, 19, 20], "exist": [5, 13, 18, 19, 21], "occupi": 5, "gazebo": [5, 11], "roslaunch": [5, 6, 11, 12, 15, 18, 21], "turtlebot3_gazebo": [5, 6, 11], "turtlebot3_world": [5, 11], "syntax": [5, 6, 12, 13, 23, 26], "launchfil": [5, 12, 15], "turtlebot3_gazebo_rviz": [5, 11], "physic": [5, 8, 11], "enabl": [5, 7, 8, 11, 12, 13, 16, 18, 21], "test": [5, 8, 16, 17, 19, 23, 26], "algorithm": [5, 11, 21, 26], "real": [5, 11, 21], "world": [5, 11, 23, 26], "sit": 5, "face": 5, "posit": [5, 6, 8, 14, 15, 21], "surround": [5, 8, 21], "maze": [5, 11, 26], "pan": 5, "wheel": [5, 6, 8, 14], "orient": [5, 13, 15, 16, 20], "camera": [5, 18, 20], "zoom": 5, "red": [5, 18, 21], "dot": [5, 18], "outlin": [5, 21, 26], "obstacl": [5, 18, 19], "object": [5, 8, 13, 15, 18, 20, 21], "scan": [5, 18, 21], "nice": [5, 7, 13, 15], "thei": [5, 6, 8, 15, 21, 26], "were": [5, 8, 12, 15, 17, 18, 21], "worri": 5, "ahead": 5, "nb": 5, "third": [5, 8, 11, 21], "fourth": [5, 11], "Not": [5, 20, 21, 26], "too": [5, 12], "excit": [5, 12], "cmd_vel": [5, 6, 12, 13, 14], "hardwar": [5, 11, 18, 26], "movement": [5, 14], "publish": [5, 6, 7, 8, 9, 10, 12, 14, 15, 16, 17, 18, 19, 20, 21], "interest": [5, 15, 16, 17, 18, 19, 21, 26], "linear": [5, 6, 12, 13, 15, 16, 19, 21], "angular": [5, 6, 12, 13, 15, 16, 18, 19], "z": [5, 6, 13, 15, 16, 19], "valu": [5, 8, 13, 15, 16, 17, 18, 19, 20, 21], "dimension": 5, "space": [5, 13, 21], "googl": [5, 8, 12, 13, 20], "dimens": 5, "backward": [5, 14], "luckili": [5, 15], "pre": [5, 12, 13, 17, 18, 21], "teleop_twist_keyboard": [5, 11, 12], "letter": 5, "autocomplet": 5, "rosrun": [5, 6, 11, 12, 13, 17, 20, 21, 22], "py": [5, 6, 11, 12, 13, 14, 15, 17, 18, 21, 22], "decreas": 5, "rad": [5, 6, 12, 16, 18], "followd": 5, "subscrib": [5, 6, 7, 8, 9, 10, 12, 14, 16, 19, 20], "where": [5, 7, 11, 14, 15, 18, 21, 26], "arc": 5, "u": [5, 6, 8], "o": [5, 6, 20], "futur": [5, 6, 8, 11, 12, 14, 15, 21, 26], "kill": [5, 6, 9, 17, 18, 21], "geometry_msg": [5, 6, 13, 15, 16, 18, 19], "Or": [5, 21], "combin": [5, 8, 15, 21], "exercis": [5, 12, 26], "document": [6, 17, 21, 23, 26], "ubuntu": [6, 22], "went": 6, "repres": [6, 8, 15, 20, 21], "blink": 6, "cursor": [6, 14], "readi": [6, 12, 20], "my_fold": 6, "highlight": [6, 14, 21], "turtlebot3": [6, 12, 13, 15, 16, 17, 18, 19], "touch": [6, 13, 15, 17, 18, 21], "move_turtlebot": 6, "regular": 6, "turtlebot": [6, 8, 11, 14], "wwiii": 6, "fought": 6, "option": [6, 7, 21], "vim": 6, "emac": 6, "simpl": [6, 8, 13, 17, 18, 21], "feel": [6, 21, 26], "whichev": 6, "guidanc": 6, "arg1": 6, "15": [2, 6, 25], "elif": [6, 8, 16], "rotat": [6, 15, 16, 17, 18], "els": [6, 8, 11, 12, 13, 15, 16, 17, 18], "pleas": [6, 8, 26], "fi": 6, "exactli": [6, 18], "simul": [6, 12, 14], "turtlebot3_empty_world": 6, "did": [6, 8, 13, 17, 21], "error": [6, 7, 12, 16, 18, 19, 21], "becaus": [5, 6, 8, 21], "been": [6, 8], "properli": [6, 8, 20], "la": 6, "rw": [2, 6], "owner": 6, "permis": 6, "isn": [6, 17], "ad": [6, 8], "rwxrwxr": 6, "github": [6, 11, 12, 21, 23, 26], "repo": [6, 9, 11, 12, 14, 15, 21], "ece387_master_sp23": 6, "homework": [6, 23, 26], "catkin_create_pkg": [6, 13, 15, 18, 20, 21], "module02": 6, "roscpp": 6, "further": [6, 8, 21], "alwai": [6, 8, 12, 18], "idea": [6, 8, 12, 26], "compil": [6, 8, 15, 26], "begin": [6, 8, 17], "bash_script": 6, "my_script": [6, 21], "move_turtlebot_squar": 6, "modifi": [6, 8, 13, 21], "without": [6, 8, 13, 17, 20, 21, 23, 26], "done": [6, 11, 17, 20, 26], "man": 6, "whole": [6, 21], "thier": 6, "recurs": 6, "cp": 6, "mv": 6, "destin": 6, "otherwis": [6, 21, 23, 26], "env": [6, 13, 15, 17, 18, 20], "import": [6, 9, 10, 11, 15, 16, 17, 18, 19, 20, 21, 26], "math": [6, 18, 21], "moveturtlebot": 6, "__init__": [6, 7, 8, 10, 13, 15, 16, 18, 19, 20], "self": [6, 7, 8, 10, 13, 14, 15, 16, 18, 19, 20], "cmd": [6, 16], "ctrl_c": [6, 7, 13, 15, 18, 20], "fals": [6, 7, 8, 13, 15, 16, 18, 19, 20, 21], "on_shutdown": [6, 7, 13, 15, 18, 20], "shutdownhook": [6, 7, 13, 15, 18, 20], "publish_cmd_vel_onc": 6, "case": [6, 8, 17, 21], "get_num_connect": 6, "break": 6, "shut": [6, 7, 13, 15, 18, 20], "stop_turtlebot": 6, "move_tim": 6, "moving_tim": 6, "lin_spd": 6, "ang_spd": 6, "move_squar": 6, "sec": [2, 6], "radian": 6, "move_object": 6, "hopefulli": 6, "sens": 6, "summar": 6, "squar": [6, 21], "degre": [6, 15, 16, 18, 19, 21], "won": [6, 14, 21], "perfect": [6, 8], "doesn": [6, 8], "perfectli": 6, "suspend": [2, 6], "ps": 6, "faux": 6, "grep": 6, "filter": [6, 21], "entri": [6, 7], "leftmost": 6, "pid": 6, "unfortun": 6, "avail": [2, 6, 17], "rospack": [6, 12, 18], "vertic": [6, 21], "pipe": 6, "result": [5, 6, 8, 12, 21, 23, 26], "unplug": [6, 21], "raspberri": 6, "uesrnam": 6, "ping": 6, "ssh": [6, 12, 13, 15, 17, 18, 21], "store": [6, 7, 8, 10, 13, 14, 16, 17, 19, 20], "ece387_robot_sp23": 6, "resolut": [6, 18], "vice": [6, 8], "versa": [6, 8], "120": 6, "plug": [6, 21], "addr": 6, "inet": 6, "wlo1": 6, "roobt": 6, "scp": [6, 20, 21], "dest": [6, 20], "jupter": [6, 9], "captur": [6, 11], "bring": [6, 11, 17], "pictur": [6, 18, 20, 21], "push": [6, 11, 12, 14, 15, 16, 19, 20], "credit": [6, 15, 23, 26], "seen": [6, 20, 23, 26], "differ": [6, 8, 11, 14, 15, 17, 18, 20, 21], "scratch": [6, 21], "surfac": [6, 21], "ton": 6, "onlin": [2, 6, 17, 23, 26], "resourc": [6, 14, 16, 19, 20, 23, 26], "tutori": [6, 17, 21], "www": [5, 6], "ee": 6, "surrei": 6, "ac": 6, "uk": 6, "unix": [6, 22], "fairli": 6, "willl": 6, "insight": [6, 13], "discuss": [6, 8, 13, 21], "safe": 6, "techniqu": [7, 17, 21], "advanc": [7, 8, 26], "ice1": 7, "respond": 7, "expand": 7, "dictionari": [7, 8], "callback": [7, 16, 19, 20], "timer": [7, 13, 20], "pick": 7, "todai": [7, 21], "callback_input": 7, "event": [7, 8, 13, 14, 16, 20, 23, 26], "valid": [7, 16, 18, 20], "convert": [7, 15, 18, 20, 21], "int": [7, 8, 18], "throw": 7, "valueerror": 7, "val": 7, "callback_receiv": 7, "respons": [7, 26], "function": [5, 7, 9, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 26], "forev": [7, 13], "ice3_ro": [], "fundament": [8, 21], "rock": [8, 9, 10], "paper": [8, 9, 10], "scissor": [8, 9, 10], "compet": 8, "convent": [5, 8, 17], "addit": [8, 17, 21], "compris": 8, "serv": [8, 17, 26], "By": [8, 17, 21], "load": [8, 17, 21], "anyon": [8, 23, 26], "quickli": [8, 11, 21], "referenc": 8, "comment": 8, "intend": 8, "leav": [8, 26], "short": [8, 11, 14, 16, 19, 20], "concis": 8, "lazi": 8, "fall": [8, 26], "prei": 8, "trap": 8, "languag": [8, 13, 17, 21], "my": 8, "integ": [8, 13, 21], "decim": 8, "ask": [8, 23, 26], "welcome_text": 8, "num_choic": 8, "verifi": [8, 18], "02d": 8, "defin": [8, 13, 17, 21], "variable_nam": 8, "long": 8, "quot": 8, "doubl": [8, 13, 15, 18], "matter": 8, "whether": 8, "consist": [8, 13], "its": [8, 11], "given": [8, 20, 21, 26], "convers": [8, 21], "element": [8, 17], "tupl": [8, 21], "specifi": [8, 17, 18], "easili": [8, 17], "chosen": 8, "hard": [5, 8, 21], "separ": [8, 13, 17], "comma": 8, "structur": [8, 17, 21], "probabl": [8, 26], "think": [8, 11, 21], "question": [8, 14, 16, 19, 20], "lingo": 8, "index": [8, 10], "furthest": 8, "increas": [8, 21], "retriev": 8, "length": [8, 13, 20, 21], "len": [8, 10], "organ": [8, 14], "simplifi": [5, 8, 13, 21], "rather": 8, "keyword": 8, "parenthesi": [8, 13], "colon": 8, "return": [8, 10, 12, 15, 18, 21, 26], "rand_choic": 8, "get_comp_choic": 8, "choices_list": 8, "rand_index": [8, 10], "randint": [8, 10], "choice1": 8, "choice2": 8, "choice3": 8, "smallest": 8, "largest": 8, "final": [8, 19, 20, 21, 25], "order": [8, 21, 26], "collect": [5, 8], "request": [8, 10, 12, 26], "whatev": 8, "box": [8, 20, 21], "interpret": [8, 13], "individu": [5, 8, 21, 23, 26], "quotat": 8, "mark": [8, 20, 21], "insid": [8, 13], "outsid": [8, 26], "tell": [8, 13, 16, 21], "newlin": 8, "being": [8, 13, 17, 21], "str": [8, 20], "word": 8, "yourself": 8, "human": [5, 8, 15, 17], "mistak": 8, "express": 8, "denot": [8, 21], "respect": [8, 21], "valid_input": 8, "invalid": 8, "bad": 8, "compar": [8, 21], "equal": [8, 16, 21], "greater": [8, 16], "less": [8, 16], "boolean": 8, "sai": [8, 21, 22], "rewrit": 8, "hint": 8, "happen": [8, 11], "bundl": 8, "togeth": [8, 14, 15], "made": [8, 26], "maintain": 8, "get_spe": 8, "set_spe": 8, "particular": [8, 12], "capword": 8, "namespac": 8, "refer": [8, 21], "num_wheel": 8, "explicit": 8, "constant": [8, 10, 13, 16, 19], "num_round": 8, "numer": [8, 11], "round": 8, "far": [8, 20, 21], "players_choic": 8, "computers_choic": [8, 10], "round_complet": 8, "user_win": 8, "computer_win": 8, "stall": 8, "durat": [8, 13, 20, 23, 26], "callback_players_choic": 8, "callback_computers_choic": [8, 10], "becom": [8, 21], "describ": [5, 8, 13, 15, 26], "clarif": 8, "explanatori": 8, "benefit": 8, "understand": [8, 9, 12, 21, 26], "familiar": [8, 14], "is_game_complet": 8, "trigger": 8, "winner": 8, "vs": [8, 15, 22], "win": [8, 21], "draw": [8, 21], "lose": [8, 13], "whenev": [8, 15], "get_result": 8, "technic": 8, "better": [8, 9, 12], "alright": 8, "users_choic": 8, "user_choic": [8, 9, 10], "callback_users_choic": 8, "computer_choic": [8, 9, 10], "percentag": 8, "module3_rpscomput": [], "module3_ro": [], "module3_python3": [9, 10], "player": 9, "rerun": [9, 21], "modul": [9, 11, 12, 14, 20, 21, 25], "style": [9, 22], "visit": [9, 17], "pep": 9, "ice3_cli": 9, "shot": [9, 16, 19, 20], "screenshot": [9, 12, 14], "module03": 9, "fill": [10, 13, 18, 21], "primarili": [13, 14, 15, 19, 20, 21], "laboratori": [5, 23], "major": [], "introduc": 12, "experi": [12, 21, 26], "someon": [12, 26], "turtlebot3_cor": [12, 15, 16, 17], "turtlebot3_bringup": [12, 15, 16, 17, 18, 19], "filenam": 12, "paticular": [], "serial_nod": [12, 17], "opencr": [12, 15], "port": [2, 12, 17], "baud": [12, 17], "rosout_agg": [12, 13], "listen": [12, 13, 14], "memori": [11, 12], "distribut": 12, "instal": [12, 14, 18, 21], "download": [12, 21], "manner": 12, "pkg": [12, 13, 17, 20, 21], "underscor": 12, "dash": 12, "demonstr": [12, 14, 16, 19, 20, 21, 26], "forget": 12, "cliff": 12, "veloc": [12, 15], "module04": [11, 12], "examin": [12, 21], "abl": [12, 13, 16, 19, 21, 26], "analyz": [12, 26], "present": [12, 26], "common_msg": 13, "meet": [13, 26], "descri": 13, "int8": 13, "int16": 13, "int64": 13, "plu": [13, 16], "uint": 13, "float32": [13, 14, 20], "float64": 13, "arrai": [13, 15, 21], "fix": [13, 26], "timestamp": 13, "coordin": [13, 21], "frame": [13, 21], "commonli": 13, "frequent": 13, "fieldtyp": 13, "fieldnam": 13, "firstnam": 13, "lastnam": 13, "int32": 13, "ag": 13, "ice5": [13, 14, 25], "year": 13, "vector3": 13, "gemoetry_msg": 13, "straight": [13, 19], "bot_cmd": 13, "ws": 13, "ece387_master_spring2023": [], "ice5_publish": 13, "todo": [13, 14, 15, 18, 20, 21], "often": [5, 13, 21, 26], "thonni": 13, "rais": [13, 15, 18], "hand": [13, 15, 18, 21], "talker": 13, "cadet": [13, 23, 26], "snuffi": 13, "21": [13, 25], "care": [13, 21], "queue": 13, "size": [2, 13, 20, 21], "callback_publish": 13, "steven": [2, 13], "33": [13, 25], "ice5_subscrib": 13, "callback_person": 13, "catkin": 13, "uncom": 13, "arrow": [13, 21], "build_depend": 13, "message_gener": 13, "exec_depend": 13, "message_runtim": 13, "find_packag": 13, "add_message_fil": 13, "message1": 13, "message2": 13, "symbol": 13, "generate_messag": 13, "catkin_depend": 13, "catkin_packag": 13, "measur": [13, 18, 21], "statist": [13, 15, 16, 18], "rosparam": [13, 15, 18, 21], "enable_statist": [13, 15, 18], "mention": [13, 21], "own": [5, 11, 13, 15, 18, 23, 26], "mouse_info": 14, "accord": [14, 23, 26], "handout": [14, 26], "immedi": 14, "deactiv": 14, "overrid": 14, "enough": [14, 21], "answer": [14, 16, 19, 20], "templat": [14, 16, 19, 20], "record": [14, 16, 19], "post": [14, 16, 19, 23, 26], "lab1": [14, 16, 22], "channel": [14, 16, 19, 21], "gradescop": [14, 16, 19, 20, 23], "repositori": [14, 16, 19, 20, 21], "mouse_control": [], "acceler": 15, "icm": 15, "20648": 15, "axi": [15, 21], "mem": [2, 15], "motiontrack": 15, "tdk": 15, "gyroscop": 15, "acceleromet": 15, "motion": 15, "processor": 15, "dmp": 15, "opencr1": 15, "arm": [2, 15], "cortex": 15, "m7": 15, "ekf": 15, "estim": [15, 20, 21], "170": 15, "accur": [15, 21], "tachomet": 15, "odometri": 15, "attitud": 15, "head": [15, 16, 26], "sensit": 15, "magnet": 15, "local": [15, 16, 17], "ec": [15, 26], "electron": [15, 23, 26], "even": [2, 15, 17, 21, 26], "wire": 15, "strateg": 15, "creator": 15, "awar": [15, 21], "serial": [15, 17], "core": [15, 17], "odom": 15, "quaternion": 15, "represent": [15, 21], "readabl": [15, 17], "euler": 15, "angl": [15, 18], "squaternion": [15, 16], "q": 15, "conver": 15, "to_eul": 15, "roll": 15, "pitch": 15, "yaw": [15, 16], "ice6": [15, 16], "ece387_master_sp2x": [15, 16, 18], "imu_sub": 15, "sublim": [15, 17, 22], "convert_yaw": [15, 16], "callback_imu": [15, 16], "clean": 15, "disabl": 16, "turtlebot_control": [14, 16, 17], "seemlessli": 16, "lab2": [16, 17, 19, 25], "k_hdg": 16, "hdg_tol": 16, "toler": 16, "min_ang_z": 16, "limit": [16, 19], "max_ang_z": 16, "curr_yaw": 16, "goal_yaw": 16, "callback_control": [16, 19], "pseudo": 16, "cannot": 16, "yaw_err": 16, "ang_z": 16, "curr": [16, 18], "minu": 16, "bound": [16, 21], "subtract": 16, "clockwis": 16, "counterclockwis": 16, "proport": [16, 19], "min": [16, 18], "neg": [16, 21], "max": 16, "ab": 16, "involv": 17, "sever": 17, "interconnect": 17, "experienc": 17, "had": 17, "stand": 17, "associ": 17, "elimin": 17, "administrivia": 17, "xml": [17, 21], "extens": [17, 21], "markup": 17, "rule": 17, "encod": 17, "necessarili": [17, 26], "wikipedia": 17, "understood": 17, "monopol": 17, "longer": 17, "rel": [17, 21], "teh": 17, "grand": 17, "scheme": 17, "overal": [17, 26], "everyth": [11, 14, 17, 21], "talk": [17, 21], "prolog": [], "proper": 18, "rosserial_python": 17, "param": [17, 20, 21], "115200": 17, "tf_prefix": 17, "arg": [17, 20, 21], "multi_robot_nam": 17, "prefix": [17, 21], "remote_env_load": 17, "remind": 17, "ohostkeyalgorithm": 17, "rsa": 17, "ye": 17, "environment": 17, "exec": 17, "joint": 17, "clearli": [17, 21], "encourag": [5, 17], "avoid": [18, 26], "sonar": 18, "infrar": 18, "afford": 18, "triangul": 18, "principl": 18, "vision": [5, 18, 25], "acquisit": 18, "1800": 18, "12": [18, 25], "accuraci": [18, 21], "015": 18, "499": 18, "airborn": 18, "bot": 18, "newer": 18, "hls_lfcd_lds_driver": 18, "trust": 18, "turtlebot3_lidar": [18, 19], "pretti": 18, "hls_laser_publish": 18, "submenu": 18, "laserscan": 18, "robotmodel": 18, "depict": 18, "ice8": [18, 19], "sensor_msg": [18, 20, 21], "lidar_sub": 18, "atom": [18, 19], "obtain": 18, "averag": [18, 19], "nose": [18, 19], "lambda": [18, 19], "rad2deg": [18, 19], "deg_conv": [18, 19], "callback_lidar": [18, 19], "taken": 18, "count": [18, 20], "scan_tim": 18, "time_incr": 18, "incr": 18, "scale": [14, 18], "append": 18, "angle_min": 18, "angle_incr": 18, "rng": 18, "range_min": 18, "range_max": 18, "iter": 18, "zip": 18, "sum": [11, 18, 21], "divid": 18, "lab3": 19, "k_po": 19, "100": [19, 21, 26], "slowli": 19, "closer": [19, 21], "min_lin_x": 19, "05": 19, "max_lin_x": 19, "avg_dist": 19, "dist": 19, "got_avg": 19, "calcul": [19, 20, 21], "decid": [19, 20], "reus": [19, 21], "design": [5, 19, 21, 23, 25, 26], "project": [19, 20, 21, 25], "live": 20, "video": [20, 21], "feed": [20, 21], "lab4": [20, 21], "cv_bridg": [20, 21], "image_captur": 20, "cv2": [20, 21], "argpars": [20, 21], "cvbridg": 20, "cvbridgeerror": 20, "savingimag": 20, "img_dest": 20, "usb_cam": [20, 21], "image_sub": 20, "image_raw": [20, 21], "camera_callback": 20, "cv": 20, "bridg": 20, "bridge_object": 20, "callback_sav": 20, "img": 20, "cv_imag": 20, "imgmsg_to_cv2": 20, "desired_encod": 20, "bgr8": 20, "waitkei": [20, 21], "refressh": 20, "imshow": [20, 21], "_": 20, "jpg": [20, 21], "imwrit": 20, "destroyallwindow": [20, 21], "image_sav": 20, "anonym": 20, "ap": [20, 21], "argumentpars": [20, 21], "add_argu": [20, 21], "var": [20, 21], "parse_arg": [20, 21], "saving_image_object": 20, "keyboardinterrupt": 20, "training_imag": 20, "9": [11, 20, 25], "hog": 20, "featur": 20, "svm": [20, 21], "stopdetector": 20, "detectorloc": 20, "simple_object_detector": [20, 21], "get_param": 20, "_detector": 20, "empti": [20, 21], "sl_detector": 20, "triangl": [20, 21], "width": [20, 21], "perceiv": 20, "pixel": [20, 21], "frac": [20, 21], "stop_width": 20, "pai": 20, "attent": 20, "stop_dist": 20, "troubleshoot": 20, "apriltag_dist": 20, "appropri": [20, 21, 25], "tag_detect": [20, 21], "characterist": [20, 21], "term": 21, "possess": 21, "horizont": 21, "color": 21, "concern": 21, "ourselv": 21, "depth": 21, "normal": 21, "intens": [21, 26], "light": 21, "appear": 21, "grid": 21, "grayscal": 21, "255": 21, "zero": [21, 23, 26], "black": 21, "white": 21, "vari": [5, 21], "shade": 21, "grai": 21, "darker": 21, "lighter": 21, "progress": 21, "rgb": 21, "come": [21, 26], "green": 21, "blue": 21, "lead": 21, "much": 21, "unsign": 21, "form": 21, "construct": 21, "bucket": 21, "presenc": 21, "absenc": [21, 26], "pure": 21, "concept": [14, 21, 26], "aqua": 21, "fuchsia": 21, "maroon": 21, "navi": 21, "oliv": 21, "purpl": 21, "teal": 21, "yellow": 21, "recogn": [5, 21], "effici": 21, "switch": 21, "robust": [5, 21], "simpli": 21, "quadrant": 21, "imread": 21, "rgb_tupl": 21, "shape": 21, "row": 21, "column": 21, "height": 21, "bgr": 21, "700": 21, "lower": 21, "smh": 21, "minor": 21, "surpris": 21, "upcom": 21, "manipul": 21, "explor": 21, "articl": 21, "descriptor": 21, "prepar": [21, 26], "custom": [11, 21, 25], "am": 21, "dig": 21, "contour": 21, "magnitud": 21, "edg": 21, "quantifi": 21, "fact": 21, "highli": 21, "sift": 21, "upon": [5, 14, 21], "salienc": 21, "subject": [21, 23, 25], "detail": 21, "li": 21, "suggest": 21, "reveal": 21, "boundari": 21, "shadow": 21, "massiv": 21, "reduct": 21, "formula": 21, "299": 21, "587": 21, "114": 21, "match": 21, "contrast": 21, "formal": [21, 26], "strong": 21, "region": 21, "blown": 21, "framework": [5, 14, 21], "wish": 21, "neighborhood": 21, "central": 21, "north": 21, "south": 21, "east": 21, "west": 21, "notat": 21, "four": 21, "critic": 21, "g_": 21, "similarli": 21, "awesom": 21, "intuit": 21, "half": 21, "thu": 21, "theta": 21, "circ": 21, "And": 21, "triangular": 21, "45": 21, "pythagorean": 21, "theorem": 21, "inspect": 21, "hypotenus": 21, "sqrt": 21, "ratio": 21, "tan": 21, "multipli": 21, "equat": 21, "arriv": 21, "fortun": 21, "sobel": 21, "numpi": 21, "np": 21, "cvtcolor": 21, "color_bgr2grai": 21, "gx": 21, "cv_64f": 21, "gy": 21, "float": 21, "convertscaleab": 21, "sobelcombin": 21, "addweight": 21, "train": 21, "watch": 21, "vector": 21, "rapidli": 21, "sampl": 21, "stronger": 21, "contribut": [5, 21], "weight": [21, 26], "nois": 21, "reduc": 21, "distinct": 21, "variat": [5, 21], "consid": 21, "car": 21, "consum": 21, "natur": 21, "impact": [11, 21], "amount": 21, "Of": 21, "hope": [5, 21], "against": 21, "demo": [21, 25], "ece495": 21, "hog_demo": 21, "imglab": 21, "annot": 21, "umm": 21, "mayb": 21, "site": [5, 21, 23, 26], "rectangl": 21, "border": 21, "drag": 21, "implicitli": 21, "mine": 21, "slider": 21, "stop_annot": 21, "traindetector": 21, "abil": 21, "recreat": 21, "runtim": 21, "__future__": 21, "print_funct": 21, "parser": 21, "pars": 21, "grab": 21, "adjust": 21, "research": [11, 21], "improv": [21, 26], "simple_object_detector_training_opt": 21, "num_thread": 21, "be_verbos": 21, "train_simple_object_detector": 21, "test_simple_object_detector": 21, "image_window": 21, "set_imag": 21, "hit_enter_to_continu": 21, "stop_detector": 21, "imutil": 21, "testdetector": 21, "testingpath": 21, "list_imag": 21, "predict": 21, "color_bgr2rgb": 21, "ok": [2, 21], "react": 21, "wild": 21, "tune": 21, "commerci": 21, "primari": 21, "usb_cam_nod": 21, "video_devic": 21, "video0": 21, "image_width": 21, "640": 21, "image_height": 21, "480": 21, "pixel_format": 21, "yuyv": 21, "camera_frame_id": 21, "io_method": 21, "mmap": 21, "view": [21, 22], "image_view": 21, "rqt_image_view": 21, "camera_calibr": 21, "monocular": 21, "stereo": 21, "checkerboard": 21, "target": [2, 21], "9x6": 21, "cm": 21, "8x5": 21, "interior": 21, "vertex": 21, "folow": 21, "cameracalibr": 21, "027": 21, "toward": 21, "awai": 21, "tilt": 21, "skew": 21, "grei": 21, "tmp": 21, "xf": 21, "calibrationdata": 21, "gz": 21, "ost": 21, "camera_info": 21, "head_camera": 21, "narrow_stero": 21, "reopen": 21, "april": 21, "artifici": 21, "truth": 21, "aruco": 21, "qr": 21, "websit": 21, "focus": 21, "gotten": 21, "165": 21, "tag_0": 21, "standalon": 21, "standalone_tag": 21, "tag_1": 21, "continuous_detect": 21, "launch_prefix": 21, "node_namespac": 21, "apriltag_ros_continuous_nod": 21, "camera_nam": 21, "image_top": 21, "ns": 21, "clear_param": 21, "remap": 21, "image_rect": 21, "publish_tag_detections_imag": 21, "bool": [14, 21], "tag_detections_imag": 21, "Will": 21, "kind": 21, "unless": [23, 26], "cod": [], "written": [23, 26], "NO": [2, 23, 26], "academ": [23, 26], "furthermor": [23, 26], "dishonor": [23, 26], "dealt": [23, 26], "honor": [23, 26], "violat": [23, 26], "submit": [23, 26], "gr": [23, 25, 26], "effort": [11, 14, 23, 26], "exam": 23, "phone": [23, 26], "smartwatch": [23, 26], "tablet": [23, 26], "sight": [23, 26], "487": [], "enrol": [], "matlab": 24, "pep8": 24, "peter": [], "cork": [], "springer": [], "1st": [], "2nd": [], "ed": [], "pdf": [], "sometim": 5, "skip": [], "craig": [], "mehcan": [], "pearson": [], "4th": [], "popular": [], "spong": [], "hutchinson": [], "viidyasagar": [], "altern": [], "book": 11, "cover": 26, "deal": 5, "murrai": [], "sastri": [], "mathemat": [], "manipualt": [], "roboticist": [], "extremelei": [], "biginn": [], "hate": [], "uc": [], "berkelei": [], "ee106a": [], "206a": [], "modern": [], "youtub": [], "mechan": [], "coursera": [], "gilbert": [], "strang": [], "introductioin": [], "phenomen": [], "mit": [], "hi": 11, "graduat": [], "school": [], "studi": [], "intellig": [], "scienc": [], "17": [2, 25], "valvano": [], "0700": [], "lectur": [5, 23, 26], "assembl": [], "homework1": [], "homework2": [], "homework3": [], "softwar": [5, 11, 26], "homework5": [], "gpio": [], "lab5": [], "homework6": [], "11": 25, "lab6": [], "homework7": [], "13": 25, "led": [], "lab7": [], "14": 25, "homework8": [], "lab8": [], "16": 25, "homework9": [], "18": 25, "multithread": [], "lab9": [], "19": [2, 25], "homework10": [], "spi": [], "uart": [], "lab10": [], "homework11": [], "dc": [], "motor": [], "lab11": [], "23": 25, "homework12": [], "pwm": [], "lab12": [], "homework13": [], "26": 25, "lab13": [], "27": 25, "homework14": [], "28": [2, 25], "adc": [], "lab14": [], "29": 25, "homework15": [], "homework16": [], "31": [2, 25], "lab15": [], "32": 25, "lab16": [], "homework17": [], "34": 25, "35": 25, "lab17a": [], "36": 25, "lab17b": [], "37": 25, "38": 25, "39": 25, "race": [], "shall": [23, 26], "skill": [], "microcontrol": [], "incorpor": [23, 26], "successfulli": [23, 26], "extern": [23, 26], "explain": [], "oral": [], "evalu": 26, "solv": [], "ill": [], "cs": [], "110": [], "281": [], "prog": 26, "review": [], "shown": 26, "chart": 26, "93": 26, "77": 26, "80": 26, "73": 26, "87": 26, "70": 26, "83": 26, "satisfact": 26, "prevent": [2, 26], "incomplet": 26, "conveni": [], "bitbucket": [], "johnathan": [], "2019": [], "isbn": [], "978": [], "1074544300": [], "msp432": [], "vol": [], "1512185676": [], "author": [23, 26], "382": 23, "stub": [23, 26], "challeng": [23, 26], "assist": [], "who": [], "whom": [], "reason": [11, 26], "vagu": [], "mcfly": [], "flux": [], "capacitor": [], "conceptu": [], "diagram": [], "he": [], "nor": [], "get_flux": [], "current_flux": [], "max_flux": [], "85": [], "situat": [], "neither": [], "nearli": 11, "ident": [], "earn": [], "me": 26, "wrote": [], "wrong": [], "difficulti": 26, "miss": 26, "announc": [23, 26], "quiz": [25, 26], "classmat": 26, "ve": 26, "notif": 26, "date": 26, "sca": 26, "soon": 26, "occur": [14, 26], "absent": 26, "dai": 26, "sport": 26, "trip": 26, "lasik": 26, "surgeri": 26, "unschedul": 26, "aoc": 26, "approv": 26, "bedrest": 26, "famili": 26, "emerg": 26, "duti": 26, "penalti": 26, "m17": 26, "t17": 26, "m18": 26, "circumst": 26, "broad": 26, "categori": 26, "concuss": 26, "protocol": 26, "earli": 26, "director": 26, "action": 26, "aris": 26, "cutoff": [], "submiss": [], "hour": [], "07": [], "00": 2, "grace": [], "grant": 26, "extra": [], "reimag": [], "discount": [], "thereaft": [], "deadlin": [], "calendar": 26, "discret": 26, "testabl": 26, "usafa": 26, "foi": 26, "537": 26, "unabl": 26, "beyond": 26, "hospit": 26, "delai": [2, 26], "contact": 26, "x3190": 26, "makeup": 26, "held": 26, "2e48": 26, "prelab": 26, "tend": 26, "isol": 26, "chanc": 26, "ll": 26, "53": 26, "extrem": 26, "fast": 26, "wast": 26, "culmin": 26, "competit": 26, "seven": 26, "cyber": 26, "scientist": 26, "feedback": 26, "enhanc": 26, "seek": 26, "procrastin": 26, "truli": [5, 26], "enemi": 26, "foresight": 26, "reward": 26, "basi": 26, "microprocessor": 26, "rpscomput": 8, "hostnamectl": [], "lo": 2, "loopback": 2, "lower_up": 2, "mtu": 2, "65536": 2, "qdisc": 2, "noqueu": 2, "unknown": 2, "qlen": 2, "1000": 2, "brd": 2, "carrier": 2, "broadcast": 2, "multicast": 2, "1500": 2, "mq": 2, "ether": 2, "e4": 2, "5f": 2, "5b": 2, "ff": 2, "fq_codel": 2, "dormant": 2, "buff": 2, "cach": 2, "6gi": 2, "201mi": 2, "1gi": 2, "0mi": 2, "328mi": 2, "3gi": 2, "0b": 2, "lh": 2, "0g": 2, "aug": 2, "mkswap": 2, "swapspac": 2, "gib": 2, "2147479552": 2, "byte": 2, "uuid": 2, "b5bc4abf": 2, "2bce": 2, "419e": 2, "870d": 2, "4d44a7a05778": 2, "prio": 2, "robot99": 2, "224mi": 2, "5gi": 2, "903mi": 2, "0gi": 2, "neccessari": [], "utf": [], "gen": [], "en_u": [], "lc_all": [], "lang": [], "univers": [], "properti": [], "ssl": [], "keyr": [], "archiv": [], "arch": [], "colcon": [], "ament": [], "ever": [], "printenv": [], "ros_distro": [], "ros_vers": [], "ros_python_vers": [], "robotics_5ghz": 2, "208": 2, "systemd": 2, "mask": 2, "networkd": 2, "servic": 2, "hibern": 2, "hybrid": 2, "releas": [], "ubuntu_codenam": [], "null": [], "ref": [], "doc": [], "en": [], "beginn": [], "cli": [], "html": [], "domain": [], "freeli": [], "interfer": [], "101": [], "inclus": [], "ros_domain_id": [], "your_domain_id": [], "argcomplet": [], "libboost": [], "turtlebot3_w": [], "turtlebot3_cartograph": [], "turtlebot3_navigation2": [], "symlink": [], "parallel": [], "worker": [], "cdc": [], "udev": [], "udevadm": [], "reload": [], "aarch64": 2, "opencr_ld_shel": 2, "ver": 2, "opencr_ld_main": 2, "136": [], "kb": 2, "fw_name": 2, "fw_ver": 2, "v230127r1": [], "r1": 2, "0x17020800": 2, "rev": 2, "0x00000000": 2, "flash_eras": 2, "95": [], "flash_writ": 2, "crc": 2, "d92222": [], "004000": [], "jump_to_fw": 2, "sbeyer": 2, "beyersbot": 2, "aim": 5, "behavior": [5, 11], "varieti": 5, "platform": 5, "meta": 5, "perspect": 5, "seem": 5, "trivial": 5, "wildli": 5, "institut": 5, "collabor": 5, "expert": 5, "indoor": 5, "produc": 5, "approach": 5, "clutter": 5, "ninjemi": 5, "microcomput": [23, 26], "textbook": 23, "syllabu": 23, "schedul": 23, "ms": 23, "ice3": 25, "actuat": 26, "outcom": 26, "cs206": 26, "cs210": 26, "cs211": 26, "cs212": 26, "75": 26, "blockboard": 26, "387": 26, "penal": 26, "robot8": 2, "183": 2, "12a5c20": 2, "005000": 2, "diagnost": 11, "foundat": 11, "ecosystem": 11, "variou": [11, 17], "subsystem": 11, "stress": 11, "inconveni": 11, "occasion": 11, "inconsist": 11, "Its": 11, "behav": 11, "graviti": 11, "excerpt": 11, "morgan": 11, "quiglei": 11, "valuabl": 11, "teleop_twist": 11, "strongli": 11, "sequenc": 11, "recal": 11, "noth": 11, "module04_drivingtherobot": 11, "ice4": 11, "messaghpg": [], "dead": 14, "zone": 14, "pyautogui": 14, "pynput": 14, "prove": 14, "mouse_client_oo": 14, "precis": 14, "header": 14, "statu": 14, "xpo": 14, "ypo": 14, "procedur": 14, "module05": 14, "adher": 14, "THE": 14, "IN": 14, "curriculum": 14, "ece387_master_spring2024": 13, "mouse_cli": 22, "cr": 22, "lf": 22, "wherea": 22, "crlf": 22, "declar": 17, "goe": 17, "dec": 25, "brief": 25}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"modul": [0, 3, 4, 6, 7, 8, 10], "0": 0, "set": [0, 2, 5, 17], "up": [0, 2, 5, 17], "git": 0, "repositori": 0, "purpos": [0, 3, 4, 5, 6, 7, 8, 12, 13, 15, 16, 17, 18, 19, 20], "creat": [0, 4, 8, 13, 19, 20], "repo": 0, "within": 0, "github": [0, 1, 2], "classroom": 0, "enabl": [0, 2], "ssh": [0, 1, 2], "connect": [0, 17], "your": [0, 14, 16, 19, 20], "account": 0, "clone": 0, "master": [0, 1, 16, 17, 19], "robot": [0, 2, 5, 6, 8, 10, 11, 12, 17, 20, 23], "setup": [1, 2, 15, 16, 18, 19, 20], "hardwar": [1, 2], "assembl": 2, "raspberri": 2, "pi": 2, "camera": [2, 21], "softwar": [1, 2], "download": [1, 2], "ubuntu": [1, 2], "flash": [1, 2], "microsd": 2, "card": 2, "login": 2, "chang": 2, "password": 2, "usernam": 2, "option": 2, "hostnam": 2, "wi": 2, "fi": 2, "disabl": 2, "automat": 2, "updat": [1, 2], "gener": [2, 22], "new": 2, "kei": [1, 2], "add": 2, "swap": 2, "space": 2, "verifi": 2, "upgrad": 2, "instal": [1, 2], "desktop": 2, "network": 2, "altern": [1, 2], "ro": [1, 2, 4, 5, 6, 7, 8, 9, 12, 13, 20, 21], "noetic": [1, 2], "depend": [1, 2, 14], "turtlebot3": [1, 2, 5, 11], "ece387": [1, 2], "curriculum": [1, 2], "opencr": 2, "firmwar": 2, "usb": [1, 21], "addit": [1, 11, 14], "simul": [1, 5, 11], "10": 3, "final": [3, 26], "project": [3, 26], "design": 3, "present": 3, "implement": [3, 4, 7], "demonstr": 3, "turn": [3, 14, 16, 19, 20], "requir": [3, 13, 14, 16, 19, 20], "ice1": 4, "talker": 4, "listen": 4, "A": [4, 7, 8, 10], "note": [4, 7, 8, 10], "thi": [4, 7, 8, 10], "document": [4, 7, 8, 10], "chat": [4, 7], "subscrib": [4, 13, 15, 18], "import": [4, 7, 8, 13], "callback": [4, 8], "function": [4, 8], "main": [4, 7, 8], "run": [4, 5, 7, 8, 13, 16, 17, 19], "initi": [4, 7, 8], "publish": [4, 13], "command": [4, 5, 6, 7], "checkpoint": [4, 6, 9, 12, 13, 15, 17, 18, 20, 21], "cleanup": [4, 5, 6, 9, 12, 13, 15, 17, 18, 21], "oper": 5, "system": [5, 23], "termin": 5, "common": 5, "2": [6, 16, 20, 21], "linux": 6, "work": [6, 26], "remot": [6, 17], "machin": 6, "summari": [6, 12, 13, 15, 17, 18, 21], "ice3": [7, 9], "client": 7, "server": 7, "class": [7, 8, 13, 14], "program": [7, 8], "python3": [8, 10, 22], "top": 8, "python": [8, 21, 24], "code": [8, 12, 13, 21], "variabl": 8, "list": 8, "user": 8, "input": 8, "valid": 8, "statement": 8, "player": [8, 10], "init": 8, "method": 8, "part": [8, 21], "call": 8, "timer": 8, "topic": 8, "let": 8, "s": 8, "start": 8, "game": 8, "build": [8, 21], "comput": [8, 10, 20, 21], "refresh": 8, "deliver": 9, "3": [10, 19, 20, 21], "drive": [11, 12], "us": [12, 13, 21], "custom": [13, 14], "messag": [13, 14], "msg": 13, "field": 13, "header": 13, "format": 13, "In": 13, "exercis": 13, "5": [13, 21], "write": [13, 15], "packag": [13, 20], "xml": 13, "cmakelist": 13, "txt": 13, "compil": 13, "lab": [14, 16, 19, 20], "1": [14, 20, 21], "report": [14, 16, 19, 20], "inerti": [15, 16], "measur": [15, 16], "unit": [15, 16], "calibr": [15, 21], "imu": 15, "test": [15, 18, 20, 21], "control": [14, 16, 19, 23, 26], "py": [16, 19, 20], "node": [14, 16, 19, 20], "launch": [17, 19, 20, 21], "file": [17, 19, 20, 21, 22], "roslaunch": 17, "current": 17, "state": 17, "lidar": [18, 19], "video": 18, "quick": 18, "check": 18, "variant": 18, "4": [20, 21], "vision": [20, 21], "save": 20, "imag": [20, 21], "train": 20, "stop": 20, "detector": [20, 21], "move": 20, "determin": 20, "distanc": 20, "from": 20, "sign": 20, "edit": 20, "stop_detector": 20, "print": 20, "april": 20, "tag": 20, "inform": [20, 23], "basic": 21, "opencv": 21, "assign": [11, 21, 26], "gradient": 21, "histogram": 21, "orient": 21, "hog": 21, "featur": 21, "captur": 21, "cam": 21, "6": 21, "fiduci": 21, "marker": 21, "apriltag": 21, "apriltag_ro": 21, "ec": 23, "387": 23, "introduct": [5, 23], "collabor": [23, 26], "polici": [23, 26], "tutori": 24, "onlin": 24, "numpi": 24, "style": 24, "guid": 24, "resourc": [], "recommend": [], "textbook": 26, "lectur": [], "seri": [], "k": [], "lynch": [], "northwestern": [], "univ": [], "linear": [], "algebra": [], "cours": [23, 25, 26], "schedul": [25, 26], "syllabu": 26, "goal": 26, "object": [11, 14, 26], "prerequisit": 26, "grade": 26, "distribut": 26, "primari": 26, "commun": [23, 26], "c2": [23, 26], "sampl": [], "ei": 26, "ca": 26, "late": 26, "exam": 26, "quizz": 26, "laboratori": 26, "miscellan": 26, "ros2": [], "humbl": [], "configur": [], "environ": 11, "instructor": 23, "lesson": [11, 14], "agenda": 11, "gain": 11, "familiar": 11, "platform": 11, "next": 11, "time": 11, "ice4": 12, "synopsi": 14, "due": 14, "0730": 14, "13": 14, "m": 14, "t": 14, "which": 14, "dai": 14, "meet": 14, "mous": 14, "mousecontrol": 14, "faq": 22, "usr": 22, "bin": 22, "env": 22, "r": 22, "No": 22, "directori": 22}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.intersphinx": 1, "sphinx": 56}}) \ No newline at end of file +Search.setIndex({"docnames": ["Appendix/GitSetup", "Appendix/MasterSetup", "Appendix/RobotSetup", "Module10_FinalProject/FinalProject", "Module1_ROS/ICE1_ListenerTalker", "Module1_ROS/ROS", "Module2_Linux/Linux", "Module3_Python3/ICE3_ClientServer", "Module3_Python3/Python3", "Module3_Python3/ROS", "Module3_Python3/RPSComputer", "Module4_DrivingTheRobot/DrivingTheRobot", "Module4_DrivingTheRobot/ICE4_DrivingTheRobot", "Module5_CustomMessages/CustomMessages", "Module5_CustomMessages/Lab1_CustomMessages", "Module6_IMU/IMU", "Module6_IMU/Lab2_IMU", "Module7_LaunchFile/LaunchFile", "Module8_LIDAR/LIDAR", "Module8_LIDAR/Lab3_LIDAR", "Module9_CV/Lab4_ComputerVision", "Module9_CV/computer_vision", "faq", "intro", "python", "schedule", "syllabus"], "filenames": ["Appendix/GitSetup.md", "Appendix/MasterSetup.md", "Appendix/RobotSetup.md", "Module10_FinalProject/FinalProject.md", "Module1_ROS/ICE1_ListenerTalker.md", "Module1_ROS/ROS.md", "Module2_Linux/Linux.md", "Module3_Python3/ICE3_ClientServer.md", "Module3_Python3/Python3.md", "Module3_Python3/ROS.md", "Module3_Python3/RPSComputer.md", "Module4_DrivingTheRobot/DrivingTheRobot.md", "Module4_DrivingTheRobot/ICE4_DrivingTheRobot.md", "Module5_CustomMessages/CustomMessages.md", "Module5_CustomMessages/Lab1_CustomMessages.md", "Module6_IMU/IMU.md", "Module6_IMU/Lab2_IMU.md", "Module7_LaunchFile/LaunchFile.md", "Module8_LIDAR/LIDAR.md", "Module8_LIDAR/Lab3_LIDAR.md", "Module9_CV/Lab4_ComputerVision.md", "Module9_CV/computer_vision.md", "faq.md", "intro.md", "python.md", "schedule.md", "syllabus.md"], "titles": ["Module 0: Setting Up GIT Repositories", "Master Setup", "Robot Setup", "Module 10: Final Project", "ICE1: Talker and Listener", "Robotics Operating System (ROS)", "Module 2: Linux for Robotics", "ICE3 - Client and Server", "Python3 for Robotics", "ICE3: ROS", "Module 3: Python3 for Robotics", "Driving the Robot", "ICE4: Driving the Robot", "Custom Messages", "Lab 1: Custom Messages", "Inertial Measurement Unit", "Lab 2: Inertial Measurement Unit", "Launch Files", "LIDAR", "Lab 3: LIDAR", "Lab 4: Computer Vision", "Computer Vision", "\ud83d\ude4b FAQ", "ECE 387 Introduction to Robotic Systems", "\ud83d\udc0d Python Tutorials", "\ud83d\udcc6 Course Schedule", "\ud83d\udccc Syllabus"], "terms": {"setup": [0, 5, 6, 17], "access": [0, 2, 4, 5, 6, 7, 8, 13, 21], "brows": [0, 4, 5, 7, 13, 15, 17, 18, 20, 21], "com": [0, 1, 2, 21], "you": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 26], "do": [0, 2, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16, 17, 19, 20, 21, 22, 26], "alreadi": [0, 1, 2, 4, 6, 7, 8, 11, 15, 20, 21], "have": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 26], "one": [0, 2, 4, 5, 6, 7, 8, 11, 12, 13, 17, 21], "It": [0, 2, 3, 4, 5, 6, 8, 11, 12, 18, 21, 26], "us": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19, 20, 22, 23, 26], "usernam": [0, 6, 13, 15, 16, 18, 19, 20, 21], "someth": [0, 8], "identifi": [0, 3, 20, 21], "e": [0, 1, 2, 3, 6, 7, 13, 15, 17, 20, 21, 26], "g": [0, 2, 3, 6, 13, 21, 26], "bneff1013": 0, "One": [0, 4, 6, 8], "student": [0, 2, 3, 6, 12, 23, 26], "per": [0, 4, 8, 13, 18, 21, 26], "group": [0, 2, 5, 6, 14, 21], "follow": [0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 26], "person": [0, 13, 14, 16, 19, 23, 26], "comput": [0, 1, 2, 4, 5, 6, 9, 11, 15, 23, 25, 26], "first": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 21, 26], "we": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 23, 26], "ar": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 21, 22, 23, 26], "go": [0, 4, 5, 6, 8, 11, 14, 17, 18, 21, 26], "thi": [0, 1, 2, 3, 5, 6, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 25, 26], "allow": [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 18, 20, 21, 23, 26], "instructor": [0, 4, 6, 13, 15, 17, 18, 21, 26], "see": [0, 2, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, 20, 21, 26], "all": [0, 1, 2, 3, 4, 5, 6, 8, 9, 11, 12, 13, 14, 15, 17, 18, 20, 21, 23, 26], "commit": [0, 11, 21], "throughout": [0, 2, 3, 4, 5, 8, 12, 21], "semest": [0, 21], "http": [0, 1, 2, 5, 6, 17], "ooh0u_xw": 0, "select": [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 17, 18, 21, 22], "accept": [0, 1, 2, 8, 14], "assign": [0, 3, 6, 8, 13, 23], "mai": [0, 1, 2, 4, 6, 7, 8, 11, 12, 13, 21, 23, 26], "need": [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "hit": [0, 1, 2, 4, 5, 6, 7, 12, 13, 19, 20, 21], "refresh": [0, 4, 12, 13, 20], "eventu": [0, 5, 13, 21], "provid": [0, 1, 2, 3, 4, 5, 8, 12, 13, 14, 15, 17, 18, 20, 21, 23, 26], "link": [0, 1, 2], "note": [0, 1, 2, 3, 6, 12, 13, 15, 17, 19, 20, 21, 23, 26], "url": 0, "save": [0, 1, 2, 5, 6, 13, 15, 17, 18, 19, 21], "best": [0, 5, 6, 12, 15, 26], "wai": [0, 2, 3, 4, 5, 6, 8, 18, 21], "check": [0, 2, 4, 6, 7, 8, 12, 13, 15, 16, 17, 26], "updat": [0, 6, 25], "manag": 0, "invit": 0, "team": [0, 3, 14, 16, 19, 20, 23, 26], "peopl": [0, 8], "member": 0, "user": [0, 1, 2, 4, 5, 6, 7, 9, 10, 12, 16, 17, 19, 20, 21, 24], "name": [0, 1, 2, 4, 6, 8, 12, 13, 17, 20, 21], "now": [0, 1, 2, 5, 6, 8, 9, 10, 11, 12, 15, 17, 21], "exact": [0, 8], "same": [0, 1, 2, 4, 5, 6, 8, 12, 13, 17, 21], "thing": [0, 2, 5, 6, 8, 17], "0mpq4tne": 0, "repeat": [0, 6, 12, 13, 20, 21], "step": [0, 1, 2, 3, 4, 7, 8, 17, 19, 20, 21, 24], "c": [0, 1, 2, 4, 5, 6, 7, 9, 12, 13, 15, 17, 18, 20, 21, 23, 26], "h": [0, 2, 21], "open": [0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22], "termin": [0, 2, 4, 6, 7, 8, 9, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21], "ctrl": [0, 4, 5, 6, 7, 8, 9, 12, 13, 15, 17, 18, 20, 21], "alt": [0, 4, 5, 6, 7, 8, 21], "t": [0, 1, 2, 4, 5, 6, 7, 8, 12, 13, 17, 18, 21, 26], "The": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 26], "1": [0, 1, 2, 3, 4, 6, 7, 8, 10, 12, 13, 16, 17, 18, 25], "2": [0, 2, 3, 4, 5, 7, 8, 12, 14, 17, 19, 25], "gener": [0, 1, 5, 13, 15, 17, 21], "new": [0, 1, 4, 5, 6, 7, 8, 12, 13, 15, 17, 18, 20, 21], "kei": [0, 5, 8, 17, 21], "substitut": [0, 11], "email": [0, 1, 2, 26], "address": [0, 1, 2, 6, 17, 21], "keygen": [0, 1, 2, 17], "ed25519": [0, 1, 2], "your_email": 0, "exampl": [0, 2, 5, 6, 8, 13, 14, 16, 17, 19, 20, 21, 26], "when": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 17, 19, 20, 21, 26], "re": [0, 8], "prompt": [0, 1, 2, 6, 17, 20], "enter": [0, 1, 2, 4, 6, 7, 8, 10, 17, 20, 21], "file": [0, 1, 2, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 18, 25], "which": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 13, 15, 16, 17, 18, 19, 21], "click": [0, 1, 2, 5, 6, 13, 15, 18, 21, 22], "At": [0, 1, 2, 4, 6, 7, 9, 11, 17, 18, 22], "type": [0, 1, 2, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16, 17, 18, 20, 21], "secur": [0, 1, 2, 6, 12, 15, 16, 17, 18, 21], "passphras": [0, 17], "start": [0, 1, 2, 3, 4, 5, 12, 14, 15, 17, 19, 21], "agent": [0, 1, 2], "background": [0, 1, 2, 4], "add": [0, 1, 4, 5, 6, 13, 14, 16, 17, 19, 20, 21], "privat": [0, 1, 2, 17], "eval": [0, 1, 2], "s": [0, 1, 2, 4, 5, 6, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 26], "id_ed25519": [0, 1, 2], "public": [0, 1, 2, 17], "nano": [0, 1, 2, 5, 6, 12, 13, 17, 18, 21], "pub": [0, 1, 2, 6, 8, 10, 13], "content": [0, 1, 2, 5, 6, 8, 13, 17], "maxim": [0, 1, 2], "window": [0, 1, 2, 4, 5, 6, 7, 8, 11, 12, 13, 15, 17, 18, 21, 22], "ensur": [0, 1, 2, 3, 4, 5, 8, 9, 12, 13, 15, 17, 18, 21], "ha": [0, 2, 3, 4, 5, 7, 8, 12, 18, 21], "end": [0, 3, 8, 14, 16, 19, 20, 21, 22], "right": [0, 1, 2, 3, 5, 8, 12, 14, 16, 17, 19, 21, 22], "copi": [0, 1, 2, 4, 6, 7, 8, 13, 15, 16, 17, 18, 19, 20, 23, 26], "web": [0, 1, 2, 23, 26], "browser": [0, 1, 2, 12, 13], "sign": [0, 1, 2, 3, 21], "In": [0, 1, 2, 4, 5, 6, 8, 9, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 26], "upper": [0, 1, 2, 21], "corner": [0, 1, 2, 12, 21], "ani": [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 18, 20, 21, 23, 26], "page": [0, 1, 2, 12, 14, 16, 19, 20], "profil": [0, 1, 2], "photo": [0, 1, 2], "sidebar": [0, 1, 2, 21], "gpg": [0, 1, 2], "titl": [0, 1, 2, 13], "field": [0, 1, 2, 6, 12, 14, 15, 21, 26], "descript": [0, 1, 2, 8, 13, 21, 26], "label": [0, 1, 2, 20, 21], "masterx": [0, 6, 17], "past": [0, 1, 2, 6], "If": [0, 2, 3, 4, 6, 8, 12, 13, 14, 17, 18, 19, 20, 21, 23, 26], "confirm": [0, 2, 8], "password": [0, 1, 6, 17], "shell": [0, 2, 6, 12, 15, 16, 17, 18, 21], "dfec3141": 0, "pi": [0, 6, 17, 18, 20, 21], "robotx": [0, 6, 17, 20, 21], "f": [0, 15, 20, 26], "j": [0, 5], "n": [0, 2, 8], "On": [0, 2, 11, 12, 16, 17, 19, 21], "mode": [0, 2], "workspac": [0, 1, 2, 5, 6, 15, 17, 19, 20, 21], "sourc": [0, 1, 2, 5, 6, 12, 13, 15, 17, 18, 19, 20, 21, 23, 26], "folder": [0, 5, 6, 9, 12, 13, 14, 16, 17, 19, 20, 21], "cd": [0, 1, 2, 5, 6, 13, 15, 17, 18, 20, 21], "master_w": [0, 1, 5, 6, 13, 15, 16, 18, 19, 20, 21], "src": [0, 1, 2, 5, 6, 13, 15, 16, 17, 18, 19, 20, 21], "replac": [0, 1, 2, 4, 5, 6, 8, 12, 13, 18, 21], "ece387": [0, 14], "ece387_master_spring202x": [0, 19, 20], "last": [0, 1, 2, 4, 5, 6, 7, 8, 13, 18, 21, 25], "mate": 0, "config": [0, 2, 21], "global": 0, "lastname1": 0, "lastname2": 0, "ro": [0, 3, 10, 11, 14, 15, 17, 18, 23, 25, 26], "directori": [0, 5, 6, 13, 17, 19, 21], "robot_w": [0, 2, 6, 17, 20], "ece387_robot_spring202x": [0, 20, 21], "guid": [1, 2, 4, 9, 11, 21], "walk": [1, 2], "through": [1, 2, 4, 5, 6, 8, 11, 12, 21, 23, 26], "desktop": 1, "20": [1, 2, 11, 25, 26], "04": [1, 2], "lt": [1, 2], "system": [1, 2, 3, 4, 6, 11, 12, 13, 17, 18, 19, 21, 22, 26], "util": [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16, 17, 19, 20, 21, 26], "unit": [1, 2], "state": [1, 2, 3, 4, 8, 15, 21], "air": [1, 2], "forc": [1, 2, 11], "academi": [1, 2], "electr": [1, 2], "engin": [1, 2, 26], "depart": [1, 2, 26], "an": [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 26], "embed": [1, 2, 5, 15, 17, 21], "network": [1, 4, 5, 6, 21], "ground": [1, 5, 21], "robot": [1, 3, 13, 14, 15, 16, 18, 19, 21, 25, 26], "burger": [1, 2, 5, 17], "host": [1, 5, 11], "roscor": [1, 4, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16, 17, 18, 21], "gui": [1, 2, 6, 13, 15, 18], "tool": [1, 2, 3, 4, 5, 6, 8, 11, 12, 13, 15, 17, 18, 21], "creat": [1, 2, 5, 6, 7, 9, 12, 14, 15, 16, 17, 18, 21, 22], "connect": [1, 2, 6, 11, 12, 13, 15, 18, 21], "adapt": [1, 2], "from": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 26], "manual": [1, 2, 5, 6], "For": [1, 2, 3, 5, 6, 8, 11, 13, 17, 20, 21, 23, 26], "our": [1, 2, 4, 5, 6, 7, 8, 11, 12, 13, 15, 17, 18, 20, 21], "applic": [1, 2, 3, 8, 17, 21, 26], "intel": 1, "nuc": 1, "kit": 1, "instruct": [1, 2, 4, 7, 8, 11, 12], "work": [1, 2, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 21, 23], "amd64": 1, "architectur": [1, 2], "machin": [1, 2, 3, 17, 19, 20, 21], "onc": [1, 2, 4, 5, 6, 8, 9, 12, 13, 15, 17, 18, 21], "bootabl": 1, "stick": [1, 8], "within": [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "maco": 1, "well": [1, 5, 6, 8, 12, 16, 21], "master0": [1, 6], "requir": [1, 2, 4, 5, 7, 8, 10, 11, 15, 17, 21, 22, 25, 26], "press": [1, 4, 5, 6, 7, 8, 10, 14, 17, 20, 21], "hold": [1, 4, 5, 8, 21], "f10": 1, "startup": [1, 2], "boot": [1, 2], "assum": [1, 2, 4, 21], "account": [1, 2], "your": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 17, 18, 21, 22, 23, 26], "login": 1, "favorit": [1, 2, 12, 21], "command": [1, 2, 3, 9, 11, 12, 17, 18, 20, 21], "line": [1, 2, 4, 5, 6, 8, 9, 12, 13, 17, 18, 20, 21, 22], "editor": [1, 2, 5, 6, 12, 13, 15, 16, 17, 18, 19, 21], "easier": [1, 2, 8, 13, 15], "accomplish": [1, 2, 3, 4, 6, 7, 8, 11, 12, 17, 18, 19, 26], "via": [1, 2, 11, 13, 14, 16, 17, 19, 20], "so": [1, 2, 3, 4, 5, 6, 8, 13, 14, 15, 16, 17, 20, 21, 26], "can": [1, 2, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "entir": [1, 2, 5, 19, 20, 21], "up": [1, 3, 4, 6, 7, 11, 12, 14, 18, 21, 26], "set": [1, 6, 7, 8, 12, 13, 15, 16, 18, 19, 20, 21], "python3": [1, 2, 6, 7, 13, 15, 18, 20, 21], "ubuntu20": [1, 2], "default": [1, 2, 8, 17, 21], "some": [1, 2, 4, 5, 6, 7, 8, 9, 12, 14, 16, 17, 21], "packag": [1, 2, 4, 5, 6, 8, 10, 12, 14, 15, 16, 17, 18, 19, 21], "python": [1, 2, 4, 9, 10, 11, 13, 14, 15, 18, 20, 23, 25, 26], "instead": [1, 2, 6, 8, 12, 17, 18, 20, 21], "execut": [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 18, 19, 20, 21], "usr": [1, 2, 6, 13, 15, 18, 20], "bin": [1, 2, 6, 13, 15, 17, 18, 20, 21], "call": [1, 2, 4, 5, 6, 7, 11, 13, 14, 15, 17, 20, 21], "basic": [1, 2, 4, 6, 8, 13, 14, 26], "sudo": [1, 2, 5, 12, 18, 21], "10": [1, 2, 4, 6, 11, 13, 16, 21, 25], "apt": [1, 2, 12, 18], "jupyt": [1, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 18, 21], "notebook": [1, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 18, 21], "contrib": 1, "nbextens": 1, "point": [1, 2, 3, 4, 7, 11, 14, 16, 17, 18, 19, 20, 21], "environ": [1, 2, 5, 6, 17], "wiki": [1, 2, 4, 5, 12], "latest": [1, 2], "version": [1, 2, 8, 17, 21], "support": [1, 2, 21], "focal": [1, 2, 20], "org": [1, 2, 5], "sh": [1, 2, 6, 17], "echo": [1, 2, 4, 5, 6, 9, 15, 18, 21], "deb": [1, 2], "lsb_releas": [1, 2], "sc": [1, 2], "main": [1, 2, 9, 15, 21], "etc": [1, 2, 18, 21, 23, 26], "list": [1, 2, 4, 5, 6, 9, 10, 12, 13, 17, 18, 21], "d": [1, 2, 6, 7, 8, 13, 20, 21, 26], "curl": [1, 2], "haven": [1, 2, 5], "raw": [1, 2, 14], "githubusercont": [1, 2], "rosdistro": [1, 2], "asc": [1, 2], "y": [1, 2, 4, 5, 6, 9, 13, 15, 17, 18, 21], "full": [1, 26], "theminimum": 1, "build": [1, 2, 3, 5, 9, 11, 14, 18, 20, 26], "commun": [1, 2, 4, 5, 6, 8, 9, 12], "librari": [1, 2, 5, 14, 15, 16, 21], "2d": 1, "3d": [1, 2, 11, 21], "percept": [1, 11, 21], "rosdep": [1, 2], "rosinstal": [1, 2], "wstool": [1, 2], "pip": [1, 2, 14], "xterm": [1, 17], "essenti": [1, 2, 14, 21], "initi": [1, 2, 5, 10, 15, 16, 19], "init": [1, 2], "opt": [1, 2, 5, 12, 17, 18], "bash": [1, 2, 4, 5, 6, 9, 17], "mkdir": [1, 2, 6, 13, 17, 21], "p": [1, 2, 20], "catkin_mak": [1, 2, 6, 13, 15, 18], "variabl": [1, 2, 4, 7, 10, 12, 13, 16, 17, 19, 20, 21], "script": [1, 2, 4, 5, 6, 8, 13, 17, 20], "bashrc": [1, 2, 5, 12, 13, 15, 17, 18], "bottom": [1, 2, 5, 13, 21, 22], "devel": [1, 2, 5, 17], "export": [1, 2, 5, 17, 18], "ros_package_path": [1, 2, 5, 17], "share": [1, 2, 5, 12, 17, 18], "ros_hostnam": [1, 2, 17], "hostnam": [1, 6, 17], "backtick": [1, 2], "apostroph": [1, 2], "ros_master_uri": [1, 2, 5, 17], "master_ip": [1, 2], "11311": [1, 2, 5, 17], "ip": [1, 2, 6], "w": [1, 2, 5, 6, 12, 15, 17, 20, 21], "choic": [1, 2, 8, 10, 21], "rose": [1, 2, 12, 13, 17, 18], "turtlebot3_model": [1, 2, 17, 18], "lds_model": [1, 2, 17, 18], "ld": [1, 2, 17, 18], "01": [1, 2, 17, 18], "02": [1, 2, 18], "lidar": [1, 2, 3, 5, 13, 25], "time": [1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 15, 17, 18, 20, 21, 26], "make": [1, 2, 3, 5, 6, 7, 8, 9, 12, 13, 15, 17, 18, 19, 20, 21, 26], "chang": [1, 4, 5, 6, 7, 8, 13, 15, 18, 21, 22, 23, 25], "must": [1, 2, 6, 7, 8, 13, 14, 20, 21, 23, 26], "There": [1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 17, 21, 23, 26], "number": [1, 2, 3, 4, 5, 6, 7, 8, 13, 15, 21, 26], "oper": [1, 2, 3, 4, 6, 8, 11, 12, 17, 23, 26], "get": [1, 2, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "joi": 1, "teleop": [1, 12], "twist": [1, 5, 6, 12, 13, 14, 16, 19], "keyboard": [1, 2, 5, 6, 11, 12, 13, 16], "laser": [1, 2, 18, 19], "proc": [1, 2], "rgbd": [1, 2], "launch": [1, 2, 4, 5, 6, 7, 8, 11, 12, 15, 16, 18, 25], "rosseri": [1, 2], "arduino": [1, 2], "client": [1, 2, 9, 14], "msg": [1, 2, 4, 6, 7, 8, 10, 20], "amcl": [1, 2], "map": [1, 2, 5, 7, 18, 21], "server": [1, 2, 4, 6, 9, 11, 13, 15, 17, 18, 21], "move": [1, 2, 4, 5, 6, 7, 8, 9, 11, 13, 14, 15, 17, 18, 21], "base": [1, 2, 3, 6, 13, 14, 18, 21, 23, 26], "urdf": [1, 2], "xacro": [1, 2], "compress": [1, 2], "imag": [1, 2], "transport": [1, 2], "rqt": [1, 2], "rviz": [1, 2, 5, 11, 17, 18], "gmap": [1, 2], "navig": [1, 2, 3, 5, 15, 17, 21], "interact": [1, 2, 5, 6, 11, 12, 21], "marker": [1, 2], "dynamixel": 1, "sdk": 1, "git": [1, 2, 21, 25], "clone": [1, 2, 21], "b": [1, 2, 7, 21, 26], "roboti": [1, 2, 11, 18], "turtlebot3_simul": [1, 2], "af": [1, 2], "ece387_curriculum": [1, 2, 11, 21], "includ": [1, 2, 3, 4, 5, 6, 8, 13, 14, 15, 16, 17, 19, 20, 21, 23, 26], "run": [1, 2, 6, 9, 10, 12, 15, 18, 20, 21, 26], "node": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 17, 18, 21], "automat": [1, 6, 17, 20, 21], "path": [1, 2, 5, 6, 12, 18, 20, 21], "ignor": [1, 2, 3, 4, 5, 8, 12], "r": [1, 2, 6, 16, 21], "take": [1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 20, 21, 23, 26], "while": [1, 2, 4, 5, 6, 7, 8, 11, 15, 21, 23, 26], "These": [1, 2, 5, 8, 12, 15, 17, 18, 21], "pip3": [1, 2], "roscd": [1, 2, 6, 11, 13, 15, 17, 18, 20, 21], "txt": [1, 2], "dlib": [1, 2, 20, 21], "quit": [1, 2, 8], "4": [2, 3, 4, 5, 7, 14, 19, 25], "along": [2, 12, 21], "usb": [2, 20], "teach": [2, 6], "undergradu": 2, "below": [2, 3, 4, 5, 6, 8, 9, 13, 14, 15, 16, 18, 20, 21, 26], "steven": [2, 13], "beyer": [2, 13], "sbeyer": 2, "beyersbot": 2, "recommend": [2, 6, 11, 17], "other": [2, 4, 5, 6, 8, 11, 12, 13, 15, 17, 18, 19, 20, 21, 26], "off": [2, 4, 5, 6, 8, 12, 13, 15, 17, 18, 19, 20, 21], "shelf": [2, 21], "compon": [2, 8, 12, 13, 15, 18, 19, 20, 21], "ones": [2, 12], "cam": 2, "128": [2, 21], "gb": 2, "high": [2, 18], "speed": [2, 5, 8, 13, 18, 19, 20, 21], "monitor": [2, 6], "mous": [2, 5, 16], "older": [2, 18], "jetson": 2, "3": [2, 3, 4, 5, 7, 8, 11, 14, 15, 16, 17, 18, 25, 26], "purchas": 2, "model": [2, 4, 17], "prefer": [2, 14, 16, 19, 21, 22, 26], "8": [2, 4, 9, 21, 25], "ram": 2, "stop": [2, 3, 6, 13, 14, 15, 18, 19, 21], "after": [2, 3, 4, 5, 6, 7, 8, 12, 13, 20, 26], "A": [2, 3, 5, 6, 13, 16, 17, 19, 21, 26], "heat": 2, "sink": 2, "propertli": 2, "canakit": 2, "also": [2, 4, 8, 12, 13, 14, 15, 16, 19, 20, 21], "small": [2, 5, 15, 21], "fan": 2, "help": [2, 3, 4, 5, 6, 8, 13, 15, 16, 17, 18, 20, 21, 23, 26], "cool": 2, "print": [2, 4, 5, 6, 7, 8, 13, 15, 16, 18, 19, 21, 23, 26], "bracket": [2, 4, 7, 8], "mount": 2, "level": [2, 3, 5, 6, 8, 19], "prior": [2, 5, 26], "finish": [2, 8], "cours": [2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 21], "found": [2, 14, 16, 19, 20, 21], "materi": [2, 23, 26], "two": [2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 18, 20, 21, 26], "front": [2, 4, 5, 6, 9], "standoff": 2, "multipl": [2, 8, 13, 17, 20], "easiest": [2, 6], "os": [2, 6], "choos": [2, 8], "button": [2, 12, 14, 20, 21, 22], "scroll": [2, 5, 6, 14], "down": [2, 6, 7, 13, 14, 15, 18, 20], "menu": [2, 4, 5, 9, 18, 21, 22], "purpos": 2, "next": [2, 4, 6, 7, 8, 15, 17, 20, 21, 26], "lastli": [2, 4, 6, 8, 18, 20, 26], "64": 2, "bit": [2, 5, 8, 21], "22": [2, 25], "correct": [2, 8, 15, 18, 21], "storag": 2, "devic": [2, 15, 23, 26], "correspond": [2, 13, 15, 18, 21], "process": [2, 6, 8, 9, 11, 18, 20, 21], "overwrit": 2, "drive": [2, 3, 5, 6, 13, 14, 17, 19, 25], "befor": [2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, 21, 25, 26], "insert": [2, 15, 17, 18], "pop": [2, 21], "sure": [2, 3, 8, 9, 21], "write": [2, 3, 4, 5, 6, 8, 17, 20, 26], "complet": [2, 3, 4, 6, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 26], "should": [2, 3, 4, 5, 6, 7, 8, 9, 12, 14, 15, 16, 17, 18, 19, 20, 21], "sd": 2, "power": [2, 6, 21], "few": [2, 5, 6, 7, 8, 12, 13, 17, 21], "minut": [2, 3, 21, 26], "configur": [2, 17], "fail": [2, 3, 6, 8], "try": [2, 3, 4, 6, 7, 8, 20, 21], "again": [2, 4, 6, 8, 13, 21], "current": [2, 3, 5, 6, 12, 15, 16], "twice": [2, 17], "i": [2, 4, 5, 6, 8, 11, 14, 17, 18, 21], "like": [2, 5, 6, 8, 13, 18, 21, 22], "rememb": [2, 8, 12, 13, 14, 17, 21], "anyth": [2, 4, 5, 13, 17], "temp": 2, "addus": 2, "easi": [2, 4, 6, 7, 8, 10, 21, 24], "until": [2, 3, 4, 6, 7, 8, 11, 13, 14, 16], "back": [2, 4, 5, 6, 7, 21], "log": [2, 4], "out": [2, 3, 5, 6, 8, 21, 23, 26], "exit": [2, 4, 5, 6, 9, 12, 13, 15, 17, 18, 19, 20, 21], "usermod": 2, "l": [2, 5, 16], "newusernam": 2, "home": [2, 5, 6, 17, 20, 21], "newhomedir": 2, "m": [2, 5, 12, 13, 18, 19, 21], "suermod": 2, "still": [2, 4, 5, 6, 7, 8, 12, 17, 26], "delet": [2, 6, 21], "delus": 2, "rm": [2, 6], "pwd": [2, 5], "chose": [2, 8], "good": [2, 6, 8, 12, 17, 21], "give": [2, 6, 8, 11, 17, 21], "each": [2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 15, 17, 18, 20, 21], "uniqu": [2, 8, 15, 26], "0": [2, 3, 5, 6, 8, 10, 12, 13, 15, 16, 17, 18, 19, 20, 21, 26], "robot0": [2, 6, 17, 21], "effect": [2, 11, 21], "reboot": 2, "don": [2, 4, 5, 6, 8, 12, 13, 14, 18, 21, 26], "yet": [2, 4, 5, 6], "though": [2, 4, 21, 26], "coupl": [2, 5, 14, 21], "more": [2, 3, 4, 5, 6, 8, 9, 12, 13, 14, 15, 17, 18, 21, 26], "most": [2, 5, 6, 14, 21], "reliabl": 2, "method": [2, 4, 7, 13, 20, 21], "determin": [2, 4, 5, 6, 8, 12, 15, 16, 18, 21], "typic": [2, 6, 8, 12, 17], "wlan0": [2, 6], "lo": 2, "loopback": 2, "lower_up": 2, "mtu": 2, "65536": 2, "qdisc": 2, "noqueu": 2, "unknown": 2, "qlen": 2, "1000": 2, "00": 2, "brd": 2, "eth0": [2, 6], "NO": [2, 23, 26], "carrier": 2, "broadcast": 2, "multicast": 2, "1500": 2, "mq": 2, "ether": 2, "e4": 2, "5f": 2, "15": [2, 6, 25], "5b": 2, "30": [2, 3, 18, 25], "ff": 2, "fq_codel": 2, "dormant": 2, "31": [2, 25], "netplan": 2, "50": [2, 14, 16, 19, 20, 26], "cloud": 2, "yaml": [2, 21], "edit": [2, 6, 13, 14, 16, 17, 19, 21, 22], "look": [2, 5, 6, 8, 13, 14, 15, 17, 18, 21], "tab": [2, 4, 5, 6, 7, 8, 12, 17], "wireless": [2, 6], "interfac": [2, 6, 23, 26], "ssid": 2, "inform": [2, 4, 5, 6, 7, 8, 9, 12, 13, 15, 21, 26], "datasourc": 2, "persist": 2, "across": [2, 5, 14], "instanc": [2, 5, 7, 8, 10, 13, 16, 17, 19], "To": [2, 4, 5, 6, 8, 9, 12, 13, 15, 21], "capabl": [2, 11, 14, 18], "cfg": 2, "99": 2, "ethernet": [2, 6], "dhcp4": 2, "true": [2, 6, 7, 8, 13, 15, 16, 17, 18, 20, 21], "wifi": 2, "appli": [2, 5, 6, 15, 18, 20, 21], "benefici": 2, "static": 2, "subnet": 2, "gatewai": 2, "rout": 2, "192": [2, 6], "168": [2, 6], "dev": [2, 17, 21], "proto": 2, "24": [2, 21, 25], "kernel": [2, 4, 5, 6, 9, 21], "scope": [2, 8], "201": 2, "rang": [2, 8, 18, 21], "robotics_5ghz": 2, "208": 2, "nameserv": 2, "attempt": [2, 6, 26], "caus": [2, 4, 14], "issu": [2, 6, 15], "control": [2, 6, 8, 11, 12, 13, 15, 17, 21], "habit": 2, "fine": 2, "period": [2, 6, 8, 26], "auto": 2, "sudoedit": 2, "conf": 2, "20auto": 2, "unattend": 2, "autocleaninterv": 2, "systemd": 2, "prevent": [2, 26], "delai": [2, 26], "even": [2, 15, 17, 21, 26], "mask": 2, "systemctl": 2, "networkd": 2, "wait": [2, 3, 4, 5, 7, 8, 21], "onlin": [2, 6, 17, 23, 26], "servic": 2, "suspend": [2, 6], "hibern": 2, "sleep": [2, 4, 6], "target": [2, 21], "hybrid": 2, "might": [2, 4, 5, 6, 8, 13, 21], "necessari": [2, 5, 6, 7, 9, 17, 19, 21], "larger": 2, "activ": [2, 4, 5, 6, 9, 11, 12, 13, 14, 26], "free": [2, 6, 11, 21, 26], "total": [2, 3, 8, 21, 26], "buff": 2, "cach": 2, "avail": [2, 6, 17], "mem": [2, 15], "7": [2, 3, 4, 21, 25], "6gi": 2, "201mi": 2, "1gi": 2, "0mi": 2, "328mi": 2, "3gi": 2, "0b": 2, "falloc": 2, "program": [2, 11, 21, 23, 26], "2g": 2, "swapfil": 2, "wa": [2, 5, 6, 21, 22], "correctli": [2, 21], "ls": [2, 5, 6], "lh": 2, "rw": [2, 6], "root": [2, 5, 17], "0g": 2, "aug": 2, "19": [2, 25], "17": [2, 25], "onli": [2, 3, 5, 6, 8, 12, 14, 17, 18, 21, 23, 26], "chmod": [2, 6, 17], "600": 2, "permiss": [2, 6, 26], "28": [2, 25], "read": [2, 4, 6, 7, 8, 10, 14, 15, 16, 17, 18, 19, 23, 26], "flag": 2, "mkswap": 2, "swapspac": 2, "size": [2, 13, 20, 21], "gib": 2, "2147479552": 2, "byte": 2, "uuid": 2, "b5bc4abf": 2, "2bce": 2, "419e": 2, "870d": 2, "4d44a7a05778": 2, "Then": [2, 6, 8, 15, 21], "turn": [2, 5, 6, 12, 15, 18, 26], "swapon": 2, "show": [2, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, 20, 21], "prio": 2, "perman": 2, "fstab": 2, "none": [2, 8], "sw": 2, "tee": 2, "addition": [2, 6, 8, 11, 21], "observ": [2, 6, 12, 13, 15, 18], "output": [2, 3, 4, 5, 6, 8, 9, 12, 13, 15, 17, 18, 20, 21], "remot": [2, 21], "either": [2, 6, 8, 14, 21, 26], "dynam": 2, "dn": 2, "anoth": [2, 3, 4, 5, 8, 14, 16, 18, 21, 23, 26], "ip_address": 2, "robot99": 2, "224mi": 2, "6": [2, 3, 4, 15, 25], "5gi": 2, "903mi": 2, "0gi": 2, "sinc": [2, 5, 13, 21], "singl": [2, 5, 8, 21], "both": [2, 3, 4, 5, 8, 11, 13, 15, 17, 18, 21], "usafabot": 2, "about": [2, 4, 5, 6, 8, 9, 11, 12, 13, 17, 21], "direct": [2, 5, 16, 21, 23, 26], "gnome": 2, "flexibl": [2, 5, 14, 21], "want": [2, 6, 8, 11, 13, 14, 20, 21], "remov": [2, 6, 13, 16, 19, 20, 21], "origin": [2, 11, 21], "top": [2, 4, 5, 6, 9, 12, 21, 22], "screen": [2, 4, 6, 9, 11, 14, 16, 17, 19, 20, 21, 22], "master": [2, 5, 6, 9, 11, 12, 13, 14, 15, 18, 20, 21, 25], "bare": [2, 21], "bone": 2, "minimum": [2, 11, 26], "No": [2, 23, 26], "As": [2, 4, 5, 6, 8, 12, 15, 17, 21], "ideal": 2, "keep": [2, 13, 15, 17, 18, 21], "overhead": 2, "low": 2, "possibl": [2, 8, 21, 26], "mani": [2, 4, 5, 8, 11, 17, 18, 20, 21], "ran": [2, 4, 5, 6, 7, 9, 13, 15, 17, 18, 21], "hl": [2, 18], "lfcd": [2, 18], "driver": [2, 18], "libudev": 2, "develop": [2, 5, 7, 11, 13, 14, 21, 23, 26], "ld08_driver": 2, "board": [2, 15, 16, 21], "dpkg": 2, "armhf": 2, "libc6": 2, "opencr_port": 2, "ttyacm0": [2, 17], "opencr_model": 2, "burger_noet": 2, "loader": [2, 17], "extract": [2, 21], "wget": 2, "binari": 2, "ros1": 2, "opencr_upd": 2, "tar": [2, 21], "bz2": 2, "xvf": 2, "rf": 2, "upload": [2, 9], "success": [2, 3, 6, 21, 26], "robot8": 2, "aarch64": 2, "arm": [2, 15], "opencr_ld_shel": 2, "ver": 2, "opencr_ld_main": 2, "183": 2, "kb": 2, "fw_name": 2, "fw_ver": 2, "ok": [2, 21], "port": [2, 12, 17], "r1": 2, "0x17020800": 2, "rev": 2, "0x00000000": 2, "flash_eras": 2, "flash_writ": 2, "60": [2, 3, 26], "crc": 2, "12a5c20": 2, "005000": 2, "sec": [2, 6], "jump_to_fw": 2, "debug": [2, 4, 23, 26], "previou": [3, 5, 6, 12, 13, 21], "dfec": [3, 5, 6, 11, 14, 20, 26], "hall": [3, 5], "center": [3, 14, 15], "between": [3, 4, 5, 6, 8, 9, 11, 12, 13, 18, 20, 21], "wall": [3, 19], "imu": [3, 5, 13, 16, 17, 25], "90": [3, 6, 15, 16, 19, 21, 26], "180": [3, 15, 16, 18, 21], "360": [3, 15, 16, 18], "deg": [3, 16, 18], "opencv": [3, 20, 25], "randomli": [3, 8], "place": [3, 6, 8, 11, 15, 20, 21, 23, 26], "act": 3, "accordingli": [3, 6, 7], "apriltag_ro": [3, 20], "certain": [3, 13], "distanc": [3, 18, 19, 21], "apriltag": [3, 20, 25], "left": [3, 4, 5, 7, 8, 12, 14, 16, 18, 19, 21], "around": [3, 5, 6, 8, 20, 21], "reach": [3, 8, 16], "goal": [3, 16, 21, 23], "depend": [3, 8, 13, 15, 16, 19], "tag": [3, 6, 13, 15, 17, 18, 19, 21], "id": [3, 6, 17, 20, 21], "complex": [3, 5, 17], "task": [3, 5, 8, 12, 21, 26], "plan": [3, 26], "just": [3, 4, 6, 8, 12, 13, 15, 18, 21, 26], "code": [3, 4, 5, 6, 7, 9, 10, 11, 14, 15, 16, 18, 19, 20, 22, 23, 26], "solut": [3, 18, 23, 26], "therefor": [3, 15, 17, 21], "phase": 3, "focu": [3, 4], "graph": [3, 14], "finit": 3, "notion": 3, "powerpoint": 3, "digit": [3, 8, 15], "lot": [3, 4, 5, 6, 13, 17, 19, 26], "practic": [3, 6, 7, 11, 12, 13, 14, 15, 21], "rqt_graph": [3, 4, 5, 6, 9, 12, 13, 15, 16, 17, 18], "topic": [3, 4, 5, 6, 7, 9, 10, 12, 13, 14, 15, 16, 18, 19, 20, 21, 25], "forese": 3, "great": [3, 8], "logic": [3, 8], "flow": [3, 12], "problem": [3, 5, 17, 22, 26], "visual": [3, 5, 11, 13, 17, 18, 21], "how": [3, 4, 7, 8, 9, 11, 12, 13, 15, 17, 18, 19, 20, 21], "transit": [3, 17], "condit": [3, 21], "forward": [3, 5, 6, 7, 14, 19], "input": [3, 4, 5, 7, 10, 16, 19, 20, 21], "receiv": [3, 4, 7, 8, 10, 13, 18, 23, 26], "sensor": [3, 5, 11, 15, 17, 18, 26], "rest": [3, 4, 5], "simplic": [3, 21], "moor": 3, "reli": 3, "quick": [3, 6], "intro": [3, 25], "ece382": 3, "here": [3, 4, 5, 8, 10, 14, 17, 21, 23, 26], "part": [3, 13, 14, 15], "fsm": 3, "report": [3, 4, 6, 23, 26], "section": [3, 7, 14, 17, 18, 19, 20, 21, 26], "under": [3, 6, 14, 16, 19, 20, 21, 26], "class": [3, 4, 5, 6, 9, 10, 11, 12, 15, 16, 17, 18, 19, 20, 25, 26], "than": [3, 4, 8, 12, 16, 18, 26], "undisclos": 3, "locat": [3, 5, 6, 15, 20, 21], "larg": [3, 17, 21], "travel": [3, 15], "approxim": [3, 21], "meter": 3, "wide": [3, 5, 13, 21], "stai": [3, 8], "middl": [3, 8, 21], "halfwai": 3, "intersect": 3, "five": [3, 5], "second": [3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 18, 21], "continu": [3, 6, 8, 12, 21], "door": 3, "discov": [3, 5], "5": [3, 4, 5, 6, 11, 12, 14, 16, 18, 25, 26], "lesson": [3, 6, 13, 15, 17, 18, 21, 25, 26], "40": [3, 25], "random": [3, 8, 10, 21], "deduct": 3, "checkpoint": 3, "doe": [3, 4, 6, 13, 14, 21, 23, 26], "rubric": 3, "item": [3, 8, 9, 10, 26], "worth": [3, 26], "apriltag0": 3, "apriltag1": 3, "apriltag2": 3, "apriltag3": 3, "intersectiom": 3, "detect": [3, 5, 14, 18, 20, 21], "warn": [3, 16, 21], "25": [3, 5, 13, 14, 16, 19, 20, 25, 26], "grade": [3, 19, 20, 23], "rospi": [4, 6, 7, 8, 10, 13, 15, 16, 18, 19, 20, 21], "std_msg": [4, 6, 7, 8, 10, 13, 14, 15, 16, 19, 20, 21], "string": [4, 7, 8, 10, 13], "messag": [4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 18, 19, 20, 21, 25], "def": [4, 6, 7, 8, 10, 13, 15, 16, 18, 20], "callback_chat": 4, "abov": [4, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21], "everi": [4, 5, 7, 8, 13, 17], "sent": [4, 5, 6, 7, 8, 9, 12, 13, 15, 16, 19, 21], "over": [4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 18, 20, 21], "interrupt": [4, 8], "spin": [4, 7, 10, 13, 15, 18, 20], "thread": 4, "displai": [4, 6, 8, 9, 12, 18, 20, 21], "what": [4, 5, 6, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21, 26], "loginfo": [4, 6], "get_caller_id": 4, "heard": 4, "data": [4, 5, 8, 10, 13, 15, 17, 18, 20, 21], "paramet": [4, 12, 17, 21], "meaning": 4, "actual": [4, 8, 11, 14, 16, 19, 20, 21], "attribut": [4, 5, 8, 23, 26], "mesag": 4, "why": [4, 5], "init_nod": [4, 6, 8, 10, 13, 15, 18, 20], "except": [4, 6, 7, 20, 26], "rosinterruptexcept": [4, 6], "pass": [4, 6, 8, 13, 14, 20], "similar": [4, 7, 12, 13, 17, 18, 19, 20, 23, 26], "infinit": 4, "loop": [4, 20, 21], "__name__": [4, 6, 8, 13, 15, 18, 20], "__main__": [4, 6, 8, 13, 15, 18, 20], "info": [4, 5, 9, 12, 13, 14, 18, 21], "1666756858": 4, "595079": 4, "308": 4, "564000": 4, "hello": [4, 7], "send": [4, 5, 7, 8, 9, 10, 12, 13, 14, 17, 18, 26], "known": [4, 6, 7, 8, 10, 20, 21], "academia": [4, 6, 7, 8, 10], "industri": [4, 6, 7, 8, 10], "text": [4, 6, 7, 8, 10, 13, 14, 23], "coexist": [4, 6, 7, 8, 10, 16], "veri": [4, 6, 7, 8, 10, 11, 15, 18, 19, 21, 26], "format": [4, 6, 7, 8, 10, 14, 16, 17, 19, 20, 21], "block": [4, 5, 6, 7, 8, 9, 10, 13, 21], "contain": [4, 6, 7, 8, 10, 13, 21], "shift": [4, 5, 6, 7, 8, 10], "earlier": [4, 5, 6, 7, 8, 10, 17, 21], "later": [4, 5, 6, 7, 8, 10, 26], "delv": 4, "littl": [4, 22, 26], "deeper": 4, "explicitli": [4, 7, 8], "dure": [4, 6, 7, 8, 11, 12, 18, 21, 23, 26], "portion": [4, 7, 8, 15, 21], "advantag": [4, 6, 7, 8, 21], "shortcut": [4, 6, 7, 8], "ic": [4, 5, 9, 13, 25], "could": [4, 5, 6, 8, 21], "put": [4, 8, 21], "would": [4, 5, 6, 8, 9, 11, 12, 13, 21], "knowledg": [4, 5, 6, 11, 15, 18, 21], "regard": [4, 21, 23, 26], "know": [4, 7, 8, 13, 17, 21, 26], "cell": [4, 7, 8, 21], "charact": [4, 5, 6, 7, 8, 9, 22], "gone": [4, 7, 8], "standard": [4, 9, 26], "common": [4, 17, 21], "find": [4, 8, 11, 12, 13, 17, 18, 20, 21, 26], "chat_pub": 4, "queue_siz": [4, 6, 8, 10, 13], "rate": [4, 6, 12, 19], "hz": [4, 5, 6, 13], "is_shutdown": 4, "chat_str": [4, 7], "drop": 4, "old": [4, 13], "faster": [4, 8], "handl": [4, 7, 13, 14, 15], "With": [4, 8, 9], "previous": [4, 6, 13, 21], "desir": [4, 12, 19, 22], "statement": [4, 16, 19, 20], "realli": [4, 11, 21], "cleanli": 4, "side": [4, 13, 14, 21], "That": [4, 5, 6, 8, 17, 22], "due": [4, 21, 25, 26], "mean": [4, 5, 6, 8], "those": [4, 6, 8, 9, 13, 21, 23, 26], "investig": [4, 18], "equival": 4, "linux": [4, 5, 11, 12, 22, 25, 26], "lab": [4, 6, 11, 12, 13, 15, 17, 18, 21, 23, 25, 26], "NOT": [4, 5, 6, 9, 14, 21], "rosnod": [4, 5, 9, 13, 21], "rosout": [4, 12, 13, 17], "facilit": 4, "rostop": [4, 5, 6, 9, 12, 13, 18, 21], "expect": [4, 8, 12, 16, 21, 23, 26], "them": [4, 6, 7, 8, 9, 11, 12, 17, 21, 26], "close": [4, 5, 6, 8, 9, 11, 12, 13, 15, 17, 18, 21, 26], "rosmsg": [4, 5, 9, 12, 13, 14, 18], "saw": [4, 5, 8], "ice1_talk": 4, "reset": [4, 6, 9], "clear": [4, 5, 6, 9, 21, 26], "bar": [4, 5, 9, 21], "restart": [4, 5, 9, 20, 21], "shutdown": [4, 6, 7, 9, 13, 15, 17, 18, 21], "lectur": [5, 23, 26], "accompani": [5, 6], "notetak": [5, 6], "learn": [5, 6, 7, 9, 12, 13, 14, 15, 18, 21, 26], "turltebot3": 5, "www": [5, 6], "framework": [5, 14, 21], "softwar": [5, 11, 26], "collect": [5, 8], "convent": [5, 8, 17], "aim": 5, "simplifi": [5, 8, 13, 21], "robust": [5, 21], "behavior": [5, 11], "varieti": 5, "platform": 5, "sometim": 5, "meta": 5, "becaus": [5, 6, 8, 21], "perform": [5, 8, 15, 21], "function": [5, 7, 9, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 26], "truli": [5, 26], "hard": [5, 8, 21], "perspect": 5, "seem": 5, "trivial": 5, "human": [5, 8, 15, 17], "often": [5, 13, 21, 26], "vari": [5, 21], "wildli": 5, "deal": 5, "variat": [5, 21], "individu": [5, 8, 21, 23, 26], "laboratori": [5, 23], "institut": 5, "hope": [5, 21], "own": [5, 11, 13, 15, 18, 23, 26], "result": [5, 6, 8, 12, 21, 23, 26], "built": [5, 8, 12, 13, 14, 17, 18, 21, 23, 26], "encourag": [5, 17], "collabor": 5, "expert": 5, "indoor": 5, "contribut": [5, 21], "world": [5, 11, 23, 26], "produc": 5, "vision": [5, 18, 25], "approach": 5, "recogn": [5, 21], "object": [5, 8, 13, 15, 18, 20, 21], "clutter": 5, "design": [5, 19, 21, 23, 25, 26], "specif": [5, 8, 13, 14, 17, 21, 23, 26], "upon": [5, 14, 21], "describ": [5, 8, 13, 15, 26], "site": [5, 21, 23, 26], "noetic": [5, 12, 17, 18], "ninjemi": 5, "argument": [5, 6, 19, 21], "notic": [5, 13, 14, 17], "priveleg": 5, "indic": [5, 6, 8, 15, 17, 18, 21], "anywher": 5, "absolut": [5, 6, 11, 12, 18], "three": [5, 6, 8, 21], "establish": [5, 17, 21], "x": [5, 6, 12, 13, 15, 16, 17, 18, 19, 20, 21], "implement": [5, 8, 10, 23, 25, 26], "howev": [5, 6, 11, 14, 15, 17, 21], "let": [5, 6, 12, 17, 21, 26], "plai": [5, 8], "virtual": [5, 11], "integr": [5, 15, 16, 18, 19, 20], "ultim": [5, 21], "dedic": 5, "But": [5, 21], "pro": [5, 12], "tip": [5, 6, 11, 12, 15, 18, 19, 20], "exist": [5, 13, 18, 19, 21], "occupi": 5, "gazebo": [5, 11], "roslaunch": [5, 6, 11, 12, 15, 18, 21], "turtlebot3_gazebo": [5, 6, 11], "turtlebot3_world": [5, 11], "syntax": [5, 6, 12, 13, 23, 26], "launchfil": [5, 12, 15], "turtlebot3_gazebo_rviz": [5, 11], "physic": [5, 8, 11], "enabl": [5, 7, 8, 11, 12, 13, 16, 18, 21], "test": [5, 8, 16, 17, 19, 23, 26], "algorithm": [5, 11, 21, 26], "real": [5, 11, 21], "sit": 5, "face": 5, "posit": [5, 6, 8, 14, 15, 21], "surround": [5, 8, 21], "maze": [5, 11, 26], "pan": 5, "wheel": [5, 6, 8, 14], "orient": [5, 13, 15, 16, 20], "camera": [5, 18, 20], "zoom": 5, "red": [5, 18, 21], "dot": [5, 18], "outlin": [5, 21, 26], "obstacl": [5, 18, 19], "scan": [5, 18, 21], "nice": [5, 7, 13, 15], "thei": [5, 6, 8, 15, 21, 26], "were": [5, 8, 12, 15, 17, 18, 21], "worri": 5, "ahead": 5, "nb": 5, "third": [5, 8, 11, 21], "fourth": [5, 11], "Not": [5, 20, 21, 26], "too": [5, 12], "excit": [5, 12], "cmd_vel": [5, 6, 12, 13, 14], "hardwar": [5, 11, 18, 26], "movement": [5, 14], "publish": [5, 6, 7, 8, 9, 10, 12, 14, 15, 16, 17, 18, 19, 20, 21], "interest": [5, 15, 16, 17, 18, 19, 21, 26], "linear": [5, 6, 12, 13, 15, 16, 19, 21], "angular": [5, 6, 12, 13, 15, 16, 18, 19], "z": [5, 6, 13, 15, 16, 19], "valu": [5, 8, 13, 15, 16, 17, 18, 19, 20, 21], "dimension": 5, "space": [5, 13, 21], "googl": [5, 8, 12, 13, 20], "dimens": 5, "backward": [5, 14], "luckili": [5, 15], "pre": [5, 12, 13, 17, 18, 21], "teleop_twist_keyboard": [5, 11, 12], "letter": 5, "autocomplet": 5, "rosrun": [5, 6, 11, 12, 13, 17, 20, 21, 22], "py": [5, 6, 11, 12, 13, 14, 15, 17, 18, 21, 22], "decreas": 5, "rad": [5, 6, 12, 16, 18], "followd": 5, "subscrib": [5, 6, 7, 8, 9, 10, 12, 14, 16, 19, 20], "where": [5, 7, 11, 14, 15, 18, 21, 26], "arc": 5, "u": [5, 6, 8], "o": [5, 6, 20], "futur": [5, 6, 8, 11, 12, 14, 15, 21, 26], "kill": [5, 6, 9, 17, 18, 21], "geometry_msg": [5, 6, 13, 15, 16, 18, 19], "Or": [5, 21], "combin": [5, 8, 15, 21], "exercis": [5, 12, 26], "document": [6, 17, 21, 23, 26], "introduct": [6, 21], "ubuntu": [6, 22], "went": 6, "repres": [6, 8, 15, 20, 21], "blink": 6, "cursor": [6, 14], "readi": [6, 12, 20], "my_fold": 6, "highlight": [6, 14, 21], "turtlebot3": [6, 12, 13, 15, 16, 17, 18, 19], "touch": [6, 13, 15, 17, 18, 21], "move_turtlebot": 6, "regular": 6, "turtlebot": [6, 8, 11, 14], "wwiii": 6, "fought": 6, "option": [6, 7, 21], "vim": 6, "emac": 6, "simpl": [6, 8, 13, 17, 18, 21], "feel": [6, 21, 26], "whichev": 6, "guidanc": 6, "arg1": 6, "elif": [6, 8, 16], "rotat": [6, 15, 16, 17, 18], "els": [6, 8, 11, 12, 13, 15, 16, 17, 18], "pleas": [6, 8, 26], "fi": 6, "exactli": [6, 18], "simul": [6, 12, 14], "turtlebot3_empty_world": 6, "did": [6, 8, 13, 17, 21], "error": [6, 7, 12, 16, 18, 19, 21], "been": [6, 8], "properli": [6, 8, 20], "la": 6, "owner": 6, "permis": 6, "isn": [6, 17], "ad": [6, 8], "rwxrwxr": 6, "github": [6, 11, 12, 21, 23, 26], "repo": [6, 9, 11, 12, 14, 15, 21], "ece387_master_sp23": 6, "homework": [6, 23, 26], "catkin_create_pkg": [6, 13, 15, 18, 20, 21], "module02": 6, "roscpp": 6, "further": [6, 8, 21], "alwai": [6, 8, 12, 18], "idea": [6, 8, 12, 26], "compil": [6, 8, 15, 26], "begin": [6, 8, 17], "bash_script": 6, "figur": [6, 21], "my_script": [6, 21], "move_turtlebot_squar": 6, "modifi": [6, 8, 13, 21], "without": [6, 8, 13, 17, 20, 21, 23, 26], "done": [6, 11, 17, 20, 26], "man": 6, "whole": [6, 21], "thier": 6, "recurs": 6, "cp": 6, "mv": 6, "destin": 6, "otherwis": [6, 21, 23, 26], "env": [6, 13, 15, 17, 18, 20], "import": [6, 9, 10, 11, 15, 16, 17, 18, 19, 20, 21, 26], "math": [6, 18, 21], "moveturtlebot": 6, "__init__": [6, 7, 8, 10, 13, 15, 16, 18, 19, 20], "self": [6, 7, 8, 10, 13, 14, 15, 16, 18, 19, 20], "cmd": [6, 16], "ctrl_c": [6, 7, 13, 15, 18, 20], "fals": [6, 7, 8, 13, 15, 16, 18, 19, 20, 21], "on_shutdown": [6, 7, 13, 15, 18, 20], "shutdownhook": [6, 7, 13, 15, 18, 20], "publish_cmd_vel_onc": 6, "case": [6, 8, 17, 21], "get_num_connect": 6, "break": 6, "shut": [6, 7, 13, 15, 18, 20], "stop_turtlebot": 6, "move_tim": 6, "moving_tim": 6, "lin_spd": 6, "ang_spd": 6, "move_squar": 6, "radian": 6, "move_object": 6, "hopefulli": 6, "sens": 6, "summar": 6, "squar": [6, 21], "degre": [6, 15, 16, 18, 19, 21], "won": [6, 14, 21], "perfect": [6, 8], "doesn": [6, 8], "perfectli": 6, "ps": 6, "faux": 6, "grep": 6, "filter": [6, 21], "entri": [6, 7], "leftmost": 6, "pid": 6, "unfortun": 6, "rospack": [6, 12, 18], "vertic": [6, 21], "pipe": 6, "unplug": [6, 21], "raspberri": 6, "uesrnam": 6, "ping": 6, "ssh": [6, 12, 13, 15, 17, 18, 21], "store": [6, 7, 8, 10, 13, 14, 16, 17, 19, 20], "ece387_robot_sp23": 6, "resolut": [6, 18], "vice": [6, 8], "versa": [6, 8], "120": 6, "plug": [6, 21], "addr": 6, "inet": 6, "wlo1": 6, "roobt": 6, "scp": [6, 20, 21], "dest": [6, 20], "jupter": [6, 9], "captur": [6, 11], "bring": [6, 11, 17], "pictur": [6, 18, 20, 21], "push": [6, 11, 12, 14, 15, 16, 19, 20], "credit": [6, 15, 23, 26], "seen": [6, 20, 23, 26], "differ": [6, 8, 11, 14, 15, 17, 18, 20, 21], "scratch": [6, 21], "surfac": [6, 21], "ton": 6, "resourc": [6, 14, 16, 19, 20, 23, 26], "tutori": [6, 17, 21], "ee": 6, "surrei": 6, "ac": 6, "uk": 6, "unix": [6, 22], "fairli": 6, "willl": 6, "insight": [6, 13], "discuss": [6, 8, 13, 21], "safe": 6, "techniqu": [7, 17, 21], "advanc": [7, 8, 26], "ice1": 7, "respond": 7, "expand": 7, "dictionari": [7, 8], "callback": [7, 16, 19, 20], "timer": [7, 13, 20], "pick": 7, "todai": [7, 21], "callback_input": 7, "event": [7, 8, 13, 14, 16, 20, 23, 26], "valid": [7, 16, 18, 20], "convert": [7, 15, 18, 20, 21], "int": [7, 8, 18], "throw": 7, "valueerror": 7, "val": 7, "callback_receiv": 7, "respons": [7, 26], "forev": [7, 13], "fundament": [8, 21], "rock": [8, 9, 10], "paper": [8, 9, 10], "scissor": [8, 9, 10], "compet": 8, "addit": [8, 17, 21], "compris": 8, "serv": [8, 17, 26], "By": [8, 17, 21], "load": [8, 17, 21], "anyon": [8, 23, 26], "quickli": [8, 11, 21], "referenc": 8, "comment": 8, "intend": 8, "leav": [8, 26], "short": [8, 11, 14, 16, 19, 20], "concis": 8, "lazi": 8, "fall": [8, 26], "prei": 8, "trap": 8, "languag": [8, 13, 17, 21], "my": 8, "integ": [8, 13, 21], "decim": 8, "ask": [8, 23, 26], "welcome_text": 8, "num_choic": 8, "verifi": [8, 18], "02d": 8, "defin": [8, 13, 17, 21], "variable_nam": 8, "long": 8, "quot": 8, "doubl": [8, 13, 15, 18], "matter": 8, "whether": 8, "consist": [8, 13], "its": [8, 11], "given": [8, 20, 21, 26], "convers": [8, 21], "element": [8, 17], "tupl": [8, 21], "specifi": [8, 17, 18], "easili": [8, 17], "chosen": 8, "separ": [8, 13, 17], "comma": 8, "structur": [8, 17, 21], "probabl": [8, 26], "think": [8, 11, 21], "question": [8, 14, 16, 19, 20], "lingo": 8, "index": [8, 10], "furthest": 8, "increas": [8, 21], "retriev": 8, "length": [8, 13, 20, 21], "len": [8, 10], "organ": [8, 14], "rather": 8, "keyword": 8, "parenthesi": [8, 13], "colon": 8, "return": [8, 10, 12, 15, 18, 21, 26], "rand_choic": 8, "get_comp_choic": 8, "choices_list": 8, "rand_index": [8, 10], "randint": [8, 10], "choice1": 8, "choice2": 8, "choice3": 8, "smallest": 8, "largest": 8, "final": [8, 19, 20, 21, 25], "order": [8, 21, 26], "request": [8, 10, 12, 26], "whatev": 8, "box": [8, 20, 21], "interpret": [8, 13], "quotat": 8, "mark": [8, 20, 21], "insid": [8, 13], "outsid": [8, 26], "tell": [8, 13, 16, 21], "newlin": 8, "being": [8, 13, 17, 21], "str": [8, 20], "word": 8, "yourself": 8, "mistak": 8, "express": 8, "denot": [8, 21], "respect": [8, 21], "valid_input": 8, "invalid": 8, "bad": 8, "compar": [8, 21], "equal": [8, 16, 21], "greater": [8, 16], "less": [8, 16], "boolean": 8, "sai": [8, 21, 22], "rewrit": 8, "hint": 8, "happen": [8, 11], "bundl": 8, "togeth": [8, 14, 15], "made": [8, 26], "maintain": 8, "get_spe": 8, "set_spe": 8, "particular": [8, 12], "capword": 8, "namespac": 8, "refer": [8, 21], "num_wheel": 8, "explicit": 8, "constant": [8, 10, 13, 16, 19], "num_round": 8, "numer": [8, 11], "round": 8, "far": [8, 20, 21], "players_choic": 8, "computers_choic": [8, 10], "round_complet": 8, "user_win": 8, "computer_win": 8, "stall": 8, "durat": [8, 13, 20, 23, 26], "callback_players_choic": 8, "callback_computers_choic": [8, 10], "becom": [8, 21], "clarif": 8, "explanatori": 8, "benefit": 8, "understand": [8, 9, 12, 21, 26], "familiar": [8, 14], "is_game_complet": 8, "trigger": 8, "winner": 8, "vs": [8, 15, 22], "win": [8, 21], "draw": [8, 21], "lose": [8, 13], "whenev": [8, 15], "get_result": 8, "technic": 8, "better": [8, 9, 12], "alright": 8, "users_choic": 8, "user_choic": [8, 9, 10], "callback_users_choic": 8, "computer_choic": [8, 9, 10], "percentag": 8, "rpscomput": 8, "module3_python3": [9, 10], "player": 9, "rerun": [9, 21], "modul": [9, 11, 12, 14, 20, 21, 25], "style": [9, 22], "visit": [9, 17], "pep": 9, "ice3_cli": 9, "shot": [9, 16, 19, 20], "screenshot": [9, 12, 14], "module03": 9, "fill": [10, 13, 18, 21], "diagnost": 11, "nearli": 11, "foundat": 11, "ecosystem": 11, "variou": [11, 17], "subsystem": 11, "stress": 11, "inconveni": 11, "occasion": 11, "inconsist": 11, "reason": [11, 26], "Its": 11, "behav": 11, "impact": [11, 21], "graviti": 11, "sum": [11, 18, 21], "excerpt": 11, "morgan": 11, "quiglei": 11, "hi": 11, "book": 11, "research": [11, 21], "effort": [11, 14, 23, 26], "valuabl": 11, "teleop_twist": 11, "strongli": 11, "sequenc": 11, "memori": [11, 12], "recal": 11, "noth": 11, "9": [11, 20, 25], "everyth": [11, 14, 17, 21], "module04": [11, 12], "module04_drivingtherobot": 11, "ice4": 11, "custom": [11, 21, 25], "introduc": 12, "experi": [12, 21, 26], "someon": [12, 26], "turtlebot3_cor": [12, 15, 16, 17], "turtlebot3_bringup": [12, 15, 16, 17, 18, 19], "filenam": 12, "serial_nod": [12, 17], "opencr": [12, 15], "baud": [12, 17], "rosout_agg": [12, 13], "listen": [12, 13, 14], "distribut": 12, "instal": [12, 14, 18, 21], "download": [12, 21], "manner": 12, "pkg": [12, 13, 17, 20, 21], "underscor": 12, "dash": 12, "demonstr": [12, 14, 16, 19, 20, 21, 26], "forget": 12, "cliff": 12, "veloc": [12, 15], "examin": [12, 21], "abl": [12, 13, 16, 19, 21, 26], "analyz": [12, 26], "present": [12, 26], "common_msg": 13, "meet": [13, 26], "descri": 13, "primarili": [13, 14, 15, 19, 20, 21], "int8": 13, "int16": 13, "int64": 13, "plu": [13, 16], "uint": 13, "float32": [13, 14, 20], "float64": 13, "arrai": [13, 15, 21], "fix": [13, 26], "timestamp": 13, "coordin": [13, 21], "frame": [13, 21], "commonli": 13, "frequent": 13, "fieldtyp": 13, "fieldnam": 13, "firstnam": 13, "lastnam": 13, "int32": 13, "ag": 13, "ice5": [13, 14, 25], "year": 13, "vector3": 13, "gemoetry_msg": 13, "straight": [13, 19], "bot_cmd": 13, "ws": 13, "ece387_master_spring2024": 13, "ice5_publish": 13, "todo": [13, 14, 15, 18, 20, 21], "thonni": 13, "rais": [13, 15, 18], "hand": [13, 15, 18, 21], "talker": 13, "cadet": [13, 23, 26], "snuffi": 13, "21": [13, 25], "care": [13, 21], "queue": 13, "callback_publish": 13, "33": [13, 25], "ice5_subscrib": 13, "callback_person": 13, "catkin": 13, "uncom": 13, "arrow": [13, 21], "build_depend": 13, "message_gener": 13, "exec_depend": 13, "message_runtim": 13, "find_packag": 13, "add_message_fil": 13, "message1": 13, "message2": 13, "symbol": 13, "generate_messag": 13, "catkin_depend": 13, "catkin_packag": 13, "measur": [13, 18, 21], "statist": [13, 15, 16, 18], "rosparam": [13, 15, 18, 21], "enable_statist": [13, 15, 18], "mention": [13, 21], "mouse_info": 14, "accord": [14, 23, 26], "handout": [14, 26], "concept": [14, 21, 26], "scale": [14, 18], "dead": 14, "zone": 14, "occur": [14, 26], "templat": [14, 16, 19, 20], "pyautogui": 14, "pynput": 14, "prove": 14, "mouse_client_oo": 14, "lab1": [14, 16, 22], "turtlebot_control": [14, 16, 17], "precis": 14, "header": 14, "statu": 14, "bool": [14, 21], "xpo": 14, "ypo": 14, "procedur": 14, "module05": 14, "adher": 14, "THE": 14, "IN": 14, "curriculum": 14, "immedi": 14, "deactiv": 14, "overrid": 14, "enough": [14, 21], "answer": [14, 16, 19, 20], "record": [14, 16, 19], "post": [14, 16, 19, 23, 26], "channel": [14, 16, 19, 21], "gradescop": [14, 16, 19, 20, 23], "repositori": [14, 16, 19, 20, 21], "acceler": 15, "icm": 15, "20648": 15, "axi": [15, 21], "motiontrack": 15, "tdk": 15, "gyroscop": 15, "acceleromet": 15, "motion": 15, "processor": 15, "dmp": 15, "opencr1": 15, "cortex": 15, "m7": 15, "ekf": 15, "estim": [15, 20, 21], "170": 15, "accur": [15, 21], "tachomet": 15, "odometri": 15, "attitud": 15, "head": [15, 16, 26], "sensit": 15, "magnet": 15, "local": [15, 16, 17], "ec": [15, 26], "electron": [15, 23, 26], "wire": 15, "strateg": 15, "creator": 15, "awar": [15, 21], "serial": [15, 17], "core": [15, 17], "odom": 15, "quaternion": 15, "represent": [15, 21], "readabl": [15, 17], "euler": 15, "angl": [15, 18], "squaternion": [15, 16], "q": 15, "conver": 15, "to_eul": 15, "roll": 15, "pitch": 15, "yaw": [15, 16], "ice6": [15, 16], "ece387_master_sp2x": [15, 16, 18], "imu_sub": 15, "sublim": [15, 17, 22], "convert_yaw": [15, 16], "callback_imu": [15, 16], "clean": 15, "disabl": 16, "seemlessli": 16, "lab2": [16, 17, 19, 25], "k_hdg": 16, "hdg_tol": 16, "toler": 16, "min_ang_z": 16, "limit": [16, 19], "max_ang_z": 16, "curr_yaw": 16, "goal_yaw": 16, "callback_control": [16, 19], "pseudo": 16, "cannot": 16, "yaw_err": 16, "ang_z": 16, "curr": [16, 18], "minu": 16, "bound": [16, 21], "subtract": 16, "clockwis": 16, "counterclockwis": 16, "proport": [16, 19], "min": [16, 18], "neg": [16, 21], "max": 16, "ab": 16, "involv": 17, "sever": 17, "interconnect": 17, "experienc": 17, "had": 17, "stand": 17, "associ": 17, "elimin": 17, "administrivia": 17, "xml": [17, 21], "extens": [17, 21], "markup": 17, "rule": 17, "encod": 17, "necessarili": [17, 26], "wikipedia": 17, "understood": 17, "monopol": 17, "longer": 17, "rel": [17, 21], "teh": 17, "grand": 17, "scheme": 17, "overal": [17, 26], "talk": [17, 21], "declar": 17, "goe": 17, "rosserial_python": 17, "param": [17, 20, 21], "115200": 17, "tf_prefix": 17, "arg": [17, 20, 21], "multi_robot_nam": 17, "prefix": [17, 21], "remote_env_load": 17, "remind": 17, "ohostkeyalgorithm": 17, "rsa": 17, "ye": 17, "environment": 17, "exec": 17, "joint": 17, "clearli": [17, 21], "avoid": [18, 26], "sonar": 18, "infrar": 18, "afford": 18, "triangul": 18, "principl": 18, "acquisit": 18, "1800": 18, "12": [18, 25], "accuraci": [18, 21], "015": 18, "499": 18, "airborn": 18, "bot": 18, "newer": 18, "proper": 18, "hls_lfcd_lds_driver": 18, "trust": 18, "turtlebot3_lidar": [18, 19], "pretti": 18, "hls_laser_publish": 18, "submenu": 18, "laserscan": 18, "robotmodel": 18, "depict": 18, "shown": [18, 26], "ice8": [18, 19], "sensor_msg": [18, 19, 20, 21], "lidar_sub": 18, "atom": [18, 19], "obtain": 18, "averag": [18, 19], "nose": [18, 19], "lambda": [18, 19], "rad2deg": [18, 19], "deg_conv": [18, 19], "callback_lidar": [18, 19], "taken": 18, "count": [18, 20], "scan_tim": 18, "time_incr": 18, "incr": 18, "append": 18, "angle_min": 18, "angle_incr": 18, "rng": 18, "range_min": 18, "range_max": 18, "iter": 18, "zip": 18, "divid": 18, "lab3": 19, "k_po": 19, "100": [19, 21, 26], "slowli": 19, "closer": [19, 21], "min_lin_x": 19, "05": 19, "max_lin_x": 19, "avg_dist": 19, "dist": 19, "got_avg": 19, "calcul": [19, 20, 21], "decid": [19, 20], "reus": [19, 21], "project": [19, 20, 21, 25], "live": 20, "video": [20, 21], "feed": [20, 21], "lab4": [20, 21], "cv_bridg": [20, 21], "image_captur": 20, "cv2": [20, 21], "argpars": [20, 21], "cvbridg": 20, "cvbridgeerror": 20, "savingimag": 20, "img_dest": 20, "usb_cam": [20, 21], "image_sub": 20, "image_raw": [20, 21], "camera_callback": 20, "cv": 20, "bridg": 20, "bridge_object": 20, "callback_sav": 20, "img": 20, "cv_imag": 20, "imgmsg_to_cv2": 20, "desired_encod": 20, "bgr8": 20, "waitkei": [20, 21], "refressh": 20, "imshow": [20, 21], "_": 20, "jpg": [20, 21], "imwrit": 20, "destroyallwindow": [20, 21], "image_sav": 20, "anonym": 20, "ap": [20, 21], "argumentpars": [20, 21], "add_argu": [20, 21], "var": [20, 21], "parse_arg": [20, 21], "saving_image_object": 20, "keyboardinterrupt": 20, "training_imag": 20, "hog": 20, "featur": 20, "svm": [20, 21], "stopdetector": 20, "detectorloc": 20, "simple_object_detector": [20, 21], "get_param": 20, "_detector": 20, "empti": [20, 21], "sl_detector": 20, "triangl": [20, 21], "width": [20, 21], "perceiv": 20, "pixel": [20, 21], "frac": [20, 21], "stop_width": 20, "pai": 20, "attent": 20, "stop_dist": 20, "troubleshoot": 20, "apriltag_dist": 20, "appropri": [20, 21, 25], "tag_detect": [20, 21], "characterist": [20, 21], "term": 21, "possess": 21, "horizont": 21, "color": 21, "concern": 21, "ourselv": 21, "depth": 21, "normal": 21, "intens": [21, 26], "light": 21, "appear": 21, "grid": 21, "grayscal": 21, "255": 21, "zero": [21, 23, 26], "black": 21, "white": 21, "shade": 21, "grai": 21, "darker": 21, "lighter": 21, "progress": 21, "rgb": 21, "come": [21, 26], "green": 21, "blue": 21, "lead": 21, "much": 21, "unsign": 21, "form": 21, "construct": 21, "bucket": 21, "presenc": 21, "absenc": [21, 26], "pure": 21, "aqua": 21, "fuchsia": 21, "maroon": 21, "navi": 21, "oliv": 21, "purpl": 21, "teal": 21, "yellow": 21, "effici": 21, "switch": 21, "simpli": 21, "quadrant": 21, "imread": 21, "rgb_tupl": 21, "shape": 21, "row": 21, "column": 21, "height": 21, "bgr": 21, "700": 21, "lower": 21, "smh": 21, "minor": 21, "surpris": 21, "upcom": 21, "manipul": 21, "explor": 21, "articl": 21, "descriptor": 21, "prepar": [21, 26], "am": 21, "dig": 21, "contour": 21, "magnitud": 21, "edg": 21, "quantifi": 21, "fact": 21, "highli": 21, "sift": 21, "salienc": 21, "subject": [21, 23, 25], "detail": 21, "li": 21, "suggest": 21, "reveal": 21, "boundari": 21, "shadow": 21, "massiv": 21, "reduct": 21, "formula": 21, "299": 21, "587": 21, "114": 21, "match": 21, "contrast": 21, "formal": [21, 26], "strong": 21, "region": 21, "blown": 21, "wish": 21, "neighborhood": 21, "central": 21, "north": 21, "south": 21, "east": 21, "west": 21, "notat": 21, "four": 21, "critic": 21, "g_": 21, "similarli": 21, "awesom": 21, "intuit": 21, "half": 21, "thu": 21, "theta": 21, "circ": 21, "And": 21, "triangular": 21, "45": 21, "pythagorean": 21, "theorem": 21, "inspect": 21, "hypotenus": 21, "sqrt": 21, "ratio": 21, "tan": 21, "multipli": 21, "equat": 21, "arriv": 21, "fortun": 21, "sobel": 21, "numpi": 21, "np": 21, "cvtcolor": 21, "color_bgr2grai": 21, "gx": 21, "cv_64f": 21, "gy": 21, "float": 21, "convertscaleab": 21, "sobelcombin": 21, "addweight": 21, "train": 21, "watch": 21, "vector": 21, "rapidli": 21, "sampl": 21, "stronger": 21, "weight": [21, 26], "nois": 21, "reduc": 21, "distinct": 21, "consid": 21, "car": 21, "consum": 21, "natur": 21, "amount": 21, "Of": 21, "against": 21, "demo": [21, 25], "ece495": 21, "hog_demo": 21, "imglab": 21, "annot": 21, "umm": 21, "mayb": 21, "rectangl": 21, "border": 21, "drag": 21, "implicitli": 21, "mine": 21, "slider": 21, "stop_annot": 21, "traindetector": 21, "abil": 21, "recreat": 21, "runtim": 21, "__future__": 21, "print_funct": 21, "parser": 21, "pars": 21, "grab": 21, "adjust": 21, "improv": [21, 26], "simple_object_detector_training_opt": 21, "num_thread": 21, "be_verbos": 21, "train_simple_object_detector": 21, "test_simple_object_detector": 21, "image_window": 21, "set_imag": 21, "hit_enter_to_continu": 21, "stop_detector": 21, "imutil": 21, "testdetector": 21, "testingpath": 21, "list_imag": 21, "predict": 21, "color_bgr2rgb": 21, "react": 21, "wild": 21, "tune": 21, "commerci": 21, "primari": 21, "usb_cam_nod": 21, "video_devic": 21, "video0": 21, "image_width": 21, "640": 21, "image_height": 21, "480": 21, "pixel_format": 21, "yuyv": 21, "camera_frame_id": 21, "io_method": 21, "mmap": 21, "view": [21, 22], "image_view": 21, "rqt_image_view": 21, "camera_calibr": 21, "monocular": 21, "stereo": 21, "checkerboard": 21, "9x6": 21, "cm": 21, "8x5": 21, "interior": 21, "vertex": 21, "folow": 21, "cameracalibr": 21, "027": 21, "toward": 21, "awai": 21, "tilt": 21, "skew": 21, "grei": 21, "tmp": 21, "xf": 21, "calibrationdata": 21, "gz": 21, "ost": 21, "camera_info": 21, "head_camera": 21, "narrow_stero": 21, "reopen": 21, "april": 21, "artifici": 21, "truth": 21, "aruco": 21, "qr": 21, "websit": 21, "focus": 21, "gotten": 21, "165": 21, "tag_0": 21, "standalon": 21, "standalone_tag": 21, "tag_1": 21, "continuous_detect": 21, "launch_prefix": 21, "node_namespac": 21, "apriltag_ros_continuous_nod": 21, "camera_nam": 21, "image_top": 21, "ns": 21, "clear_param": 21, "remap": 21, "image_rect": 21, "publish_tag_detections_imag": 21, "tag_detections_imag": 21, "Will": 21, "kind": 21, "mouse_cli": 22, "cr": 22, "lf": 22, "wherea": 22, "crlf": 22, "shall": [23, 26], "incorpor": [23, 26], "successfulli": [23, 26], "microcomput": [23, 26], "extern": [23, 26], "textbook": 23, "syllabu": 23, "schedul": 23, "announc": [23, 26], "ms": 23, "submit": [23, 26], "unless": [23, 26], "author": [23, 26], "382": 23, "stub": [23, 26], "challeng": [23, 26], "written": [23, 26], "academ": [23, 26], "furthermor": [23, 26], "dishonor": [23, 26], "dealt": [23, 26], "honor": [23, 26], "violat": [23, 26], "gr": [23, 25, 26], "exam": 23, "phone": [23, 26], "smartwatch": [23, 26], "tablet": [23, 26], "sight": [23, 26], "matlab": 24, "pep8": 24, "dec": 25, "2023": 25, "quiz": [25, 26], "ice3": 25, "11": 25, "13": 25, "14": 25, "16": 25, "18": 25, "23": 25, "26": 25, "27": 25, "29": 25, "brief": 25, "32": 25, "34": 25, "35": 25, "36": 25, "37": 25, "38": 25, "39": 25, "actuat": 26, "evalu": 26, "outcom": 26, "cs206": 26, "cs210": 26, "cs211": 26, "cs212": 26, "approv": 26, "prog": 26, "75": 26, "chart": 26, "93": 26, "77": 26, "80": 26, "73": 26, "87": 26, "70": 26, "83": 26, "satisfact": 26, "incomplet": 26, "blockboard": 26, "387": 26, "difficulti": 26, "miss": 26, "classmat": 26, "ve": 26, "notif": 26, "date": 26, "sca": 26, "soon": 26, "cover": 26, "absent": 26, "dai": 26, "sport": 26, "trip": 26, "lasik": 26, "surgeri": 26, "unschedul": 26, "aoc": 26, "bedrest": 26, "famili": 26, "emerg": 26, "duti": 26, "penalti": 26, "m17": 26, "t17": 26, "m18": 26, "circumst": 26, "broad": 26, "categori": 26, "concuss": 26, "protocol": 26, "earli": 26, "director": 26, "action": 26, "aris": 26, "penal": 26, "discret": 26, "calendar": 26, "testabl": 26, "usafa": 26, "foi": 26, "537": 26, "unabl": 26, "beyond": 26, "hospit": 26, "contact": 26, "x3190": 26, "makeup": 26, "grant": 26, "held": 26, "2e48": 26, "prelab": 26, "tend": 26, "isol": 26, "chanc": 26, "ll": 26, "53": 26, "extrem": 26, "fast": 26, "wast": 26, "culmin": 26, "competit": 26, "seven": 26, "cyber": 26, "scientist": 26, "feedback": 26, "enhanc": 26, "me": 26, "seek": 26, "procrastin": 26, "enemi": 26, "foresight": 26, "reward": 26, "basi": 26, "microprocessor": 26}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"modul": [0, 3, 4, 6, 7, 8, 10], "0": 0, "set": [0, 2, 5, 17], "up": [0, 2, 5, 17], "git": 0, "repositori": 0, "purpos": [0, 3, 4, 5, 6, 7, 8, 12, 13, 15, 16, 17, 18, 19, 20], "creat": [0, 4, 8, 13, 19, 20], "repo": 0, "within": 0, "github": [0, 1, 2], "classroom": 0, "enabl": [0, 2], "ssh": [0, 1, 2], "connect": [0, 17], "your": [0, 14, 16, 19, 20], "account": 0, "clone": 0, "master": [0, 1, 16, 17, 19], "robot": [0, 2, 5, 6, 8, 10, 11, 12, 17, 20, 23], "setup": [1, 2, 15, 16, 18, 19, 20], "hardwar": [1, 2], "softwar": [1, 2], "download": [1, 2], "ubuntu": [1, 2], "flash": [1, 2], "usb": [1, 21], "kei": [1, 2], "updat": [1, 2], "altern": [1, 2], "addit": [1, 11, 14], "ro": [1, 2, 4, 5, 6, 7, 8, 9, 12, 13, 20, 21], "noetic": [1, 2], "instal": [1, 2], "depend": [1, 2, 14], "turtlebot3": [1, 2, 5, 11], "simul": [1, 5, 11], "ece387": [1, 2], "curriculum": [1, 2], "assembl": 2, "raspberri": 2, "pi": 2, "camera": [2, 21], "microsd": 2, "card": 2, "login": 2, "chang": 2, "password": 2, "usernam": 2, "option": 2, "hostnam": 2, "wi": 2, "fi": 2, "disabl": 2, "automat": 2, "gener": [2, 22], "new": 2, "add": 2, "swap": 2, "space": 2, "verifi": 2, "upgrad": 2, "desktop": 2, "network": 2, "opencr": 2, "firmwar": 2, "10": 3, "final": [3, 26], "project": [3, 26], "design": 3, "present": 3, "implement": [3, 4, 7], "demonstr": 3, "turn": [3, 14, 16, 19, 20], "requir": [3, 13, 14, 16, 19, 20], "ice1": 4, "talker": 4, "listen": 4, "chat": [4, 7], "subscrib": [4, 13, 15, 18], "import": [4, 7, 8, 13], "callback": [4, 8], "function": [4, 8], "main": [4, 7, 8], "run": [4, 5, 7, 8, 13, 16, 17, 19], "A": [4, 7, 8, 10], "note": [4, 7, 8, 10], "thi": [4, 7, 8, 10], "document": [4, 7, 8, 10], "initi": [4, 7, 8], "publish": [4, 13], "command": [4, 5, 6, 7], "checkpoint": [4, 6, 9, 12, 13, 15, 17, 18, 20, 21], "cleanup": [4, 5, 6, 9, 12, 13, 15, 17, 18, 21], "oper": 5, "system": [5, 23], "introduct": [5, 23], "termin": 5, "common": 5, "2": [6, 16, 20, 21], "linux": 6, "work": [6, 26], "remot": [6, 17], "machin": 6, "summari": [6, 12, 13, 15, 17, 18, 21], "ice3": [7, 9], "client": 7, "server": 7, "class": [7, 8, 13, 14], "program": [7, 8], "python3": [8, 10, 22], "top": 8, "python": [8, 21, 24], "code": [8, 12, 13, 21], "variabl": 8, "list": 8, "user": 8, "input": 8, "valid": 8, "statement": 8, "player": [8, 10], "init": 8, "method": 8, "part": [8, 21], "call": 8, "timer": 8, "topic": 8, "let": 8, "s": 8, "start": 8, "game": 8, "build": [8, 21], "comput": [8, 10, 20, 21], "refresh": 8, "deliver": 9, "3": [10, 19, 20, 21], "drive": [11, 12], "lesson": [11, 14], "object": [11, 14, 26], "agenda": 11, "gain": 11, "familiar": 11, "environ": 11, "platform": 11, "assign": [11, 21, 26], "next": 11, "time": 11, "ice4": 12, "us": [12, 13, 21], "custom": [13, 14], "messag": [13, 14], "msg": 13, "field": 13, "header": 13, "format": 13, "In": 13, "exercis": 13, "5": [13, 21], "write": [13, 15], "packag": [13, 20], "xml": 13, "cmakelist": 13, "txt": 13, "compil": 13, "lab": [14, 16, 19, 20], "1": [14, 20, 21], "synopsi": 14, "due": 14, "0730": 14, "13": 14, "m": 14, "t": 14, "which": 14, "dai": 14, "meet": 14, "mous": 14, "node": [14, 16, 19, 20], "control": [14, 16, 19, 23, 26], "mousecontrol": 14, "report": [14, 16, 19, 20], "inerti": [15, 16], "measur": [15, 16], "unit": [15, 16], "calibr": [15, 21], "imu": 15, "test": [15, 18, 20, 21], "py": [16, 19, 20], "launch": [17, 19, 20, 21], "file": [17, 19, 20, 21, 22], "roslaunch": 17, "current": 17, "state": 17, "lidar": [18, 19], "video": 18, "quick": 18, "check": 18, "variant": 18, "4": [20, 21], "vision": [20, 21], "save": 20, "imag": [20, 21], "train": 20, "stop": 20, "detector": [20, 21], "move": 20, "determin": 20, "distanc": 20, "from": 20, "sign": 20, "edit": 20, "stop_detector": 20, "print": 20, "april": 20, "tag": 20, "inform": [20, 23], "basic": 21, "opencv": 21, "gradient": 21, "histogram": 21, "orient": 21, "hog": 21, "featur": 21, "captur": 21, "cam": 21, "6": 21, "fiduci": 21, "marker": 21, "apriltag": 21, "apriltag_ro": 21, "faq": 22, "usr": 22, "bin": 22, "env": 22, "r": 22, "No": 22, "directori": 22, "ec": 23, "387": 23, "instructor": 23, "cours": [23, 25, 26], "commun": [23, 26], "c2": [23, 26], "collabor": [23, 26], "polici": [23, 26], "tutori": 24, "onlin": 24, "numpi": 24, "style": 24, "guid": 24, "schedul": [25, 26], "syllabu": 26, "goal": 26, "prerequisit": 26, "grade": 26, "distribut": 26, "primari": 26, "textbook": 26, "ei": 26, "ca": 26, "late": 26, "exam": 26, "quizz": 26, "laboratori": 26, "miscellan": 26}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.intersphinx": 1, "sphinx": 56}}) \ No newline at end of file diff --git a/syllabus.html b/syllabus.html old mode 100755 new mode 100644 index edcafd5..9035c38 --- a/syllabus.html +++ b/syllabus.html @@ -1,739 +1,739 @@ - - - - - - - - - - - - 📌 Syllabus — Introduction to Robotic Systems - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    - - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - - - - - - -
    - -
    -

    📌 Syllabus#

    -
    -

    Course Goals#

    -

    Cadets shall design, implement, test, and debug robotics-based systems by developing Python programs incorporating built-in Robotics Operating System (ROS) functions and successfully interfacing the microcomputer with the external world.

    -
    -
    -

    Course Objectives#

    -

    Cadets shall be able to:

    -
      -
    • Demonstrate a clear understanding of basic concepts of robotic systems.

    • -
    • Utilize the Linux operating system to develop software systems for robotic applications.

    • -
    • Write, compile, and run Robotics Operating System (ROS) code in Python.

    • -
    • Interface sensors and actuators with a microcomputer.

    • -
    • Implement control algorithms to accomplish robotic tasks.

    • -
    • Evaluate and analyze in writing the outcomes of laboratory work.

    • -
    -
    -
    -

    Course Prerequisites#

    -

    CS206, CS210, CS211, CS212, or department approval.

    -
    -
    -

    Course Schedule#

    -

    The course schedule is here

    -
    -
    -

    Grade Distribution and Policy#

    -

    The overall weighting of the graded items is:

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Prog

    Final

    GRs

    20%

    GRs

    20%

    Labs

    75%

    Labs

    50%

    Quizzes

    5%

    Quizzes

    5%

    Final Project

    25%

    Total

    100%

    Total

    100%

    -

    The Grade distribution for this course is shown in the chart below.

    - - - - - - - - - - - - - - - - - - - - - - - -

    Grade

    Grade

    93 <= A <= 100

    77 <= C+ < 80

    90 <= A- < 93

    73 <= C < 77

    87 <= B+ < 90

    70 <= C- < 73

    83 <= B < 87

    60 <= D < 70

    80 <= B- < 83

    0 <= F < 60

    -

    You must complete all minimum functionalities on labs in order to complete the course. Even if an assignment is so late that no credit will be received, the assignment must be completed to the satisfaction of the instructor to prevent a grade of “Incomplete.”

    -
    -
    -

    Primary Communication and Control (C2)#

    -

    All communication and course material will be provided through a course/section Team. A Blockboard course will be used to provide grades. Lastly, GitHub will be used for cadets to provide their source code for laboratories.

    -
    -
    -

    Textbooks#

    -

    There is no printed textbook for ECE 387. Reading materials/labs are posted on this Course Web.

    -
    -
    -

    Collaboration Policy#

    -

    Unless specifically directed otherwise, the collaboration policy for this course is:

    -

    Authorized resources: Any material from the ECE 387 course site and online sources regarding C programming syntax only. This does not include any solutions or solution stubs for challenges similar to those asked in any assignments.

    -
      -
    • For all assignments in this course, unless otherwise noted on the assignment, you may work with anyone. We expect all graded work, including code and written reports, to be in your own work. Copying another person’s work, with or without documentation, will result in NO academic credit. Furthermore, copying without attribution is dishonorable and will be dealt with as an honor code violation.

    • -
    • All help received on work submitted for grading must be documented in accordance with the course documentation policy.

    • -
    • GRs are individual efforts. No collaboration is allowed while taking these exams. All electronic devices (phones, smartwatches, computers, tablets, etc.) must be placed out of sight for the duration of the event. If any electronic device is seen during the event, the student will receive a zero for that effort.

    • -
    -
    -
    -

    EI Policy#

    -

    Schedule EI with an instructor if you are having difficulty with the course material. You must have read the assignment and attempted the homework before requesting EI. Note: You are responsible for material if you miss class, so get notes from someone in your section. For example, you miss the lesson where the instructor announces a quiz for the next lesson or the instructor assigns homework due next lesson. Even though you missed the lesson, you are still responsible for the quiz, homework, or any other assignments made. It is in your best interest to check with your classmates after an absence. After you’ve read the assignment, attempted the homework, and checked with your classmates, you may then schedule EI if you have difficulty with the material—not to make up a class you missed.

    -
    -
    -

    CAS Policy#

    -

    For CAS notification, email your instructor prior to your absence and include the lesson number, the date, and the reason (descriptive reason — don’t just send a CAS code or SCA number) as soon as possible, preferably before the absence occurs. It is your responsibility to check your SCA to see if instructor permission is required. If it is, you must make the request prior to your absence. If you miss class, you are responsible for all material (e.g. assignments, notes, announcements, handouts, etc.) covered in class. Please check with another cadet in your section to find out what you missed.

    -

    When a cadet is absent on the day that an assignment is due, or on the date of a quiz or GR, the cadet is responsible for meeting the following standards:

    -
      -
    • Scheduled Absence: If a cadet will miss any graded event due to a scheduled absence such as an SCA, sport team trip, or scheduled lasik surgery, the cadet is expected to complete all work BEFORE the absence.

    • -
    • Unscheduled Absence: If a cadet misses a graded event for an unscheduled reason such as AOC approved bedrest or a family emergency, the cadet must complete all work on the first full class day that they return to duty in order to avoid a late penalty. For example, if a cadet is on AOC bedrest for a GR on M17 and can return to duty on T17 or M18, the cadet is expected to make up the work by M18.

    • -
    • Unique Circumstances: For circumstances that do not fall under either of these broad categories (e.g. concussion protocol), the cadet is expected to communicate early and often with the instructor. The instructor and course director will work with the cadet on a course of action.

    • -
    -
    -
    -

    Late Work Policy#

    -

    All work is due as shown on the schedule or in the assignment. If problems arise with graded assignments, see your instructor in advance. Assignments turned in later than the due date without prior permission from the instructor will be penalized (with instructor discretion) 25% per calendar day.

    -
    -
    -

    Assignments#

    -

    Assignments and due dates are included on the schedule.

    -
    -
    -

    Exams and Quizzes#

    -

    All exams and quizzes are closed textbook and notes. Quizzes will be given at instructor discretion. Testable material includes any concepts from the labs, lectures, exercises, homework, and assigned readings. Not all testable concepts will necessarily be covered in class (e.g., readings).

    -

    For missed GRs, the following policies are outlined in USAFA FOI 537-3:

    -
      -
    • Scheduled Absence - If you know that you will be unable to take the GR during the scheduled GR period, you are required to inform your instructor as soon as possible before the GR and to schedule a make-up exam.

    • -
    • Unscheduled Absence - If you miss the GR for reasons beyond your control (e.g. hospitalization, emergency leave, delayed field trip return, etc.), you must contact DFEC (x3190) within two working days to schedule a makeup. Exceptions can only be granted by the Department Head.

    • -
    -
    -
    -

    Laboratories#

    -

    Labs are held in 2E48 but may include a prelab assignment that must be done before coming to class. The labs tend to be very hardware intensive and will probably require debugging to isolate and fix problems. In-class time is your primary chance to get active help for these problems so the more you prepare outside of class, the more successful you’ll be. The 53 minutes go by extremely fast - don’t waste them!

    -
    -
    -

    Final Project#

    -

    The final project will be a culmination of the learned material and will include a robot maze and competition. The final project will include a formal laboratory write-up and seven-minute presentation describing your design, solution, and results. The final project is worth 25% of your final grade.

    -
    -
    -

    Miscellaneous#

    -

    This course is designed to help in your development as an engineer or cyber scientist. Feel free to provide feedback on the lessons and labs at any time. If you have ideas to improve or enhance the course, please let me know. The class builds on concepts from the prerequisites, so it is important for you to seek help as soon as you need it. Procrastination is truly the enemy in a hardware design course. A little foresight and planning and a lot of effort will result in an extremely rewarding experience serving as the basis for future microprocessor design work.

    -
    -
    - - - - -
    - - - - - - -
    - - - - - - -
    -
    - - -
    - - -
    -
    -
    - - - - - -
    -
    - + + + + + + + + + + + + 📌 Syllabus — Introduction to Robotic Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    + + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    + + + + + + + + +
    + +
    +

    📌 Syllabus#

    +
    +

    Course Goals#

    +

    Cadets shall design, implement, test, and debug robotics-based systems by developing Python programs incorporating built-in Robotics Operating System (ROS) functions and successfully interfacing the microcomputer with the external world.

    +
    +
    +

    Course Objectives#

    +

    Cadets shall be able to:

    +
      +
    • Demonstrate a clear understanding of basic concepts of robotic systems.

    • +
    • Utilize the Linux operating system to develop software systems for robotic applications.

    • +
    • Write, compile, and run Robotics Operating System (ROS) code in Python.

    • +
    • Interface sensors and actuators with a microcomputer.

    • +
    • Implement control algorithms to accomplish robotic tasks.

    • +
    • Evaluate and analyze in writing the outcomes of laboratory work.

    • +
    +
    +
    +

    Course Prerequisites#

    +

    CS206, CS210, CS211, CS212, or department approval.

    +
    +
    +

    Course Schedule#

    +

    The course schedule is here

    +
    +
    +

    Grade Distribution and Policy#

    +

    The overall weighting of the graded items is:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    Prog

    Final

    GRs

    20%

    GRs

    20%

    Labs

    75%

    Labs

    50%

    Quizzes

    5%

    Quizzes

    5%

    Final Project

    25%

    Total

    100%

    Total

    100%

    +

    The Grade distribution for this course is shown in the chart below.

    + + + + + + + + + + + + + + + + + + + + + + + +

    Grade

    Grade

    93 <= A <= 100

    77 <= C+ < 80

    90 <= A- < 93

    73 <= C < 77

    87 <= B+ < 90

    70 <= C- < 73

    83 <= B < 87

    60 <= D < 70

    80 <= B- < 83

    0 <= F < 60

    +

    You must complete all minimum functionalities on labs in order to complete the course. Even if an assignment is so late that no credit will be received, the assignment must be completed to the satisfaction of the instructor to prevent a grade of “Incomplete.”

    +
    +
    +

    Primary Communication and Control (C2)#

    +

    All communication and course material will be provided through a course/section Team. A Blockboard course will be used to provide grades. Lastly, GitHub will be used for cadets to provide their source code for laboratories.

    +
    +
    +

    Textbooks#

    +

    There is no printed textbook for ECE 387. Reading materials/labs are posted on this Course Web.

    +
    +
    +

    Collaboration Policy#

    +

    Unless specifically directed otherwise, the collaboration policy for this course is:

    +

    Authorized resources: Any material from the ECE 387 course site and online sources regarding C programming syntax only. This does not include any solutions or solution stubs for challenges similar to those asked in any assignments.

    +
      +
    • For all assignments in this course, unless otherwise noted on the assignment, you may work with anyone. We expect all graded work, including code and written reports, to be in your own work. Copying another person’s work, with or without documentation, will result in NO academic credit. Furthermore, copying without attribution is dishonorable and will be dealt with as an honor code violation.

    • +
    • All help received on work submitted for grading must be documented in accordance with the course documentation policy.

    • +
    • GRs are individual efforts. No collaboration is allowed while taking these exams. All electronic devices (phones, smartwatches, computers, tablets, etc.) must be placed out of sight for the duration of the event. If any electronic device is seen during the event, the student will receive a zero for that effort.

    • +
    +
    +
    +

    EI Policy#

    +

    Schedule EI with an instructor if you are having difficulty with the course material. You must have read the assignment and attempted the homework before requesting EI. Note: You are responsible for material if you miss class, so get notes from someone in your section. For example, you miss the lesson where the instructor announces a quiz for the next lesson or the instructor assigns homework due next lesson. Even though you missed the lesson, you are still responsible for the quiz, homework, or any other assignments made. It is in your best interest to check with your classmates after an absence. After you’ve read the assignment, attempted the homework, and checked with your classmates, you may then schedule EI if you have difficulty with the material—not to make up a class you missed.

    +
    +
    +

    CAS Policy#

    +

    For CAS notification, email your instructor prior to your absence and include the lesson number, the date, and the reason (descriptive reason — don’t just send a CAS code or SCA number) as soon as possible, preferably before the absence occurs. It is your responsibility to check your SCA to see if instructor permission is required. If it is, you must make the request prior to your absence. If you miss class, you are responsible for all material (e.g. assignments, notes, announcements, handouts, etc.) covered in class. Please check with another cadet in your section to find out what you missed.

    +

    When a cadet is absent on the day that an assignment is due, or on the date of a quiz or GR, the cadet is responsible for meeting the following standards:

    +
      +
    • Scheduled Absence: If a cadet will miss any graded event due to a scheduled absence such as an SCA, sport team trip, or scheduled lasik surgery, the cadet is expected to complete all work BEFORE the absence.

    • +
    • Unscheduled Absence: If a cadet misses a graded event for an unscheduled reason such as AOC approved bedrest or a family emergency, the cadet must complete all work on the first full class day that they return to duty in order to avoid a late penalty. For example, if a cadet is on AOC bedrest for a GR on M17 and can return to duty on T17 or M18, the cadet is expected to make up the work by M18.

    • +
    • Unique Circumstances: For circumstances that do not fall under either of these broad categories (e.g. concussion protocol), the cadet is expected to communicate early and often with the instructor. The instructor and course director will work with the cadet on a course of action.

    • +
    +
    +
    +

    Late Work Policy#

    +

    All work is due as shown on the schedule or in the assignment. If problems arise with graded assignments, see your instructor in advance. Assignments turned in later than the due date without prior permission from the instructor will be penalized (with instructor discretion) 25% per calendar day.

    +
    +
    +

    Assignments#

    +

    Assignments and due dates are included on the schedule.

    +
    +
    +

    Exams and Quizzes#

    +

    All exams and quizzes are closed textbook and notes. Quizzes will be given at instructor discretion. Testable material includes any concepts from the labs, lectures, exercises, homework, and assigned readings. Not all testable concepts will necessarily be covered in class (e.g., readings).

    +

    For missed GRs, the following policies are outlined in USAFA FOI 537-3:

    +
      +
    • Scheduled Absence - If you know that you will be unable to take the GR during the scheduled GR period, you are required to inform your instructor as soon as possible before the GR and to schedule a make-up exam.

    • +
    • Unscheduled Absence - If you miss the GR for reasons beyond your control (e.g. hospitalization, emergency leave, delayed field trip return, etc.), you must contact DFEC (x3190) within two working days to schedule a makeup. Exceptions can only be granted by the Department Head.

    • +
    +
    +
    +

    Laboratories#

    +

    Labs are held in 2E48 but may include a prelab assignment that must be done before coming to class. The labs tend to be very hardware intensive and will probably require debugging to isolate and fix problems. In-class time is your primary chance to get active help for these problems so the more you prepare outside of class, the more successful you’ll be. The 53 minutes go by extremely fast - don’t waste them!

    +
    +
    +

    Final Project#

    +

    The final project will be a culmination of the learned material and will include a robot maze and competition. The final project will include a formal laboratory write-up and seven-minute presentation describing your design, solution, and results. The final project is worth 25% of your final grade.

    +
    +
    +

    Miscellaneous#

    +

    This course is designed to help in your development as an engineer or cyber scientist. Feel free to provide feedback on the lessons and labs at any time. If you have ideas to improve or enhance the course, please let me know. The class builds on concepts from the prerequisites, so it is important for you to seek help as soon as you need it. Procrastination is truly the enemy in a hardware design course. A little foresight and planning and a lot of effort will result in an extremely rewarding experience serving as the basis for future microprocessor design work.

    +
    +
    + + + + +
    + + + + + + +
    + + + + + + +
    +
    + + +
    + + +
    +
    +
    + + + + + + + \ No newline at end of file