From ab6467d0fc3417880b6e329559fe38dc3aafebc3 Mon Sep 17 00:00:00 2001 From: Marcus Behel Date: Sun, 5 May 2024 14:57:00 -0400 Subject: [PATCH] Deployed c60b0fb with MkDocs version: 1.6.0 --- 404.html | 55 +-- css/fonts/Roboto-Slab-Bold.woff | Bin css/fonts/Roboto-Slab-Bold.woff2 | Bin css/fonts/Roboto-Slab-Regular.woff | Bin css/fonts/Roboto-Slab-Regular.woff2 | Bin css/fonts/fontawesome-webfont.eot | Bin css/fonts/fontawesome-webfont.svg | 0 css/fonts/fontawesome-webfont.ttf | Bin css/fonts/fontawesome-webfont.woff | Bin css/fonts/fontawesome-webfont.woff2 | Bin css/fonts/lato-bold-italic.woff | Bin css/fonts/lato-bold-italic.woff2 | Bin css/fonts/lato-bold.woff | Bin css/fonts/lato-bold.woff2 | Bin css/fonts/lato-normal-italic.woff | Bin css/fonts/lato-normal-italic.woff2 | Bin css/fonts/lato-normal.woff | Bin css/fonts/lato-normal.woff2 | Bin css/theme.css | 6 +- css/theme_extra.css | 38 +- {firmware => devs}/build/index.html | 83 ++-- devs/buildsimcb/index.html | 190 +++++++++ {firmware => devs}/details/index.html | 101 ++--- {firmware => devs}/developing/index.html | 180 ++++---- {firmware => devs}/math/index.html | 260 ++++++++---- .../math_res/coord_system_1.png | Bin .../math_res/coord_system_2.png | Bin .../math_res/coord_system_rotations.png | Bin {firmware => devs}/math_res/global_coord1.png | Bin {firmware => devs}/math_res/global_coord2.png | Bin {firmware => devs}/math_res/global_invert.gif | Bin .../math_res/robot_local_coord.png | Bin .../math_res/thruster_config.png | Bin {firmware => devs}/overview/index.html | 86 ++-- .../simhijack => devs/pythoniface}/index.html | 82 ++-- devs/simulation/index.html | 205 ++++++++++ hardware/img/bar30.png | Bin hardware/sensors/index.html | 69 ++-- hardware/v1/extra_additoins.png | Bin hardware/v1/fritzing_header_pos.png | Bin hardware/v1/index.html | 65 +-- hardware/v1/prototype_assembled.png | Bin hardware/v1/prototype_assembled_labeled.png | Bin hardware/v1/prototype_axis.png | Bin hardware/v1/v1_back_side.png | Bin hardware/v1/wiring_diagram.dia | Bin hardware/v1/wiring_diagram.png | Bin hardware/v2/index.html | 65 +-- hardware/v2/protoboard_headers.png | Bin hardware/v2/v2_back.png | Bin hardware/v2/v2_coord_system.png | Bin hardware/v2/v2_front.png | Bin hardware/v2/v2_labeled.png | Bin hardware/v2/wiring_diagram.dia | Bin hardware/v2/wiring_diagram.png | Bin img/favicon.ico | Bin index.html | 65 +-- js/html5shiv.min.js | 0 js/jquery-3.6.0.min.js | 0 js/mathjax.js | 30 +- js/theme.js | 2 +- js/theme_extra.js | 0 none/index.html | 59 +-- search.html | 57 +-- search/lunr.js | 2 +- search/main.js | 0 search/search_index.json | 2 +- search/worker.js | 0 sitemap.xml | 63 +-- sitemap.xml.gz | Bin 361 -> 382 bytes user_guide/calibration/index.html | 69 ++-- user_guide/comm_protocol/index.html | 65 +-- user_guide/general_use/index.html | 83 ++-- user_guide/img/bno055_cal_erase.jpg | Bin user_guide/img/bno055_cal_instructions.jpg | Bin user_guide/img/bno055_cal_mode.jpg | Bin user_guide/img/bno055_cal_save.jpg | Bin user_guide/img/bno055_cal_status.jpg | Bin user_guide/img/comm_protocol_construction.dia | Bin user_guide/img/comm_protocol_construction.png | Bin user_guide/img/control_board_modes.jpg | Bin 0 -> 54171 bytes user_guide/messages/index.html | 386 ++++++++++-------- user_guide/preparing_board/index.html | 88 ++-- .../{simulator => pythoniface}/index.html | 86 ++-- user_guide/simcb_simulator/index.html | 225 ++++++++++ 85 files changed, 1843 insertions(+), 924 deletions(-) mode change 100755 => 100644 404.html mode change 100755 => 100644 css/fonts/Roboto-Slab-Bold.woff mode change 100755 => 100644 css/fonts/Roboto-Slab-Bold.woff2 mode change 100755 => 100644 css/fonts/Roboto-Slab-Regular.woff mode change 100755 => 100644 css/fonts/Roboto-Slab-Regular.woff2 mode change 100755 => 100644 css/fonts/fontawesome-webfont.eot mode change 100755 => 100644 css/fonts/fontawesome-webfont.svg mode change 100755 => 100644 css/fonts/fontawesome-webfont.ttf mode change 100755 => 100644 css/fonts/fontawesome-webfont.woff mode change 100755 => 100644 css/fonts/fontawesome-webfont.woff2 mode change 100755 => 100644 css/fonts/lato-bold-italic.woff mode change 100755 => 100644 css/fonts/lato-bold-italic.woff2 mode change 100755 => 100644 css/fonts/lato-bold.woff mode change 100755 => 100644 css/fonts/lato-bold.woff2 mode change 100755 => 100644 css/fonts/lato-normal-italic.woff mode change 100755 => 100644 css/fonts/lato-normal-italic.woff2 mode change 100755 => 100644 css/fonts/lato-normal.woff mode change 100755 => 100644 css/fonts/lato-normal.woff2 mode change 100755 => 100644 css/theme.css mode change 100755 => 100644 css/theme_extra.css rename {firmware => devs}/build/index.html (79%) mode change 100755 => 100644 create mode 100644 devs/buildsimcb/index.html rename {firmware => devs}/details/index.html (65%) mode change 100755 => 100644 rename {firmware => devs}/developing/index.html (55%) mode change 100755 => 100644 rename {firmware => devs}/math/index.html (64%) mode change 100755 => 100644 rename {firmware => devs}/math_res/coord_system_1.png (100%) mode change 100755 => 100644 rename {firmware => devs}/math_res/coord_system_2.png (100%) mode change 100755 => 100644 rename {firmware => devs}/math_res/coord_system_rotations.png (100%) mode change 100755 => 100644 rename {firmware => devs}/math_res/global_coord1.png (100%) mode change 100755 => 100644 rename {firmware => devs}/math_res/global_coord2.png (100%) mode change 100755 => 100644 rename {firmware => devs}/math_res/global_invert.gif (100%) mode change 100755 => 100644 rename {firmware => devs}/math_res/robot_local_coord.png (100%) mode change 100755 => 100644 rename {firmware => devs}/math_res/thruster_config.png (100%) mode change 100755 => 100644 rename {firmware => devs}/overview/index.html (64%) mode change 100755 => 100644 rename {firmware/simhijack => devs/pythoniface}/index.html (66%) mode change 100755 => 100644 create mode 100644 devs/simulation/index.html mode change 100755 => 100644 hardware/img/bar30.png mode change 100755 => 100644 hardware/sensors/index.html mode change 100755 => 100644 hardware/v1/extra_additoins.png mode change 100755 => 100644 hardware/v1/fritzing_header_pos.png mode change 100755 => 100644 hardware/v1/index.html mode change 100755 => 100644 hardware/v1/prototype_assembled.png mode change 100755 => 100644 hardware/v1/prototype_assembled_labeled.png mode change 100755 => 100644 hardware/v1/prototype_axis.png mode change 100755 => 100644 hardware/v1/v1_back_side.png mode change 100755 => 100644 hardware/v1/wiring_diagram.dia mode change 100755 => 100644 hardware/v1/wiring_diagram.png mode change 100755 => 100644 hardware/v2/index.html mode change 100755 => 100644 hardware/v2/protoboard_headers.png mode change 100755 => 100644 hardware/v2/v2_back.png mode change 100755 => 100644 hardware/v2/v2_coord_system.png mode change 100755 => 100644 hardware/v2/v2_front.png mode change 100755 => 100644 hardware/v2/v2_labeled.png mode change 100755 => 100644 hardware/v2/wiring_diagram.dia mode change 100755 => 100644 hardware/v2/wiring_diagram.png mode change 100755 => 100644 img/favicon.ico mode change 100755 => 100644 index.html mode change 100755 => 100644 js/html5shiv.min.js mode change 100755 => 100644 js/jquery-3.6.0.min.js mode change 100755 => 100644 js/mathjax.js mode change 100755 => 100644 js/theme.js mode change 100755 => 100644 js/theme_extra.js mode change 100755 => 100644 none/index.html mode change 100755 => 100644 search.html mode change 100755 => 100644 search/lunr.js mode change 100755 => 100644 search/main.js mode change 100755 => 100644 search/search_index.json mode change 100755 => 100644 search/worker.js mode change 100755 => 100644 sitemap.xml mode change 100755 => 100644 sitemap.xml.gz mode change 100755 => 100644 user_guide/calibration/index.html mode change 100755 => 100644 user_guide/comm_protocol/index.html mode change 100755 => 100644 user_guide/general_use/index.html mode change 100755 => 100644 user_guide/img/bno055_cal_erase.jpg mode change 100755 => 100644 user_guide/img/bno055_cal_instructions.jpg mode change 100755 => 100644 user_guide/img/bno055_cal_mode.jpg mode change 100755 => 100644 user_guide/img/bno055_cal_save.jpg mode change 100755 => 100644 user_guide/img/bno055_cal_status.jpg mode change 100755 => 100644 user_guide/img/comm_protocol_construction.dia mode change 100755 => 100644 user_guide/img/comm_protocol_construction.png create mode 100644 user_guide/img/control_board_modes.jpg mode change 100755 => 100644 user_guide/messages/index.html mode change 100755 => 100644 user_guide/preparing_board/index.html rename user_guide/{simulator => pythoniface}/index.html (64%) mode change 100755 => 100644 create mode 100644 user_guide/simcb_simulator/index.html diff --git a/404.html b/404.html old mode 100755 new mode 100644 index cd268b92..00ad2576 --- a/404.html +++ b/404.html @@ -8,14 +8,13 @@ AUV Control Board - + - - - + + @@ -27,7 +26,7 @@ AUV Control Board
- +
@@ -41,9 +40,11 @@

Developers

@@ -89,13 +94,12 @@
    -
  • »
  • +

-
@@ -135,17 +139,18 @@

404

- - - - - - - - + + + + + + + + diff --git a/css/fonts/Roboto-Slab-Bold.woff b/css/fonts/Roboto-Slab-Bold.woff old mode 100755 new mode 100644 diff --git a/css/fonts/Roboto-Slab-Bold.woff2 b/css/fonts/Roboto-Slab-Bold.woff2 old mode 100755 new mode 100644 diff --git a/css/fonts/Roboto-Slab-Regular.woff b/css/fonts/Roboto-Slab-Regular.woff old mode 100755 new mode 100644 diff --git a/css/fonts/Roboto-Slab-Regular.woff2 b/css/fonts/Roboto-Slab-Regular.woff2 old mode 100755 new mode 100644 diff --git a/css/fonts/fontawesome-webfont.eot b/css/fonts/fontawesome-webfont.eot old mode 100755 new mode 100644 diff --git a/css/fonts/fontawesome-webfont.svg b/css/fonts/fontawesome-webfont.svg old mode 100755 new mode 100644 diff --git a/css/fonts/fontawesome-webfont.ttf b/css/fonts/fontawesome-webfont.ttf old mode 100755 new mode 100644 diff --git a/css/fonts/fontawesome-webfont.woff b/css/fonts/fontawesome-webfont.woff old mode 100755 new mode 100644 diff --git a/css/fonts/fontawesome-webfont.woff2 b/css/fonts/fontawesome-webfont.woff2 old mode 100755 new mode 100644 diff --git a/css/fonts/lato-bold-italic.woff b/css/fonts/lato-bold-italic.woff old mode 100755 new mode 100644 diff --git a/css/fonts/lato-bold-italic.woff2 b/css/fonts/lato-bold-italic.woff2 old mode 100755 new mode 100644 diff --git a/css/fonts/lato-bold.woff b/css/fonts/lato-bold.woff old mode 100755 new mode 100644 diff --git a/css/fonts/lato-bold.woff2 b/css/fonts/lato-bold.woff2 old mode 100755 new mode 100644 diff --git a/css/fonts/lato-normal-italic.woff b/css/fonts/lato-normal-italic.woff old mode 100755 new mode 100644 diff --git a/css/fonts/lato-normal-italic.woff2 b/css/fonts/lato-normal-italic.woff2 old mode 100755 new mode 100644 diff --git a/css/fonts/lato-normal.woff b/css/fonts/lato-normal.woff old mode 100755 new mode 100644 diff --git a/css/fonts/lato-normal.woff2 b/css/fonts/lato-normal.woff2 old mode 100755 new mode 100644 diff --git a/css/theme.css b/css/theme.css old mode 100755 new mode 100644 index 7e03995c..ad773009 --- a/css/theme.css +++ b/css/theme.css @@ -6,8 +6,8 @@ * https://github.com/readthedocs/sphinx_rtd_theme */ - /* sphinx_rtd_theme version 1.0.0 | MIT license */ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,.wy-nav-top a,.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + /* sphinx_rtd_theme version 1.2.0 | MIT license */ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.rst-content .wy-breadcrumbs li tt,.wy-breadcrumbs li .rst-content tt,.wy-breadcrumbs li code{padding:5px;border:none;background:none}.rst-content .wy-breadcrumbs li tt.literal,.wy-breadcrumbs li .rst-content tt.literal,.wy-breadcrumbs li code.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.field-list>dt:after,html.writer-html5 .rst-content dl.footnote>dt:after{content:":"}html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.footnote>dt>span.brackets{margin-right:.5rem}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{font-style:italic}html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.footnote>dd p,html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} diff --git a/css/theme_extra.css b/css/theme_extra.css old mode 100755 new mode 100644 index 69d567d4..ab0631a1 --- a/css/theme_extra.css +++ b/css/theme_extra.css @@ -21,11 +21,11 @@ * https://github.com/mkdocs/mkdocs/issues/233 */ .rst-content pre code { - white-space: pre; - word-wrap: normal; - display: block; - padding: 12px; - font-size: 12px; + white-space: pre; + word-wrap: normal; + display: block; + padding: 12px; + font-size: 12px; } /** @@ -77,9 +77,9 @@ pre .cs, pre .c { * https://github.com/mkdocs/mkdocs/issues/319 */ .rst-content .no-highlight { - display: block; - padding: 0.5em; - color: #333; + display: block; + padding: 0.5em; + color: #333; } @@ -103,7 +103,7 @@ pre .cs, pre .c { form .search-query { width: 100%; border-radius: 50px; - padding: 6px 12px; /* csslint allow: box-model */ + padding: 6px 12px; border-color: #D1D4D5; } @@ -113,11 +113,11 @@ form .search-query { * https://github.com/mkdocs/mkdocs/issues/656 */ .rst-content .admonition code { - color: #404040; - border: 1px solid #c7c9cb; - border: 1px solid rgba(0, 0, 0, 0.2); - background: #f8fbfd; - background: rgba(255, 255, 255, 0.7); + color: #404040; + border: 1px solid #c7c9cb; + border: 1px solid rgba(0, 0, 0, 0.2); + background: #f8fbfd; + background: rgba(255, 255, 255, 0.7); } /* @@ -135,8 +135,8 @@ form .search-query { } td, th { - border: 1px solid #e1e4e5 !important; /* csslint allow: important */ - border-collapse: collapse; + border: 1px solid #e1e4e5 !important; + border-collapse: collapse; } /* @@ -155,6 +155,12 @@ td, th { padding-bottom: 40px; } +/* For section-index only */ +.wy-menu-vertical .current-section p { + background-color: #e3e3e3; + color: #404040; +} + /* * The second step of above amendment: Here we make sure the items are aligned * correctly within the .rst-current-version container. Using flexbox, we diff --git a/firmware/build/index.html b/devs/build/index.html old mode 100755 new mode 100644 similarity index 79% rename from firmware/build/index.html rename to devs/build/index.html index 8a01e779..9594e3dd --- a/firmware/build/index.html +++ b/devs/build/index.html @@ -3,26 +3,25 @@ - + - Build and Flash - AUV Control Board + Build and Flash Firmware - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
- +
@@ -48,9 +47,11 @@

Developers

@@ -104,20 +109,19 @@

-
-

Build and Flash

+

Build and Flash Firmware

Note: Run all commands shown in the firmware folder of the repo.

Building

Install the Required Tools:

@@ -127,7 +131,7 @@

Building

  • GNU Arm Toolchain
  • Make sure cmake, ninja, and arm-none-eabi-gcc are in your PATH.

    -

    Build the firmware using the commands below. Replace [preset] with v1 or v2. Replace [config] with debug, release, minsizerel, or relwithdebinfo.

    +

    Build the firmware using the commands below. Replace[preset] with v1 or v2 (depending on which version you are building firmware for). Replace [config] with debug, release, minsizerel, or relwithdebinfo.

    cmake --preset=[preset]
     cmake --build --preset=[preset]-[config]
     
    @@ -227,8 +231,8 @@

    Flashing Remotely

    - - - - - - - - + + + + + + + + diff --git a/devs/buildsimcb/index.html b/devs/buildsimcb/index.html new file mode 100644 index 00000000..13c7cad9 --- /dev/null +++ b/devs/buildsimcb/index.html @@ -0,0 +1,190 @@ + + + + + + + + Build and Run SimCB - AUV Control Board + + + + + + + + + + + + + +
    + + +
    + +
    +
    + +
    +
    +
    +
    + +

    Build and Run SimCB

    +

    Install the Required Tools:

    +
      +
    • CMake (version 3.20.0 or newer)
    • +
    • Ninja
    • +
    • C Toolchain for your system
        +
      • MSVC on windows
      • +
      • Apple Clang on macOS
      • +
      • GNU Toolchain (GCC) on Linux
      • +
      +
    • +
    +

    Make sure cmake and ninja are in your PATH.

    +

    Build the firmware using the commands below. Replace[preset] with simcb-win, simcb-macos, or simcb-linux. Replace [config] with debug, release, minsizerel, or relwithdebinfo.

    +
    cmake --preset=[preset]
    +cmake --build --preset=[preset]-[config]
    +
    +

    The SimCB binary will exist in build/[preset]/[config]/SimCB[.exe]

    + +
    +
    + +
    +
    + +
    + +
    + +
    + + + + GitHub + + + + « Previous + + + Next » + + +
    + + + + + + + + + + + + diff --git a/firmware/details/index.html b/devs/details/index.html old mode 100755 new mode 100644 similarity index 65% rename from firmware/details/index.html rename to devs/details/index.html index 4ed0287c..8a0c0a1d --- a/firmware/details/index.html +++ b/devs/details/index.html @@ -3,26 +3,25 @@ - + - Implementation Details - AUV Control Board + Firmware Details - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,9 +47,11 @@

    Developers

    @@ -98,16 +103,15 @@

    -
    @@ -132,22 +136,26 @@

    Implementation Details

    TODO: Threading model mutexes, dataflow details

    TODO: Generator use and import process details

    -

    TODO: Calibration of BNO055 -- EEPROM calibration status signature for valid check -- Erase only invalidates signature (potentially fewer writes) -- If calibration constants are valid, they are applied during bno055 configuration. Applying constants will prevent BNO055 from running calibration routine in bg. Applied calibration is assumed to be good by the sensor (CALIB_STAT has no relevance). -- Stored calibration commands - - Erase triggers reset of sensor causing constants to be lost and auto calibration routine to run - - Store triggers reset of sensor using the newly stored constants (thus no auto calibration runs) - - Load will send the stuff from eeprom -- There are separate BNO055 commands to read its calibration status register and the values from its calibration registers (these are only valid if CALIB_STAT is 3 for sensors used and BNO055 is in config mode) -- What are each of the calibration constants (offsets, radius)

    +

    TODO: Calibration of BNO055 (described from code perspective - see calibration page for user facing docs)

    +
      +
    • EEPROM calibration status signature for valid check
    • +
    • Erase only invalidates signature (potentially fewer writes)
    • +
    • If calibration constants are valid, they are applied during bno055 configuration. Applying constants will prevent BNO055 from running calibration routine in bg. Applied calibration is assumed to be good by the sensor (CALIB_STAT has no relevance).
    • +
    • Stored calibration commands
        +
      • Erase triggers reset of sensor causing constants to be lost and auto calibration routine to run
      • +
      • Store triggers reset of sensor using the newly stored constants (thus no auto calibration runs)
      • +
      • Load will send the stuff from eeprom
      • +
      +
    • +
    • There are separate BNO055 commands to read its calibration status register and the values from its calibration registers (these are only valid if CALIB_STAT is 3 for sensors used and BNO055 is in config mode)
    • +
    • What are each of the calibration constants (offsets, radius)
    • +
    - - - - - - - - + + + + + + + + diff --git a/firmware/developing/index.html b/devs/developing/index.html old mode 100755 new mode 100644 similarity index 55% rename from firmware/developing/index.html rename to devs/developing/index.html index d42b05fe..f8ce06fe --- a/firmware/developing/index.html +++ b/devs/developing/index.html @@ -3,26 +3,25 @@ - + - Firmware Development - AUV Control Board + Development Workflows - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,9 +47,11 @@

    Developers

    @@ -140,16 +149,15 @@

    -
    @@ -166,15 +174,18 @@

    Coordinate System Definition

    -

    While this coordinate system may seem strange to some (especially anyone who has worked with planes), the name of axes doesn't really matter.

    +

    While this coordinate system may seem strange to some (especially those who have worked with aircraft), the name of axes doesn't really matter. The definition of pitch, roll, and yaw relative to the front / top of the vehicle remain standard.

    6 Degree of Freedom Motion Control

    -

    Nomenclature & Convention Notes

    +

    Nomenclature, Notation, and Convention Notes

    1. Matrices are assumed to be zero indexed (not 1 indexed). This means the first element of a matrix \(M\) is \(m_{00}\) not \(m_{11}\). This is because the math will be implemented in C (which uses zero indexed arrays).
    2. -
    3. Thruster numbers (1-8) are used by the control board's user facing components. However, this math will use thruster indices (0-7) where index = number - 1. This is b
    4. -
    5. Rotations about axis (angular velocities / DoFs) are referred to as "xrot" (about x), "yrot" (about y), and "zrot" (about z).
    6. -
    7. The terms "pitch", "roll", and "yaw" are used to describe the vehicle's orientation in space. "Pitch" is angular velocity about the x-axis, "roll" is angular rotation about the y-axis, and "yaw" is angular velocity about the z-axis.
    8. +
    9. Thruster numbers (1-8) are used by the control board's user facing components. However, this math will use thruster indices (0-7) where index = number - 1. This is again because arrays in C are zero indexed.
    10. +
    11. Rotations about axes (angular velocities / DoFs) are referred to as "xrot" (about x), "yrot" (about y), and "zrot" (about z).
    12. +
    13. The terms "pitch", "roll", and "yaw" are used to describe the vehicle's orientation in space. These are a set of intrinsic Euler angles (Tait-Bryan angles to be precise) composed in the order yaw then pitch then roll (z-x'-y'' in the control board's coordinate system). Note that while these do follow the standard meanings of pitch, roll, and yaw relative to the vehicle, the axes have non-standard names. Thus, you cannot simply use standard formulas for converting between quaternions and pitch, roll, yaw.
    14. Velocities in DoFs are normalized (meaning -1.0 to 1.0).
    15. +
    16. Euler angles are represented as a set of three values \(\begin{pmatrix} pitch & roll & yaw\end{pmatrix}\)
    17. +
    18. Quaternions are represented either as a combination of a scalar component, \(s\), and a vector component, \(v\) as \(\{s,v\}\) or they may be represented using the names \(w\), \(x\), \(y\), and \(z\). In such a case this is equivelent to \(\{w, (x, y, z)\}\). In other words, \(x\), \(y\), and \(z\) are the components of the vector \(v\) and \(w=s\).
    19. +
    20. The conjugate of a quaternion, \(q\) is denoted \(q^*\)

    System Assumptions

      @@ -187,14 +198,14 @@

      System Assumptions

    • System does not have translational position information

    Example Vehicle

    -

    The examples on this page will use AquaPack robotics's SeaWolf VIII robot. This robot's thruster configuration is as shown below. The arrows indicate the direction a thruster moves water (meaning the arrows are opposite the direction the thruster excerpts force on the vehicle).

    +

    The examples on this page will use AquaPack robotics's SeaWolf VIII robot. This robot's thruster configuration is as shown below.

    The arrows indicate the direction the thruster moves water when powered in the positive direction. These arrows are opposite the direction force is excerpted on the vehicle. Note that the diagram above uses thruster numbers, not indices.

    DoF Matrix

    -

    The DoF Matrix, \(D\), is constructed based on the vehicle's thruster configuration. Rows of the matrix correspond to thrusters (by index). And columns of the matrix correspond to vehicle relative DoFs. Thus, this is an 8x6 matrix. Columns correspond to DoFs in the following order (0-5): x, y, z, pitch, roll, yaw. Note that pitch, roll, and yaw are written using the single variable notation p, r, h (h = heading = yaw).

    -

    \(D = \left(\begin{array}{c|c|c|c|c|c} d_0 & d_1 & d_2 & d_3 & d_4 & d_5 \end{array}\right) = \left(\begin{array}{c|c|c|c|c|c} d_x & d_y & d_z & d_p & d_r & d_h \end{array}\right)\)

    +

    The DoF Matrix, \(D\), is constructed based on the vehicle's thruster configuration. Rows of the matrix correspond to thrusters (by index). And columns of the matrix correspond to vehicle-relative DoFs. Thus, this is an 8x6 matrix (8 thrusters, 6 DoFs). Columns correspond to DoFs in the following order (0-5): x, y, z, xrot, yrot, zrot.

    +

    \(D = \left(\begin{array}{c|c|c|c|c|c} d_0 & d_1 & d_2 & d_3 & d_4 & d_5 \end{array}\right) = \left(\begin{array}{c|c|c|c|c|c} d_x & d_y & d_z & d_{xr} & d_{yr} & d_{zr} \end{array}\right)\)

    Each column of the DoF matrix, \(d_i\) is a set of thruster speeds that result in motion exclusively in the column's DoF. Additionally, the resultant motion should be the maximum possible speed, and in the positive direction. Each \(d_i\) is an 8 element column vector, with elements corresponding to thrusters (by index). All speeds should be normalized (between -1.0 and 1.0)

    For the example vehicle shown above, the following is the DoF matrix

    \(D = @@ -210,16 +221,16 @@

    DoF Matrix

    \end{pmatrix}\)

    Consider the first column: \(d_0 = d_x\). This column's thruster speeds should result in the vehicle moving as fast as possible in the +x direction (only). This is achieved by setting T2, T4 (index 1, 3) to the positive direction and T1, T3 (index 0, 2) to the negative direction at full speed (recall that the arrows are opposite the direction the thruster excerpts force on the vehicle). Thus

    \(d_x = \begin{pmatrix}-1 & +1 & -1 & +1 & 0 & 0 & 0 & 0\end{pmatrix}^T\)

    -

    Note that when constructing the DoF matrix for your vehicle, you should assume an ideal system and environment.

    +

    Note that when constructing the DoF matrix for your vehicle, you should assume an ideal system and environment (ignore variation between thrusters, environmental factors that create motion, etc).

    LOCAL Mode Motion

    -

    In LOCAL mode, motion is specified as a set of speeds in vehicle relative DoFs. The user provides the control board with a local target motion vector (\(t_l\)) where each element corresponds to a DoF.

    +

    In LOCAL mode, motion is specified as a set of speeds in vehicle-relative DoFs. The user provides the control board with a local target motion vector (\(t_l\)) where each element corresponds to a DoF.

    \(t_l = \begin{pmatrix} x & y & z & r_x & r_y & r_z \end{pmatrix}^T\)

    \(x\) is normalized velocity in +x direction
    \(y\) is normalized velocity in +y direction
    \(z\) is normalized velocity in +y direction
    -\(r_x\) is normalized velocity in +pitch direction (xrot) -\(r_y\) is normalized velocity in +roll direction (yrot) -\(r_z\) is normalized velocity in +yaw direction (zrot)

    +\(r_x\) is normalized angular velocity about the +x axis direction (xrot)
    +\(r_y\) is normalized angular velocity about the +y axis (yrot)
    +\(r_z\) is normalized angular velocity about the +z axis (zrot)

    By multiplying this target motion by the DoF matrix, \(D\), a speed vector \(s\) is obtained where each element of \(s\) corresponds to a specific thruster (by index).

    \(s = D t_l\)

    Consider the example where \(t_l = \begin{pmatrix}0 & 1 & 0 & 0 & 0 & 0\end{pmatrix}^T\). This should cause the vehicle to move at full possible speed forward (relative to the vehicle's orientation).

    @@ -237,7 +248,7 @@

    LOCAL Mode Motion

    \begin{pmatrix}0 \\ 1 \\ 0 \\ 0 \\ 0 \\ 0\end{pmatrix} = \begin{pmatrix}-1 \\ -1 \\ +1 \\ +1 \\ 0 \\ 0 \\ 0 \\ 0\end{pmatrix}\)

    -

    In the above example, it is trivial to see that this is the desired motion. However for a more complex example, a problem appears. Consider \(t_l = \begin{pmatrix}0 & 1 & 0 & 0 & 0 & 1\end{pmatrix}^T\). This describes the vehicle both moving forward and yawing at full possible speed.

    +

    In the above example, it is trivial to see that this is the desired motion. However for a more complex example, a problem appears. Consider \(t_l = \begin{pmatrix}0 & 1 & 0 & 0 & 0 & 1\end{pmatrix}^T\). This describes the vehicle both moving forward and about the z-axis at full possible speed.

    \(s = D t_l = \begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\ @@ -252,9 +263,10 @@

    LOCAL Mode Motion

    \begin{pmatrix}0 \\ 1 \\ 0 \\ 0 \\ 0 \\ 1\end{pmatrix} = \begin{pmatrix}0 \\ -2 \\ 0 \\ +2 \\ 0 \\ 0 \\ 0 \\ 0\end{pmatrix}\)

    -

    Notice that the resultant speed vector has motors moving in excess of 100% speed (elements with magnitude greater than 1.0). This is not possible. While a simple solution may seem to be dividing all elements of the vector by the one with the largest magnitude. This results in a scaled speed vector \(\hat{s}\)

    +

    Notice that the resultant speed vector has motors moving in excess of 100% speed (elements with magnitude greater than 1.0). This is not possible.

    +

    The simple solution would seem to be dividing all elements of the vector by the one with the largest magnitude. This results in a scaled speed vector \(\hat{s}\)

    \(\hat{s} = s \div \text{absmax}(s)\)

    -

    this will not work well in all cases. Consider \(t_l = \begin{pmatrix}0 & 1 & 1 & 1 & 1 & 1\end{pmatrix}^T\).

    +

    However, this method will not work well in all cases. Consider \(t_l = \begin{pmatrix}0 & 1 & 1 & 1 & 1 & 1\end{pmatrix}^T\).

    \(s = D t_l = \begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\ @@ -299,7 +311,7 @@

    LOCAL Mode Motion

    \end{pmatrix}\)

    Then for each thruster \(i\) an overlap vector \(o_i\) can be constructed as follows

    \(o_i = C (c^i)^T\)

    -

    where \(c^i\) is the \(i\)th row of \(C\). Thus, \(o_i\) is an 8 element vector where each element corresponds to a thruster (by index). Element \(j\) of \(o_i\) can either be \(1\) or a \(0\). \(1\) indicates that thrusters \(i\) and \(j\) overlap.

    +

    where \(c^i\) is the \(i\)th row of \(C\). Thus, \(o_i\) is an 8 element vector where each element corresponds to a thruster (by index). Element \(j\) of \(o_i\) can either be \(1\) or a \(0\). A \(1\) indicates that thrusters \(i\) and \(j\) overlap.

    For example,

    \(o_0 = C (c^0)^T = \begin{pmatrix} 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 \end{pmatrix}^T\)

    This shows that thruster index 0 (T1) overlaps with indices 0, 1, 2, and 3 (T1, T2, T3, T4).

    @@ -308,7 +320,7 @@

    LOCAL Mode Motion

    • Find the thruster (i) with the largest magnitude speed
    • Iterate over thruster i's overlap vector
    • -
    • For any thruster i overlaps with (j), divide thruster j's speed by the magnitude of thruster i's speed
    • +
    • For any thruster, j, with which thruster i overlaps, divide thruster j's speed by the magnitude of thruster i's speed
    • Repeat until the largest magnitude does not exceed 1.0
    while true
    @@ -346,14 +358,14 @@ 

    GLOBAL Mode Motion

    \(t_g = \begin{pmatrix} x & y & z & p & r & h \end{pmatrix}\)

    It is necessary to transform each DoF's motion into motions in the vehicle's DoFs. These speeds can then be passed to LOCAL mode.

    -

    WARNING: GLOBAL mode is impacted by gimbal lock issues with euler angles. This occurs when the vehicle's pitch is +/- 90 degrees. In this scenario the meaning of "increase / decrease pitch" is ambiguous. The vehicle will take the zero-roll route in this scenario. Thus, GLOBAL mode may produce undesirable motion if the roll is non-zero and you pitch through +/- 90. A potential solution for this could be some form of motion hysteresis to handle moving through gimbal lock orientations, however this is not implemented as of now.

    +

    WARNING: GLOBAL mode is impacted by gimbal lock issues with euler angles. This occurs when the vehicle's pitch is +/- 90 degrees. In this scenario the meaning of "increase / decrease pitch" is ambiguous. The vehicle will take the zero-roll route in this scenario (an arbitrary choice based on how euler angle conversion is implemented in the firmware). Thus, GLOBAL mode may produce undesirable motion if the roll is non-zero and you pitch through +/- 90. A potential solution for this could be some form of motion hysteresis to handle moving through gimbal lock orientations, however this is not implemented as of now.

    Translation DoFs

    The translation DoFs are easily transformed using gravity vectors. By applying a quaternion based rotation matrix to the base gravity vector, \(g_b = \begin{pmatrix}0 & 0 & -1\end{pmatrix}\), the following solution is determined for the current gravity vector, \(g_c\) given the vehicle's orientation quaternion, \(q\).

    \(\begin{pmatrix} 2*(q.x*q.z+q.w*q.y) \\ 2*(q.w*q.x-q.y*q.z) \\ -(q.w)^2+(q.x)^2+(q.y)^2-(q.z)^2\end{pmatrix}^T\)

    The minimal rotation from \(g_b\) to \(g_c\) is then calculated. Let this rotation be called \(q_{rot}\). This rotation will generally include no yaw component, unless the vehicle is upside down and facing backwards (eg pitch of 180 degrees) in which case it will contain a yaw component of 180 degrees. This is desirable as it ensures a continuous definition of what gy is even while the vehicle is flipping via pitch.

    This quaternion can then be applied to speeds in the gx, gy, gz basis (x, y, and z here) to rotate them onto the vehicle basis. Thus, this "converts" translation speeds from GLOBAL to LOCAL mode DoFs.

    However, it is not ideal to transform the translation vector all at once. It is best to do it in three stages to allow proper upscaling as needed (explained below).

    -

    Thus for each global mode translation vector \(\begin{pmatrix} 0 & y & 0 \end{pmatrix}\), \(\begin{pmatrix} 0 & 0 & z \end{pmatrix}\), and \(\begin{pmatrix} x & 0 & 0 \end{pmatrix}\) rotate it by \(q_{rot}\) to obtain \(t_x\), \(t_y\), and \(t_z\) respectively (note that \(\left\{s, v\right\}\) is a quaternion with scalar s and vector v).

    +

    Thus for each global mode translation vector \(\begin{pmatrix} x & 0 & 0 \end{pmatrix}\), \(\begin{pmatrix} 0 & y & 0 \end{pmatrix}\), and \(\begin{pmatrix} 0 & 0 & z \end{pmatrix}\) rotate it by \(q_{rot}\) to obtain \(t_x\), \(t_y\), and \(t_z\) respectively (note that \(\left\{s, v\right\}\) is a quaternion with scalar s and vector v).

    \(\left\{0, t_x\right\} = q_{rot} \left\{0, \begin{pmatrix} x & 0 & 0 \end{pmatrix} \right\} q_{rot}^*\)

    \(\left\{0, t_y\right\} = q_{rot} \left\{0, \begin{pmatrix} 0 & y & 0 \end{pmatrix} \right\} q_{rot}^*\)

    \(\left\{0, t_z\right\} = q_{rot} \left\{0, \begin{pmatrix} 0 & 0 & z \end{pmatrix} \right\} q_{rot}^*\)

    @@ -372,9 +384,13 @@

    Translation DoFs

  • \(l\) is a sum of three vectors (each with elements no larger than a magnitude of 1), thus it may have elements with a magnitude greater than 1.
  • Issue 1 should be handled first as correcting it may "fix" issue 2. Handling issue 2 first could result in downscaling speeds more than necessary.

    -

    Handling issue 1 requires the user to provide a little more information about the vehicle: relative speeds in each DoF. These can be used to calculate downscaling factors to slow down the faster directions (note: speeding up the slower directions would result in impossible speeds, but would be handled by solving issue 2; regardless it is less ideal).

    -

    These downscaling factors are calculated from "RELDOF" information provided by the user (see messages page of user guide). Here we will referr to the scale factors as \(m_x\), \(m_y\), \(m_z\), \(m_{rx}\), \(m_{ry}\), and \(m_{rz}\) for the x, y, z, xrot, yrot, and zrot DoFs respectively (note that these are vehicle DoFs).

    -

    Thus, the simplest option would be to let \(l = \begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\end{pmatrix}\). However, this may downscale more than necessary. Consider the slowest direction to have a speed of 0 in l. In this case, since the slowest direction is unused, we are downscaling too much. Thus, the following algorithm is used to select the ideal downscaling factors by "ignoring" the downscaling required for unused DoFs (DoFs with a speed of 0).

    +

    Handling issue 1 requires the user to provide a little more information about the vehicle: relative speeds in each DoF. These can be used to calculate downscaling factors to slow down the faster directions (note: speeding up the slower directions could result in impossible speeds, but would be handled by solving issue 2; regardless it is less ideal).

    +

    These downscaling factors are calculated from "RELDOF" information provided by the user (see messages page of user guide). This information is a set of constants, which will be referred to the as the scale factors as \(m_x\), \(m_y\), \(m_z\), \(m_{rx}\), \(m_{ry}\), and \(m_{rz}\) for the x, y, z, xrot, yrot, and zrot DoFs respectively (note that these are vehicle DoFs).

    +

    These scale factors makeup two groups: translational (\(m_x\), \(m_y\), and \(m_z\)) and rotational (\(m_{rx}\), \(m_{ry}\), and \(m_{rz}\)). Within each group, the fastest DoF's scale factor is set to 1. The other scale factors are a percentage of the fastest DoF in the group.

    +

    For example, if the vehicle can move at 2m/s along z, 1m/s along y, and 0.5m/s along x then \(m_x = 0.25\), \(m_y = 0.5\), and \(m_z = 1.0\).

    +

    Given these scale factors, the simplest option would be to let \(l = \begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\end{pmatrix}\). However, this may downscale more than necessary.

    +

    Consider a scenario where the speed vector \(l\) has a \(0\) in the position of the slowest DoF of the vehicle (eg if the vehicle is slowest in the x direction, the \(l\) vector would have a zero in the x position).

    +

    In this case, since the slowest direction is unused, we are downscaling too much. Thus, the following algorithm is used to select the ideal downscaling factors by "ignoring" the downscaling required for unused DoFs (DoFs with a speed of 0).

    // Zero downscale factors for unused DoFs
     if abs(l.x) == 0
         m_x = 0
    @@ -403,16 +419,17 @@ 

    Translation DoFs

    Rotation DoFs:

    Converting the GLOBAL mode rotations (increase/decrease pitch, roll, yaw) to motions about DoFs is a little harder. It requires decomposing the quaternion into euler angles, then calculating three quaternions describing one euler rotation each. In other words, given the vehicle's current rotation \(q\) we need to find \(q_{pitch}\), \(q_{roll}\) and \(q_{yaw}\) such that (based on the euler angle convention used by the control board)

    \(q = q_{yaw} q_{pitch} q_{roll}\)

    -

    This can be done by converting \(q\) to a set of euler angles \(e = \begin{pmatrix}pitch & roll & yaw\end{pmatrix}\) then constructing the following and converting each to a quaternion

    +

    Note: recall that when composing quaternions to be applied to a body, left-multiplied quaternions are applied in the extrinsic coordinate system (world-basis) whereas right-multiplied quaternions are applied in the intrinsic coordinate system (rotating body's basis). Thus, yaw is about the world-z axis (z), then pitch is about the vehicle's x-axis after yawing (x') and roll is about the vehicle's y-axis after both yawing then pitching (y''). This is consistent with the definitions of the coordinate system and pitch, roll, yaw for the control board.

    +

    Decomposing the quaternion can be done by converting \(q\) to a set of euler angles \(e = \begin{pmatrix}pitch & roll & yaw\end{pmatrix}\) then constructing the following and converting each to a quaternion

    \(e_{pitch} = \begin{pmatrix}pitch & 0 & 0\end{pmatrix} \rightarrow q_{pitch}\)

    \(e_{roll} = \begin{pmatrix}0 & roll & 0\end{pmatrix} \rightarrow q_{roll}\)

    \(e_{yaw} = \begin{pmatrix}0 & 0 & yaw\end{pmatrix} \rightarrow q_{yaw}\)

    -

    However, the euler angles obtained from \(q\) may not be correct for this use case. An equivalent angle go \(e\) (although improper) is \(e_{alt} = \begin{pmatrix} \pi - pitch & roll - \pi & yaw - \pi \end{pmatrix}\). We need to compensate for first roll then pitch. Thus, we need the euler angle with minimal roll component. This will be referred to here as \(e_b\) (which is either \(e\) or \(_{alt}\)).

    +

    However, the euler angles obtained from \(q\) may not be correct for this use case. An equivalent angle go \(e\) (although improper) is \(e_{alt} = \begin{pmatrix} \pi - pitch & roll - \pi & yaw - \pi \end{pmatrix}\). We need to compensate for first roll then pitch. Thus, we need the euler angle with minimal roll component. This will be referred to here as \(e_b\), equal to either \(e\) or \(_{alt}\), whichever has the smaller magnitude roll component.

    Then, given \(s\) vectors describing motion to change the vehicle's pitch, roll, or yaw

    \(s_{pitch} = \begin{pmatrix}p & 0 & 0\end{pmatrix}\)

    \(s_{roll} = \begin{pmatrix}0 & r & 0\end{pmatrix}\)

    \(s_{yaw} = \begin{pmatrix}0 & 0 & h\end{pmatrix}\)

    -

    We need to transform these onto the robot's axes as \(w\) vectors. For roll this is trivial as roll is about the vehicle's y axis. For pitch, this requires undoing roll first (rotate by \(q_{roll}*\)) and for yaw this requires undoing roll then pitch. Thus

    +

    We need to transform these onto the robot's axes as \(w\) vectors. For roll this is trivial as roll is about the vehicle's y axis. For pitch, this requires undoing roll first (rotate by \(q_{roll}^*\)) and for yaw this requires undoing roll then pitch. Thus

    \(w_{roll} = s_{roll}\)

    \(\left\{0, w_{pitch}\right\} = q_{roll}^* \left\{0, s_{pitch}\right\} q_{roll}\)

    \(\left\{0, w_{yaw}\right\} = q_{pitch}^* q_{roll}^* \left\{0, s_{yaw}\right\} q_{roll} q_{pitch}\)

    @@ -431,7 +448,6 @@

    Orientation Hold (OHOLD) Mode Motion
  • yrot PID: Controls rotation about vehicle y-axis
  • zrot PID: Controls rotation about vehicle z-axis
  • -

    Note that in some code, these PIDs are referred to as "SASSIST" PIDs. However, they are used for OHOLD too!

    The inputs to OHOLD mode are as follows

    • x: Translation along gx-axis (same as in GLOBAL mode)
    • @@ -444,15 +460,15 @@

      Orientation Hold (OHOLD) Mode Motion

      There are two variants of OHOLD mode

      • Variant 1 (OHOLD1): Speed for yaw (h) is used instead of PID control
      • -
      • Variant 2 (OHOLD2): PID is used for yaw (yaw speed / h is ignored)
      • +
      • Variant 2 (OHOLD2): PID is used for yaw (yaw speed h is ignored)
      -

      Variant 2 is actually then simpler variant. Variant 2 adds some additional complexity for orientation control.

      +

      Variant 2 is mathematically then simpler variant. Variant 1 adds some additional complexity for orientation control.

      Translation DoFs

      -

      The user provided x and y along with the z from the depth PID are handled the same way by SASSIST mode as they are in GLOBAL mode to obtain the final LOCAL mode translation vector \(l\).

      +

      The user provided x, y, and z translation speeds are handled the same way by OHOLD mode as they are in GLOBAL mode to obtain the final LOCAL mode translation vector \(l\).

      OHOLD2 Orientation Control

      While translation for OHOLD is nearly identical to GLOBAL mode, rotation DoFs are very different. Orientation is controlled with a set of 3 PID controllers that work in LOCAL DoFs. Thus it is necessary to determine the rotations necessary about the vehicle's axes to achieve the desired orientation.

      We are given a target orientation as euler angles, \(e_t\). This can be converted to a target orientation quaternion, \(q_t\) using the formulas derived in later sections.

      -

      Note: For OHOLD1 things after this point are the same (see section below for details on how to get \(q_t\) for OHOLD1).

      +

      Note: For OHOLD1 obtaining \(q_t\) is more complex. See the OHOLD1 section for details. However, what is done with \(q_t\) from here on, is the same in OHOLD1 as it is in OHOLD2.

      The vehicle's current orientation (as a quaternion) is also available from the IMU as \(q_c\). We need to calculate a quaternion \(q_d\) that represents the minimal rotation from \(q_c\) to \(q_t\). However, importantly we want \(q_d\) to be a rotation in the vehicle's basis. Recall that right multiplication of quaternions are applied in the vehicle's basis. Thus, to describe the target orientation as the current orientation plus a rotation in the vehicle's basis

      \(q_t = q_c q_d\)

      Therefore

      @@ -468,12 +484,13 @@

      OHOLD2 Orientation Control

      • let \(q_d = \left\{s, v\right\}\)
      • \(\theta = 2 atan2(|v|, s)\)
      • -
      • \(n = v / |v|\) if \(|v| > 0\) else \(n = v = 0\)
      • +
      • \(n = v / |v|\) if \(|v| > 0\) else \(n = v = (0, 0, 0)\)
      -

      This axis (\(n\)) will be a unit vector. Thus it represents proportions of the rotation about each of the vehicle's axes. The angle (\(\theta\)) is the magnitude of the rotation that must be taken.

      +

      This axis (\(n\)) will be a unit vector. Thus it represents proportions of the rotation about each of the vehicle's axes (this is about vehicle's axes because \(q_d\) is in the vehicle basis). The angle (\(\theta\)) is the magnitude of the rotation that must be taken.

      Thus, the product of \(n\) and \(\theta\) is proportional to the error in angle about each of the vehicle's axes.

      \(e = \theta n\)

      -

      This error vector, \(e\), contains the errors to be provided to each orientation PID (xrot, yrot, zrot). The output of these PIDs are angular velocity percentages (-1.0 to 1.0) about each of the vehicle's axes: \(w = \begin{pmatrix} w_x & w_y & w_z \end{pmatrix}\)

      +

      This error vector, \(e\), contains the errors to be provided to each orientation PID (xrot, yrot, zrot). The output of these PIDs are angular velocity percentages (-1.0 to 1.0) about each of the vehicle's axes (\(w_i\) denotes the output of the \(i\)rot PID)

      +

      \(w = \begin{pmatrix} w_x & w_y & w_z \end{pmatrix}\)

      Note: for OHOLD 1 there is an extra step here to calculate the correct w (see section below).

      Finally, it is necessary to downscale the \(w\) vectors just as in GLOBAL mode

        @@ -492,31 +509,99 @@

        OHOLD1 Orientation Control

        \(w_{yaw}\) will then be upscaled just as in GLOBAL mode.

        Then \(w = w + w_{yaw}\). This is then downscaled as described for OHOLD2.

        Stability Assist (SASSIST) Mode

        -

        Stability assist mode (SASSIST) is simply OHOLD mode, with the addition of closed-loop control for vehicle depth. Thus, instead of providing a z speed (for motion along the gz-axis), a PID controller is used to acheive the target depth.

        +

        Stability assist mode (SASSIST) is simply OHOLD mode, with the addition of closed-loop control for vehicle depth. Thus, instead of providing a z speed (for motion along the gz-axis), a PID controller is used to achieve the target depth.

        Inputs to this mode are the same as OHOLD mode, however instead of z, d_t is provided.

        • d_t: Target depth (meters; negative for below the surface)

        The PID used to maintain vehicle depth is referred to as the Depth PID. This PID uses the current vehicle depth (provided by a depth sensor) to calculate a z speed in the gz-axis, then provides it as an input to OHOLD mode.

        All other inputs from SASSIST are passed through to OHOLD mode as provided. Thus, there are two variants of SASSIST mode (variant 1 and variant 2) built ontop of the corresponding variants of OHOLD mode.

        -

        Depth Hold (DHOLD) Mode Motion

        -

        Depth hold (DHOLD) mode is GLOBAL mode but using a PID to control the vehicle's depth. The same depth PID used for SASSIST mode is used for DHOLD mode.

        -

        This mode uses the vehicle's current depth and a user provided target depth to calculate the depth PIDs error. The depth PID provides the gz-axis speed. Other speeds (x, y, p, r, and h) are provided by the user and mean the same thing as in GLOBAL mode.

        -

        The calculated z along with user-provided speeds are passed directly to GLOBAL mode after calculating z using the depth PID.

        -

        Note that this mode does not maintain vehicle orientation. In practice, this control mode is rarely useful.

        -

        Sensor Processing

        -

        TODO: Euler angle accumulation

        +

        Accumulated Euler Angles

        +

        WARNING: Make sure you understand what these are and are not before using them. These are not normal euler angles. They are intended to allow high level software using the control board to a measurement of orientation about an individual axis that accumulates similar to a gyroscope. Generally, use of these is discouraged. They are provided as a "compatibility bridge" for code developed for a gyroscope system.

        +

        WARNING: Accumulated euler angles are not the current orientation of the vehicle as an euler angle!

        +

        The euler and quaternion values provided by the IMU are not directly useful for tracking multiple rotations of the vehicle. Unlike simply integrating gyroscope data, euler angles (pitch, roll, yaw) and quaternions do not track the number of times the vehicle has rotated about a particular axis. They only track the vehicle's orientation in space.

        +

        While integrating raw gyro data would provide an accumulating measurement, such a solution would be rotations about the robot's axes. Generally, it is more useful to track the number of times the vehicle has "pitched", "rolled" or "yawed".

        +

        Note that an accumulated gyro z angle of 500 does not necessarily mean the robot has yawed 500 degrees; the robot could have been oriented at a pitch of 90. Instead, we want to measure how many degrees the vehicle has rotated through.

        +

        This can be done by tracking changes between subsequent quaternions from the IMU. The idea is to compare each quaternion read from the IMU with the previous quaternion received from the IMU (note that quaternions of all zeros are ignored to avoid issues with invalid IMU data samples after first configuring the sensor).

        +

        For each quaternion read from the IMU:

        +
          +
        • Calculate the shortest set of rotations from the previous to the current quaternion
        • +
        • Convert the shortest angle to euler angles
        • +
        • Add the pitch, roll, and yaw from the shortest euler angles to accumulated pitch, roll, and yaw variables
        • +
        • Note that if the IMU axis config changes, the accumulated data should be zeroed and the previously read quaternion discarded.
        • +
        +

        This makes the assumption that the smallest rotation between two quaternions is the most probable path the robot took to change its orientation. This is an approximation, however it is a fairly good one as long as sample rate of data is sufficiently high. The specifics of the path are lost, however, if the sample rate is high enough, the length of the path is sufficiently small that and this is a good approximation.

        +

        The second issue with this approximation has to do with rotations exceeding 180 degrees. The method for determining shortest path between two quaternions will be incorrect if the vehicle rotates more than 180 degrees in any axis (because the shortest path would have involved rotating the other direction). To guarantee rotations between two samples never exceed 180 degrees, the max measured rotation rate of the IMU is considered. For the BNO055 this is 2000 degrees per second. Thus, with a sample period of \(l\) milliseconds, the largest angle change between samples is

        +

        \(\frac{2000 \textrm{ deg}}{1 \textrm{ sec}} \cdot \frac{1 \textrm{ sec}}{1000 \textrm{ ms}} \cdot \frac{l \textrm{ ms}}{1 \textrm{ sample}} = 2l \textrm{ deg / sample}\)

        +

        To ensure that changes of more than 180 degrees do not occur, the following must be satisfied

        +

        \(2l < 180 \rightarrow l < 90 \textrm{ milliseconds}\)

        +

        However, it is possible for some samples from the IMU to be delayed (ie I2C bus busy with another sensor) or lost (I2C failure). Thus, it is necessary to choose a value for \(l\) that allows for at least one sample to be lost. When a sample is lost, this doubles the effective time between samples. Thus, it is necessary to half \(l\)

        +

        \(l < 45 \textrm{ milliseconds}\)

        +

        By further reducing \(l\) it is possible to allow for larger delays or more lost samples. The current firmware samples IMU data every 15ms (the max rate supported by the BNO055 in fusion mode is 100Hz = 10ms period). Using \(l=15 \textrm{ ms}\) it is possible for 5 consecutive samples to be lost while still guaranteeing that no more than 90ms passes between valid samples (thus still ensuring no more than 180 degree change between samples).

        Other Derivations

        -

        TODO: Euler / Quaternion conversion

        -

        TODO: Gravity vector calculation

        -

        TODO: Angle between vectors

        -

        TODO: Min diff quat between two quats

        +

        Euler to Quaternion Conversion

        +

        The euler angle convention used for the control board is an intrinsic set z-x'-y'' (rotate about z, then about new x, then about new y to compose an angle). Using the definitions of front/right/top for this coordinate system, this means that roll is about y, pitch is about x, and yaw is about z.

        +

        A rotation quaternion, \(Q\), can be composed using three rotations, each being one of the rotations used to construct the euler angle representation (in order).

        +

        \(Q = Q_z Q_x Q_y\)

        +

        Quaternion multiplication is associative.

        +

        Recall that right-multiplied quaternions are applied in the vehicle frame during composition. Thus, when viewed left to right:

        +
          +
        • First, apply \(Q_z\) in vehicle frame (also equal to world frame since this is the first rotation)
        • +
        • Then, apply \(Q_x\) in the vehicle frame
        • +
        • Finally, apply \(Q_y\) in the vehicle frame
        • +
        +

        However, recall that left-multiplied quaternions are applied in the world frame during composition. Thus, when viewed right to left

        +
          +
        • First, apply \(Q_y\) in the world frame
        • +
        • Then, apply \(Q_x\) in the world frame
        • +
        • Finally, apply \(Q_z\) in the world frame
        • +
        +

        In other words, the following are equivalent

        +
          +
        • Yaw about z, then pitch about x', then roll about y''
        • +
        • Roll about y, then pitch about x, the yaw about z
        • +
        +

        These use the same pitch, roll, and yaw angles, but the latter applies them about world axes, which have known and trivial direction vectors. Thus, each of pitch, roll, and yaw are a rotation about a known axis (x, y, or z respectively). Converting these from axis-angle from, the quaternions are represented as follows

        +

        \(Q_y = \left\{ cos(\frac{roll}{2}), sin(\frac{roll}{2}) \begin{pmatrix} 0 \\ 1 \\ 0 \end{pmatrix} \right\}\)

        +

        \(Q_x = \left\{ cos(\frac{pitch}{2}), sin(\frac{pitch}{2}) \begin{pmatrix} 1 \\ 0 \\ 0 \end{pmatrix} \right\}\)

        +

        \(Q_z = \left\{ cos(\frac{yaw}{2}), sin(\frac{yaw}{2}) \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix} \right\}\)

        +

        By multiplying these, Q can be obtained. This is relatively simple to do because each of the vectors defining the quaternion is a trivial vector (this is because of using world axes!)

        +

        \(Q = \left\{w, \begin{pmatrix}x \\ y \\ z\end{pmatrix}\right\} = Q_z Q_x Q_y\)

        +

        let \(cp = cos(\frac{pitch}{2})\), \(sp = sin(\frac{pitch}{2})\)

        +

        let \(cr = cos(\frac{roll}{2})\), \(sr = sin(\frac{roll}{2})\)

        +

        let \(cy = cos(\frac{yaw}{2})\), \(sr = sin(\frac{yaw}{2})\)

        +

        \(w = cy \cdot cp \cdot cr - sy \cdot sp \cdot sr\)

        +

        \(x = cy \cdot sp \cdot cr - sy \cdot cp \cdot sr\)

        +

        \(y = sy \cdot sp \cdot cr + cy \cdot cp \cdot sr\)

        +

        \(z = sy \cdot cp \cdot cr + cy \cdot sp \cdot sr\)

        +

        This provides a set of equations which can be used to convert from an euler angle representation to a quaternion.

        +

        Quaternion to Euler Conversion

        +

        Converting from quaternion to euler uses the rotation matrix representation as a go-between.

        +

        For the intrinsic set of euler angles z-x'-y'', a rotation matrix can be composed as follows (recall that right multiply applies about vehicle axis).

        +

        \(R = R_z(yaw) R_x(pitch) R_y(roll)\)

        +

        Similar to the quaternion case described above, note that matrix multiplication is associative. Thus, this can instead be interpreted as rotations about world axes in the opposite order. Thus, each of \(R_x\), \(R_y\), and \(R_z\) are rotations about world x, y, and z axes. Such matrices have known forms. When multiplied out, the following representation of R is obtained.

        +

        let \(cp = cos(pitch)\), \(sp = sin(pitch)\)

        +

        let \(cr = cos(roll)\), \(sr = sin(roll)\)

        +

        let \(cy = cos(yaw)\), \(sy = sin(yaw)\)

        +

        \(R = \begin{pmatrix} + cy \cdot cr - sr \cdot sy \cdot sp & -sy \cdot cp & cy \cdot sr + sy \cdot sp \cdot cr \\ + sy \cdot cr + sr \cdot sp \cdot cy & cy \cdot cp & sy \cdot sr - cr \cdot sp \cdot cy \\ + -cp \cdot sr & sp & cp \cdot cr +\end{pmatrix}\)

        +

        Using entries of this matrix, the following relations can be constructed

        +

        \(R_{32} = sin(pitch) \rightarrow pitch = sin^{-1}(R_{32})\)

        +

        \(\frac{R_{31}}{R_{33}} = \frac{-sin(roll)cos(pitch)}{cos(roll)cos(pitch)} = tan(roll) \rightarrow roll = tan^{-1}(\frac{-R_{31}}{R_{33}})\)

        +

        \(\frac{R_{12}}{R_{22}} = \frac{sin(yaw)cos(pitch)}{cos(yaw)cos(pitch)} = tan(yaw) \rightarrow yaw = tan^{-1}(\frac{R_{12}}{R_{22}})\)

        +

        A quaternion can also be converted to a rotation matrix with a known form. Using cells from this quaternion-backed rotation matrix yields the following relations

        +

        \(pitch = sin^{-1}(2(yz+wx))\)

        +

        \(roll = tan^{-1}(\frac{2(wy-xz)}{1-2(x^2+y^2)})\)

        +

        \(yaw = tan^{-1}(\frac{2(xy-wz)}{1-2(x^2 + z^2)})\)

        +

        Note that arctangent should be implemented in code using the quadrant aware atan2 to account for quadrants properly and avoid divide by zero issues.

    - - - - - - - - + + + + + + + + diff --git a/firmware/math_res/coord_system_1.png b/devs/math_res/coord_system_1.png old mode 100755 new mode 100644 similarity index 100% rename from firmware/math_res/coord_system_1.png rename to devs/math_res/coord_system_1.png diff --git a/firmware/math_res/coord_system_2.png b/devs/math_res/coord_system_2.png old mode 100755 new mode 100644 similarity index 100% rename from firmware/math_res/coord_system_2.png rename to devs/math_res/coord_system_2.png diff --git a/firmware/math_res/coord_system_rotations.png b/devs/math_res/coord_system_rotations.png old mode 100755 new mode 100644 similarity index 100% rename from firmware/math_res/coord_system_rotations.png rename to devs/math_res/coord_system_rotations.png diff --git a/firmware/math_res/global_coord1.png b/devs/math_res/global_coord1.png old mode 100755 new mode 100644 similarity index 100% rename from firmware/math_res/global_coord1.png rename to devs/math_res/global_coord1.png diff --git a/firmware/math_res/global_coord2.png b/devs/math_res/global_coord2.png old mode 100755 new mode 100644 similarity index 100% rename from firmware/math_res/global_coord2.png rename to devs/math_res/global_coord2.png diff --git a/firmware/math_res/global_invert.gif b/devs/math_res/global_invert.gif old mode 100755 new mode 100644 similarity index 100% rename from firmware/math_res/global_invert.gif rename to devs/math_res/global_invert.gif diff --git a/firmware/math_res/robot_local_coord.png b/devs/math_res/robot_local_coord.png old mode 100755 new mode 100644 similarity index 100% rename from firmware/math_res/robot_local_coord.png rename to devs/math_res/robot_local_coord.png diff --git a/firmware/math_res/thruster_config.png b/devs/math_res/thruster_config.png old mode 100755 new mode 100644 similarity index 100% rename from firmware/math_res/thruster_config.png rename to devs/math_res/thruster_config.png diff --git a/firmware/overview/index.html b/devs/overview/index.html old mode 100755 new mode 100644 similarity index 64% rename from firmware/overview/index.html rename to devs/overview/index.html index 7dba1813..68956786 --- a/firmware/overview/index.html +++ b/devs/overview/index.html @@ -3,26 +3,25 @@ - + - Overview - AUV Control Board + Firmware Overview - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,9 +47,11 @@

    Developers

    @@ -98,29 +103,41 @@

    -

    Overview

    TODO: Project Structure and Build System

    +

    TODO: Hardware abstraction (not really proper abstraction, just multiple implementatiuons of the same external API defined in include/hardware headers; each target has implementation in src/hardware files using include guards to choose which implementation)

    TODO: Generator projects

    + +

    TODO: System architecture

    - - - - - - - - + + + + + + + + diff --git a/firmware/simhijack/index.html b/devs/pythoniface/index.html old mode 100755 new mode 100644 similarity index 66% rename from firmware/simhijack/index.html rename to devs/pythoniface/index.html index 3f7059a1..3e868dd4 --- a/firmware/simhijack/index.html +++ b/devs/pythoniface/index.html @@ -3,26 +3,25 @@ - + - Simulation Support - AUV Control Board + Interface Scripts - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,9 +47,11 @@

    Developers

    @@ -98,26 +103,26 @@

    -
    -

    Simulation Support

    -

    TODO

    +

    Interface Scripts

    +

    TODO: Implementation details / architecture

    - - - - - - - - + + + + + + + + diff --git a/devs/simulation/index.html b/devs/simulation/index.html new file mode 100644 index 00000000..ac811467 --- /dev/null +++ b/devs/simulation/index.html @@ -0,0 +1,205 @@ + + + + + + + + Simulation Support - AUV Control Board + + + + + + + + + + + + + +
    + + +
    + +
    +
    + +
    +
    +
    +
    + +

    Simulation

    +

    SimCB

    +

    SimCB is the name used for the control board firmware built as a binary to run on a PC operating system (Windows, macOS, Linux). This is achieved using the FreeRTOS windows or posix ports. Instead of using UART over USB (as real hardware does), SimCB opens TCP sockets (server) for communication. The code using the control board would then connect to this socket (as a client) instead of opening a UART port to talk with a physical control board. The communication protocol (message structure and format) is exactly the same as described for UART.

    +

    The SimCB allows a control board to run without hardware. The same firmware that would run on the board instead runs on your PC. There are a few limitations with this. SimCB will only operate in simhijack mode (it cannot be released from simhijack) and only the sim sensors will be available.

    +

    TODO: Details on where this code lives, how TCP is handled and why.

    +

    Simulation Hijack

    +

    TODO: Simhijack and support for this in firmware (including multiple sensor stuff)

    +

    TODO: Why? Allow a simulated environment to use a control board to actually control a simulated vehicle. Allows motion testing / validation without in-water time (with a real control board). Also allows debugging / testing actual firmware without in-water time with a vehicle.

    +

    TODO: By combining with SimCB (which can also be simhijacked), motion can be tested without in-water time and without a real control board.

    +

    Simulator Implementation & Development

    +

    The simulator is implemented using the Godot game engine (3.5). This provides a 3D rendering and physics engine.

    +

    GodotAUVSim on GitHub

    +

    This simulator was initially designed to allow control board firmware development without in-water testing time with a physical vehicle. It has expanded into a reference simulator for end user use. It does not model any environment, just the vehicle(s).

    +

    TODO: Implementation details / tech docs

    +

    TODO: Docs on adding vehicles, etc

    +

    Control Board Setups

    +

    Thus, there are four ways a "control board" can be used

    +
      +
    • Real control board over UART
    • +
    • SimCB over TCP
    • +
    • Simulator models environment and provides inputs to control board and retrieves outputs from control board
        +
      • Can also do this with SimCB. Simulator is a "man in the middle"
      • +
      +
    • +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + + + GitHub + + + + « Previous + + + Next » + + +
    + + + + + + + + + + + + diff --git a/hardware/img/bar30.png b/hardware/img/bar30.png old mode 100755 new mode 100644 diff --git a/hardware/sensors/index.html b/hardware/sensors/index.html old mode 100755 new mode 100644 index 7794ed82..3df90e74 --- a/hardware/sensors/index.html +++ b/hardware/sensors/index.html @@ -8,21 +8,20 @@ Off-Board Sensors - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,9 +47,11 @@

    Developers

    @@ -98,16 +103,15 @@

    -
    @@ -120,7 +124,7 @@

    Off-Board Sensors

    - - - - - - - - + + + + + + + + diff --git a/hardware/v1/extra_additoins.png b/hardware/v1/extra_additoins.png old mode 100755 new mode 100644 diff --git a/hardware/v1/fritzing_header_pos.png b/hardware/v1/fritzing_header_pos.png old mode 100755 new mode 100644 diff --git a/hardware/v1/index.html b/hardware/v1/index.html old mode 100755 new mode 100644 index 448b4dee..8bdbe932 --- a/hardware/v1/index.html +++ b/hardware/v1/index.html @@ -8,21 +8,20 @@ Version 1 - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,9 +47,11 @@

    Hardware

    @@ -104,16 +109,15 @@

    -
    @@ -209,17 +213,18 @@

    Assembly Instructions

    - - - - - - - - + + + + + + + + diff --git a/hardware/v1/prototype_assembled.png b/hardware/v1/prototype_assembled.png old mode 100755 new mode 100644 diff --git a/hardware/v1/prototype_assembled_labeled.png b/hardware/v1/prototype_assembled_labeled.png old mode 100755 new mode 100644 diff --git a/hardware/v1/prototype_axis.png b/hardware/v1/prototype_axis.png old mode 100755 new mode 100644 diff --git a/hardware/v1/v1_back_side.png b/hardware/v1/v1_back_side.png old mode 100755 new mode 100644 diff --git a/hardware/v1/wiring_diagram.dia b/hardware/v1/wiring_diagram.dia old mode 100755 new mode 100644 diff --git a/hardware/v1/wiring_diagram.png b/hardware/v1/wiring_diagram.png old mode 100755 new mode 100644 diff --git a/hardware/v2/index.html b/hardware/v2/index.html old mode 100755 new mode 100644 index e7cc886d..cb7bf11d --- a/hardware/v2/index.html +++ b/hardware/v2/index.html @@ -8,21 +8,20 @@ Version 2 - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,9 +47,11 @@
    @@ -104,16 +109,15 @@

    -
    @@ -213,17 +217,18 @@

    Assembly Instructions

    - - - - - - - - + + + + + + + + diff --git a/hardware/v2/protoboard_headers.png b/hardware/v2/protoboard_headers.png old mode 100755 new mode 100644 diff --git a/hardware/v2/v2_back.png b/hardware/v2/v2_back.png old mode 100755 new mode 100644 diff --git a/hardware/v2/v2_coord_system.png b/hardware/v2/v2_coord_system.png old mode 100755 new mode 100644 diff --git a/hardware/v2/v2_front.png b/hardware/v2/v2_front.png old mode 100755 new mode 100644 diff --git a/hardware/v2/v2_labeled.png b/hardware/v2/v2_labeled.png old mode 100755 new mode 100644 diff --git a/hardware/v2/wiring_diagram.dia b/hardware/v2/wiring_diagram.dia old mode 100755 new mode 100644 diff --git a/hardware/v2/wiring_diagram.png b/hardware/v2/wiring_diagram.png old mode 100755 new mode 100644 diff --git a/img/favicon.ico b/img/favicon.ico old mode 100755 new mode 100644 diff --git a/index.html b/index.html old mode 100755 new mode 100644 index 7997ede7..133d655b --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ AUV Control Board - + - - - + + @@ -34,14 +33,14 @@ AUV Control Board
    - +
    @@ -100,15 +105,14 @@

    -
    @@ -158,23 +162,24 @@

    License

    - - - - - - - - + + + + + + + + diff --git a/js/html5shiv.min.js b/js/html5shiv.min.js old mode 100755 new mode 100644 diff --git a/js/jquery-3.6.0.min.js b/js/jquery-3.6.0.min.js old mode 100755 new mode 100644 diff --git a/js/mathjax.js b/js/mathjax.js old mode 100755 new mode 100644 index 34e59dbb..954b660e --- a/js/mathjax.js +++ b/js/mathjax.js @@ -1,16 +1,16 @@ -window.MathJax = { - tex: { - inlineMath: [["\\(", "\\)"]], - displayMath: [["\\[", "\\]"]], - processEscapes: true, - processEnvironments: true - }, - options: { - ignoreHtmlClass: ".*|", - processHtmlClass: "arithmatex" - } -}; - -document$.subscribe(() => { - MathJax.typesetPromise() +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } +}; + +document$.subscribe(() => { + MathJax.typesetPromise() }) \ No newline at end of file diff --git a/js/theme.js b/js/theme.js old mode 100755 new mode 100644 index 9299d964..533e634b --- a/js/theme.js +++ b/js/theme.js @@ -1,2 +1,2 @@ /* sphinx_rtd_theme version 1.0.0 | MIT license */ -!function(n){var e={};function t(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return n[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=n,t.c=e,t.d=function(n,e,i){t.o(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:i})},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.t=function(n,e){if(1&e&&(n=t(n)),8&e)return n;if(4&e&&"object"==typeof n&&n&&n.__esModule)return n;var i=Object.create(null);if(t.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:n}),2&e&&"string"!=typeof n)for(var o in n)t.d(i,o,function(e){return n[e]}.bind(null,o));return i},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.p="",t(t.s=0)}([function(n,e,t){t(1),n.exports=t(3)},function(n,e,t){(function(){var e="undefined"!=typeof window?window.jQuery:t(2);n.exports.ThemeNav={navBar:null,win:null,winScroll:!1,winResize:!1,linkScroll:!1,winPosition:0,winHeight:null,docHeight:null,isRunning:!1,enable:function(n){var t=this;void 0===n&&(n=!0),t.isRunning||(t.isRunning=!0,e((function(e){t.init(e),t.reset(),t.win.on("hashchange",t.reset),n&&t.win.on("scroll",(function(){t.linkScroll||t.winScroll||(t.winScroll=!0,requestAnimationFrame((function(){t.onScroll()})))})),t.win.on("resize",(function(){t.winResize||(t.winResize=!0,requestAnimationFrame((function(){t.onResize()})))})),t.onResize()})))},enableSticky:function(){this.enable(!0)},init:function(n){n(document);var e=this;this.navBar=n("div.wy-side-scroll:first"),this.win=n(window),n(document).on("click","[data-toggle='wy-nav-top']",(function(){n("[data-toggle='wy-nav-shift']").toggleClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift")})).on("click",".wy-menu-vertical .current ul li a",(function(){var t=n(this);n("[data-toggle='wy-nav-shift']").removeClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift"),e.toggleCurrent(t),e.hashChange()})).on("click","[data-toggle='rst-current-version']",(function(){n("[data-toggle='rst-versions']").toggleClass("shift-up")})),n("table.docutils:not(.field-list,.footnote,.citation)").wrap("
    "),n("table.docutils.footnote").wrap("
    "),n("table.docutils.citation").wrap("
    "),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t
    "),n("table.docutils.footnote").wrap("
    "),n("table.docutils.citation").wrap("
    "),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;tNone - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,9 +47,11 @@

    Developers

    @@ -96,15 +101,14 @@

    -
    @@ -140,17 +144,18 @@
    - - - - - - - - + + + + + + + + diff --git a/search.html b/search.html old mode 100755 new mode 100644 index 74cfa2d1..aa02e304 --- a/search.html +++ b/search.html @@ -8,14 +8,13 @@ AUV Control Board - + - - - + + @@ -27,7 +26,7 @@ AUV Control Board
    - +
    @@ -41,9 +40,11 @@

    Developers

    @@ -89,13 +94,12 @@
      -
    • »
    • +

    -
    @@ -104,7 +108,7 @@

    Search Results

    @@ -142,17 +146,18 @@

    Search Results

    - - - - - - - - + + + + + + + + diff --git a/search/lunr.js b/search/lunr.js old mode 100755 new mode 100644 index 6aa370fb..aca0a167 --- a/search/lunr.js +++ b/search/lunr.js @@ -3456,7 +3456,7 @@ lunr.QueryParser.parseBoost = function (parser) { } else if (typeof exports === 'object') { /** * Node. Does not work with strict CommonJS, but - * only CommonJS-like enviroments that support module.exports, + * only CommonJS-like environments that support module.exports, * like Node. */ module.exports = factory() diff --git a/search/main.js b/search/main.js old mode 100755 new mode 100644 diff --git a/search/search_index.json b/search/search_index.json old mode 100755 new mode 100644 index fc8fbb95..785aa65e --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"AUV Control Board Documentation AUV Control Board is a low-cost open-source motion controller for (Autonomous) Underwater Vehicles. It is designed for vehicles with fixed position thrusters and supports motion in 6 degrees of freedom. There are currently two versions of control board. Neither version has more features than the other. Version 2 was developed to be able to use a different chip / board that was easier to get. Version 1 uses an Adafruit ItsyBitsy M4 (Microchip ATSAMD51 chip) Version 2 use a WeAct Studio Black Pill (STMicroelectronics STM32F4) The firmware is capable of running on both versions. License Both the firmware and provided demo and interface scripts are licensed under the GNU General Public License version 3 or later (GPL-3.0-or-later). Note that the firmware includes third party components licensed under their own terms.","title":"Home"},{"location":"#auv-control-board-documentation","text":"AUV Control Board is a low-cost open-source motion controller for (Autonomous) Underwater Vehicles. It is designed for vehicles with fixed position thrusters and supports motion in 6 degrees of freedom. There are currently two versions of control board. Neither version has more features than the other. Version 2 was developed to be able to use a different chip / board that was easier to get. Version 1 uses an Adafruit ItsyBitsy M4 (Microchip ATSAMD51 chip) Version 2 use a WeAct Studio Black Pill (STMicroelectronics STM32F4) The firmware is capable of running on both versions.","title":"AUV Control Board Documentation"},{"location":"#license","text":"Both the firmware and provided demo and interface scripts are licensed under the GNU General Public License version 3 or later (GPL-3.0-or-later). Note that the firmware includes third party components licensed under their own terms.","title":"License"},{"location":"none/","text":"Coming Soon.","title":"None"},{"location":"firmware/build/","text":"Build and Flash Note: Run all commands shown in the firmware folder of the repo. Building Install the Required Tools: CMake (version 3.20.0 or newer) Ninja GNU Arm Toolchain Make sure cmake , ninja , and arm-none-eabi-gcc are in your PATH . Build the firmware using the commands below. Replace [preset] with v1 or v2 . Replace [config] with debug , release , minsizerel , or relwithdebinfo . cmake --preset=[preset] cmake --build --preset=[preset]-[config] Flashing Note: Control Board v1 Units must update Adafruit's bootloader before first use. This only needs to be done once, but it must be done. Follow Adafruit's instructions here . DO NOT SKIP THIS!!! Version Flash Method Tool Alias Required software v1 sam-ba (via bootloader) bossa BOSSA (specifically bossac / bossa-cli) v1 uf2conv (via bootloader) uf2conv None v2 dfu-util (via bootloader) dfu-util dfu-util v2 STM32CubeProgrammer DFU (via bootloader) stm32-dfu STM32CubeProgrammer Note: required tool ( bossac , dfu-util , STM32_Programmer_CLI ) must be in your PATH . Before flashing, the chip needs to enter its bootloader (unless using a debug probe such as the stlink2 to flash) If a board is already flashed, it can be rebooted into its bootloader using the reboot_bootloader.py script in the firmware directory. Otherwise, use the hardware method described below. v1: Press the reset button twice quickly (double press). v2: Hold the BOOT button. While holding it, press and release the NRST button. Then release the boot button. Note: Sometimes reboot to bootloader mode \"fails\". On v1, this usually means it fails to attach USB (LED remains red not green.) On v2 this usually means it doesn't show up as a USB device. In either case, just try to enter again using the same button combination above. To flash, run the flash.py script. It is a wrapper that will call one of the above tools python3 flash.py [version] [config] -u [tool] [version] is either v1 or v2 [config] is the configuration you want to flash (same as configuration built: Debug , Release , MinSizeRel , or RelWithDebInfo ) [tool] is one of the above upload tool aliases. Flashing Remotely Sometimes, it is useful to flash firmware without connecting directly to the control board. Typically, this is done in-system where the embedded computer using the control board (Jetson, Raspberry Pi, etc) is used to flash the control board without gaining physical access to the control board. Instead an ssh connection to the remote computer is used. There are a few requirements to be able to flash remotely The control board must already be running some version of the firmware. This is necessary to be able to enter bootloader mode without access to the buttons on the board. The remote computer must have a flash tool installed. For v1 this should be bossac and for v2 this should be dfu-util . These are available as packages for most Linux distributions ( bossa-cli and dfu-util respectively for Debian and Ubuntu based systems). You must have ssh (and by extension scp) access to the remote system (typically via ethernet tether) On the build computer (laptop, etc) build the firmware as described above. Then, login to the remote system via ssh and create a directory to hold control board flash stuff (name can be changed as desired) # Run on remote computer (via ssh) cd ~ mkdir cboard-flash Then on the build computer, use scp to copy the flash.py and reboot_bootloader.py scripts to this folder # Run on build laptop scp firmware/flash.py user@remote_ip:cboard-flash/ scp firmware/reboot_bootloader.py user@remote_ip:cboard-flash/ Next copy the build folder. You can just copy the binaries themselves, but the folder hierarchy must be maintained. # Run on remote computer (via ssh) # Delete old build folder first rm -r ~/cboard-flash/build # Run on build laptop scp -r firmware/build user@remote_ip:cboard-flash/ Finally, reboot the control board to bootloader and flash # Run on remote computer (via ssh) ./reboot_bootloader.py [port] ./flash.py [version] [config] -p [port] Note: Sometimes reboot to bootloader mode \"fails\". On v1, this usually means it fails to attach USB (LED remains red not green.) On v2 this usually means it doesn't show up as a USB device. In either case, you will loose USB communication to the board. In this case, a power cycle is required to \"fix\" the board before trying to enter the bootloader again. While inconvenient, it is still usually easier to power cycle the vehicle than to unseal it.","title":"Build and Flash"},{"location":"firmware/build/#build-and-flash","text":"Note: Run all commands shown in the firmware folder of the repo.","title":"Build and Flash"},{"location":"firmware/build/#building","text":"Install the Required Tools: CMake (version 3.20.0 or newer) Ninja GNU Arm Toolchain Make sure cmake , ninja , and arm-none-eabi-gcc are in your PATH . Build the firmware using the commands below. Replace [preset] with v1 or v2 . Replace [config] with debug , release , minsizerel , or relwithdebinfo . cmake --preset=[preset] cmake --build --preset=[preset]-[config]","title":"Building"},{"location":"firmware/build/#flashing","text":"Note: Control Board v1 Units must update Adafruit's bootloader before first use. This only needs to be done once, but it must be done. Follow Adafruit's instructions here . DO NOT SKIP THIS!!! Version Flash Method Tool Alias Required software v1 sam-ba (via bootloader) bossa BOSSA (specifically bossac / bossa-cli) v1 uf2conv (via bootloader) uf2conv None v2 dfu-util (via bootloader) dfu-util dfu-util v2 STM32CubeProgrammer DFU (via bootloader) stm32-dfu STM32CubeProgrammer Note: required tool ( bossac , dfu-util , STM32_Programmer_CLI ) must be in your PATH . Before flashing, the chip needs to enter its bootloader (unless using a debug probe such as the stlink2 to flash) If a board is already flashed, it can be rebooted into its bootloader using the reboot_bootloader.py script in the firmware directory. Otherwise, use the hardware method described below. v1: Press the reset button twice quickly (double press). v2: Hold the BOOT button. While holding it, press and release the NRST button. Then release the boot button. Note: Sometimes reboot to bootloader mode \"fails\". On v1, this usually means it fails to attach USB (LED remains red not green.) On v2 this usually means it doesn't show up as a USB device. In either case, just try to enter again using the same button combination above. To flash, run the flash.py script. It is a wrapper that will call one of the above tools python3 flash.py [version] [config] -u [tool] [version] is either v1 or v2 [config] is the configuration you want to flash (same as configuration built: Debug , Release , MinSizeRel , or RelWithDebInfo ) [tool] is one of the above upload tool aliases.","title":"Flashing"},{"location":"firmware/build/#flashing-remotely","text":"Sometimes, it is useful to flash firmware without connecting directly to the control board. Typically, this is done in-system where the embedded computer using the control board (Jetson, Raspberry Pi, etc) is used to flash the control board without gaining physical access to the control board. Instead an ssh connection to the remote computer is used. There are a few requirements to be able to flash remotely The control board must already be running some version of the firmware. This is necessary to be able to enter bootloader mode without access to the buttons on the board. The remote computer must have a flash tool installed. For v1 this should be bossac and for v2 this should be dfu-util . These are available as packages for most Linux distributions ( bossa-cli and dfu-util respectively for Debian and Ubuntu based systems). You must have ssh (and by extension scp) access to the remote system (typically via ethernet tether) On the build computer (laptop, etc) build the firmware as described above. Then, login to the remote system via ssh and create a directory to hold control board flash stuff (name can be changed as desired) # Run on remote computer (via ssh) cd ~ mkdir cboard-flash Then on the build computer, use scp to copy the flash.py and reboot_bootloader.py scripts to this folder # Run on build laptop scp firmware/flash.py user@remote_ip:cboard-flash/ scp firmware/reboot_bootloader.py user@remote_ip:cboard-flash/ Next copy the build folder. You can just copy the binaries themselves, but the folder hierarchy must be maintained. # Run on remote computer (via ssh) # Delete old build folder first rm -r ~/cboard-flash/build # Run on build laptop scp -r firmware/build user@remote_ip:cboard-flash/ Finally, reboot the control board to bootloader and flash # Run on remote computer (via ssh) ./reboot_bootloader.py [port] ./flash.py [version] [config] -p [port] Note: Sometimes reboot to bootloader mode \"fails\". On v1, this usually means it fails to attach USB (LED remains red not green.) On v2 this usually means it doesn't show up as a USB device. In either case, you will loose USB communication to the board. In this case, a power cycle is required to \"fix\" the board before trying to enter the bootloader again. While inconvenient, it is still usually easier to power cycle the vehicle than to unseal it.","title":"Flashing Remotely"},{"location":"firmware/details/","text":"Implementation Details TODO: EEPROM Emulation SAMD51 (CBv1) Using NVMCTRL SmartEEPROM Must configure size using NVMCTRL user page (requires write of fuses / config bits then device reset; XC32 compiler uses pragma config to configur this. I see no other way with GCC) Note that as such, configuring fuse settings with MCC Standalone generator will do no good (it assumes XC32) Linker script modified to shorten rom to avoid the data for eeprom STM32 (CBv2) Using code from https://github.com/STMicroelectronics/STM32CubeF4/tree/master/Projects/STM32F411RE-Nucleo/Applications/EEPROM/EEPROM_Emulation as st_eeprom.h/c Using sectors 1 and 2 isr_vectors must be in sector 0, most of this sector is wasted Flash used by program starts at sector 3. Leaves 464k flash for use. Note that since flash is split, flashing must occur in two stages (boot and main) using dfu-util TODO: Threading model mutexes, dataflow details TODO: Generator use and import process details TODO: Calibration of BNO055 - EEPROM calibration status signature for valid check - Erase only invalidates signature (potentially fewer writes) - If calibration constants are valid, they are applied during bno055 configuration. Applying constants will prevent BNO055 from running calibration routine in bg. Applied calibration is assumed to be good by the sensor (CALIB_STAT has no relevance). - Stored calibration commands - Erase triggers reset of sensor causing constants to be lost and auto calibration routine to run - Store triggers reset of sensor using the newly stored constants (thus no auto calibration runs) - Load will send the stuff from eeprom - There are separate BNO055 commands to read its calibration status register and the values from its calibration registers (these are only valid if CALIB_STAT is 3 for sensors used and BNO055 is in config mode) - What are each of the calibration constants (offsets, radius)","title":"Implementation Details"},{"location":"firmware/details/#implementation-details","text":"TODO: EEPROM Emulation SAMD51 (CBv1) Using NVMCTRL SmartEEPROM Must configure size using NVMCTRL user page (requires write of fuses / config bits then device reset; XC32 compiler uses pragma config to configur this. I see no other way with GCC) Note that as such, configuring fuse settings with MCC Standalone generator will do no good (it assumes XC32) Linker script modified to shorten rom to avoid the data for eeprom STM32 (CBv2) Using code from https://github.com/STMicroelectronics/STM32CubeF4/tree/master/Projects/STM32F411RE-Nucleo/Applications/EEPROM/EEPROM_Emulation as st_eeprom.h/c Using sectors 1 and 2 isr_vectors must be in sector 0, most of this sector is wasted Flash used by program starts at sector 3. Leaves 464k flash for use. Note that since flash is split, flashing must occur in two stages (boot and main) using dfu-util TODO: Threading model mutexes, dataflow details TODO: Generator use and import process details TODO: Calibration of BNO055 - EEPROM calibration status signature for valid check - Erase only invalidates signature (potentially fewer writes) - If calibration constants are valid, they are applied during bno055 configuration. Applying constants will prevent BNO055 from running calibration routine in bg. Applied calibration is assumed to be good by the sensor (CALIB_STAT has no relevance). - Stored calibration commands - Erase triggers reset of sensor causing constants to be lost and auto calibration routine to run - Store triggers reset of sensor using the newly stored constants (thus no auto calibration runs) - Load will send the stuff from eeprom - There are separate BNO055 commands to read its calibration status register and the values from its calibration registers (these are only valid if CALIB_STAT is 3 for sensors used and BNO055 is in config mode) - What are each of the calibration constants (offsets, radius)","title":"Implementation Details"},{"location":"firmware/developing/","text":"Firmware Development Build and Flash Building and flashing the firmware should follow the same process described here . Debugging OpenOCD config files exist in tools/debug . The following configurations exist ControlBoard Version Debugger / Debug Probe Config Name v1 CMSIS-DAP* cb_v1_via_cmsisdap.cfg v2 ST-LINK v2 cb_v2_via_stlink2.cfg *The PicoProbe firmware can be used to turn a low cost Raspberry Pi Pico board into a CMSIS-DAP debugger. Note that OpenOCD must be installed and in the PATH . Note: NEVER power the system from both the debug probe and USB at the same time. Generally, power the system over USB and do not connect the power (3.3V) from the debug probe. Signals and GND must be connected from the debug probe. Generator Projects Each version of ControlBoard has an associated \"Generator\" project. These are projects used with the chip manufacturer's tools to generate startup / configuration / library code for the chip. Note: If you are just building the firmware you do not need to understand the generator projects. The necessary portions of each project are copied to the thirdparty folder. These are used for building. The generator projects are only used if something that was generated needs to be changed (or something new needs to be generated). Why Generators The decision to use generator projects comes down to the following There are multiple versions of Control Board with multiple chips. Using generators to create chip-specific initialization code reduces the chip-specific parts of the firmware that must be written / maintained. The generators are often required to use the manufacturer's HAL (or make using the HAL much easier). Use of the manufacturer's HAL / libraries reduces development time and makes maintenance easier (especially for those less familiar with the codebase). Having a GUI tool (which these generators usually are) to configure the system is an easy way to quickly understand the architecture of the system (clocks, peripheral use, pinout, etc). Running the Generator(s) Control Board v1 Control Board v1 uses an Adafruit ItsyBitsy M4 board (Microchip ATSAMD51G19A chip). The generator used for this project is MCC Standalone . This tool is available for download on Windows, macOS, and Linux. Tested with v5.2.1 of MCC Standalone. Once installed Launch the application Make sure the Harmony Content Path is set in Tools > Options . This should only need to be done once per computer. Recommended path is ~/.mcc/harmony/v3 . File > Load Configuration Choose generator_projects/ControlBoard_v1/firmware/ControlBoard_v1/ControlBoard_v1.mc3 You will likely be prompted to install MPLAB Harmony content. Install it. Click \"Generate\" in the top left. Control Board v2 Control Board v2 uses a WeAct Studio Black Pill board (STMicro STM32F411CEU chip). The generator used for this project is STM32CubeMX . This tool is available for download on Windows, macOS, and Linux. Tested with v6.6.1 of STM32CubeMX. Once installed Launch the application File > Load Project Choose generator_projects/ControlBoard_v2/ControlBoard_v2.ioc Install any required packs (as prompted) Click \"Generate Code\" in the top right. Importing from Generators After running the generator (as described above) the generated code must be imported to the project. The import process is mostly just copying generated files, however some files are modified slightly. The import process is handled by the import_from_generator.py script. If additional components are added in the generator projects, this script may need to be modified to import additional components. When this script is run, it will prompt a version of control board to import generated code for. This script must be run after each time the generator project is modified and code is re-generated. Development using VSCode Install the C/C++ and CMake Tools extensions, then open this folder in VSCode. Choose one of the configure presets on the bottom bar. Then, choose a build preset. Finally, click build. To debug, install the Cortex-Debug extension in VSCode. Copy tools/debug/launch.json to .vscode . MAKE SURE TO BUILD DEBUG CONFIG BEFORE LAUNCHING DEBUG SESSION. IT WILL NOT BUILD AUTOMATICALLY. Development using the Simulator The simulator can be very helpful for developing or debugging firmware. The simulator is capable of connecting to a real control board, thus the firmware can be tested in simulation. Note that the simulator also offers a simulated control board (simcb), but this is not what we want in this case. The simulated control board does not use a real control board / firmware. Testing with the simulator can also be done while the control board firmware is running with a debugger attached. To test with the simulator (your development system will need the simulator running and two USB ports are required) Connect the control board to your PC via USB Connect a debug probe to the control board (do NOT connect power) Build and run the firmware under debugger In the simulator, choose the UART port for the control board Interface scripts can be run as usual using launch.py , but add the -s flag so it will connect to the simulator instead of the physical control board. In the setup described above, the simulator connects to and uses the control board. The interface scripts connect to the simulator. The simulator forwards messages from interface scripts to control board and vice versa. The simulator provides simulated sensor data to the control board and the control board provides the simulator with thruster motions. There are a few things to not about the simulation It does increase traffic over control board UART / USB significantly (shouldn't cause issues, but something to be aware of for debugging purposes) Sensor data is provided by the simulator, thus sensors will report ready even if not connected. If real sensors are connected, they will be read as normal, however the data will be ignored and simulated data used instead. This allows connecting sensors to create more realistic test scenarios (timing and scheduling wise). When controlled by the simulator, the control board will not generate PWM signals on thruster pins. The thruster pins will maintain a pulse corresponding to no motion. IMU axis configuration has no effect in the simulator (though the command will still be acknowledged)","title":"Firmware Development"},{"location":"firmware/developing/#firmware-development","text":"","title":"Firmware Development"},{"location":"firmware/developing/#build-and-flash","text":"Building and flashing the firmware should follow the same process described here .","title":"Build and Flash"},{"location":"firmware/developing/#debugging","text":"OpenOCD config files exist in tools/debug . The following configurations exist ControlBoard Version Debugger / Debug Probe Config Name v1 CMSIS-DAP* cb_v1_via_cmsisdap.cfg v2 ST-LINK v2 cb_v2_via_stlink2.cfg *The PicoProbe firmware can be used to turn a low cost Raspberry Pi Pico board into a CMSIS-DAP debugger. Note that OpenOCD must be installed and in the PATH . Note: NEVER power the system from both the debug probe and USB at the same time. Generally, power the system over USB and do not connect the power (3.3V) from the debug probe. Signals and GND must be connected from the debug probe.","title":"Debugging"},{"location":"firmware/developing/#generator-projects","text":"Each version of ControlBoard has an associated \"Generator\" project. These are projects used with the chip manufacturer's tools to generate startup / configuration / library code for the chip. Note: If you are just building the firmware you do not need to understand the generator projects. The necessary portions of each project are copied to the thirdparty folder. These are used for building. The generator projects are only used if something that was generated needs to be changed (or something new needs to be generated).","title":"Generator Projects"},{"location":"firmware/developing/#why-generators","text":"The decision to use generator projects comes down to the following There are multiple versions of Control Board with multiple chips. Using generators to create chip-specific initialization code reduces the chip-specific parts of the firmware that must be written / maintained. The generators are often required to use the manufacturer's HAL (or make using the HAL much easier). Use of the manufacturer's HAL / libraries reduces development time and makes maintenance easier (especially for those less familiar with the codebase). Having a GUI tool (which these generators usually are) to configure the system is an easy way to quickly understand the architecture of the system (clocks, peripheral use, pinout, etc).","title":"Why Generators"},{"location":"firmware/developing/#running-the-generators","text":"","title":"Running the Generator(s)"},{"location":"firmware/developing/#control-board-v1","text":"Control Board v1 uses an Adafruit ItsyBitsy M4 board (Microchip ATSAMD51G19A chip). The generator used for this project is MCC Standalone . This tool is available for download on Windows, macOS, and Linux. Tested with v5.2.1 of MCC Standalone. Once installed Launch the application Make sure the Harmony Content Path is set in Tools > Options . This should only need to be done once per computer. Recommended path is ~/.mcc/harmony/v3 . File > Load Configuration Choose generator_projects/ControlBoard_v1/firmware/ControlBoard_v1/ControlBoard_v1.mc3 You will likely be prompted to install MPLAB Harmony content. Install it. Click \"Generate\" in the top left.","title":"Control Board v1"},{"location":"firmware/developing/#control-board-v2","text":"Control Board v2 uses a WeAct Studio Black Pill board (STMicro STM32F411CEU chip). The generator used for this project is STM32CubeMX . This tool is available for download on Windows, macOS, and Linux. Tested with v6.6.1 of STM32CubeMX. Once installed Launch the application File > Load Project Choose generator_projects/ControlBoard_v2/ControlBoard_v2.ioc Install any required packs (as prompted) Click \"Generate Code\" in the top right.","title":"Control Board v2"},{"location":"firmware/developing/#importing-from-generators","text":"After running the generator (as described above) the generated code must be imported to the project. The import process is mostly just copying generated files, however some files are modified slightly. The import process is handled by the import_from_generator.py script. If additional components are added in the generator projects, this script may need to be modified to import additional components. When this script is run, it will prompt a version of control board to import generated code for. This script must be run after each time the generator project is modified and code is re-generated.","title":"Importing from Generators"},{"location":"firmware/developing/#development-using-vscode","text":"Install the C/C++ and CMake Tools extensions, then open this folder in VSCode. Choose one of the configure presets on the bottom bar. Then, choose a build preset. Finally, click build. To debug, install the Cortex-Debug extension in VSCode. Copy tools/debug/launch.json to .vscode . MAKE SURE TO BUILD DEBUG CONFIG BEFORE LAUNCHING DEBUG SESSION. IT WILL NOT BUILD AUTOMATICALLY.","title":"Development using VSCode"},{"location":"firmware/developing/#development-using-the-simulator","text":"The simulator can be very helpful for developing or debugging firmware. The simulator is capable of connecting to a real control board, thus the firmware can be tested in simulation. Note that the simulator also offers a simulated control board (simcb), but this is not what we want in this case. The simulated control board does not use a real control board / firmware. Testing with the simulator can also be done while the control board firmware is running with a debugger attached. To test with the simulator (your development system will need the simulator running and two USB ports are required) Connect the control board to your PC via USB Connect a debug probe to the control board (do NOT connect power) Build and run the firmware under debugger In the simulator, choose the UART port for the control board Interface scripts can be run as usual using launch.py , but add the -s flag so it will connect to the simulator instead of the physical control board. In the setup described above, the simulator connects to and uses the control board. The interface scripts connect to the simulator. The simulator forwards messages from interface scripts to control board and vice versa. The simulator provides simulated sensor data to the control board and the control board provides the simulator with thruster motions. There are a few things to not about the simulation It does increase traffic over control board UART / USB significantly (shouldn't cause issues, but something to be aware of for debugging purposes) Sensor data is provided by the simulator, thus sensors will report ready even if not connected. If real sensors are connected, they will be read as normal, however the data will be ignored and simulated data used instead. This allows connecting sensors to create more realistic test scenarios (timing and scheduling wise). When controlled by the simulator, the control board will not generate PWM signals on thruster pins. The thruster pins will maintain a pulse corresponding to no motion. IMU axis configuration has no effect in the simulator (though the command will still be acknowledged)","title":"Development using the Simulator"},{"location":"firmware/math/","text":"Math Coordinate System Definition The control board uses a coordinate system that is somewhat non-standard. The coordinate system is right handed +y is forward, +z is up, +x is right Pitch is about x, roll is about y, yaw is about z While this coordinate system may seem strange to some (especially anyone who has worked with planes), the name of axes doesn't really matter. 6 Degree of Freedom Motion Control Nomenclature & Convention Notes Matrices are assumed to be zero indexed (not 1 indexed). This means the first element of a matrix \\(M\\) is \\(m_{00}\\) not \\(m_{11}\\) . This is because the math will be implemented in C (which uses zero indexed arrays). Thruster numbers (1-8) are used by the control board's user facing components. However, this math will use thruster indices (0-7) where index = number - 1 . This is b Rotations about axis (angular velocities / DoFs) are referred to as \"xrot\" (about x), \"yrot\" (about y), and \"zrot\" (about z). The terms \"pitch\", \"roll\", and \"yaw\" are used to describe the vehicle's orientation in space. \"Pitch\" is angular velocity about the x-axis, \"roll\" is angular rotation about the y-axis, and \"yaw\" is angular velocity about the z-axis. Velocities in DoFs are normalized (meaning -1.0 to 1.0). System Assumptions Vehicle is capable of motion exclusively in each of 6 degrees of freedom (DoFs). These are three translational DoFs, and three rotational DoFs. The vehicle's speed in positive and negative directions are roughly equal for each DoF. Thruster orientations are fixed. Gimbaled thruster vehicles are not supported. At most 8 thrusters (less is fine) System has 3D orientation information System has depth information System does not have translational position information Example Vehicle The examples on this page will use AquaPack robotics's SeaWolf VIII robot. This robot's thruster configuration is as shown below. The arrows indicate the direction a thruster moves water (meaning the arrows are opposite the direction the thruster excerpts force on the vehicle). The arrows indicate the direction the thruster moves water when powered in the positive direction. These arrows are opposite the direction force is excerpted on the vehicle. Note that the diagram above uses thruster numbers, not indices. DoF Matrix The DoF Matrix , \\(D\\) , is constructed based on the vehicle's thruster configuration. Rows of the matrix correspond to thrusters (by index). And columns of the matrix correspond to vehicle relative DoFs. Thus, this is an 8x6 matrix. Columns correspond to DoFs in the following order (0-5): x, y, z, pitch, roll, yaw. Note that pitch, roll, and yaw are written using the single variable notation p, r, h (h = heading = yaw). \\(D = \\left(\\begin{array}{c|c|c|c|c|c} d_0 & d_1 & d_2 & d_3 & d_4 & d_5 \\end{array}\\right) = \\left(\\begin{array}{c|c|c|c|c|c} d_x & d_y & d_z & d_p & d_r & d_h \\end{array}\\right)\\) Each column of the DoF matrix, \\(d_i\\) is a set of thruster speeds that result in motion exclusively in the column's DoF. Additionally, the resultant motion should be the maximum possible speed, and in the positive direction. Each \\(d_i\\) is an 8 element column vector, with elements corresponding to thrusters (by index). All speeds should be normalized (between -1.0 and 1.0) For the example vehicle shown above, the following is the DoF matrix \\(D = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix}\\) Consider the first column: \\(d_0 = d_x\\) . This column's thruster speeds should result in the vehicle moving as fast as possible in the +x direction (only). This is achieved by setting T2, T4 (index 1, 3) to the positive direction and T1, T3 (index 0, 2) to the negative direction at full speed (recall that the arrows are opposite the direction the thruster excerpts force on the vehicle). Thus \\(d_x = \\begin{pmatrix}-1 & +1 & -1 & +1 & 0 & 0 & 0 & 0\\end{pmatrix}^T\\) Note that when constructing the DoF matrix for your vehicle, you should assume an ideal system and environment. LOCAL Mode Motion In LOCAL mode, motion is specified as a set of speeds in vehicle relative DoFs. The user provides the control board with a local target motion vector ( \\(t_l\\) ) where each element corresponds to a DoF. \\(t_l = \\begin{pmatrix} x & y & z & r_x & r_y & r_z \\end{pmatrix}^T\\) \\(x\\) is normalized velocity in +x direction \\(y\\) is normalized velocity in +y direction \\(z\\) is normalized velocity in +y direction \\(r_x\\) is normalized velocity in +pitch direction (xrot) \\(r_y\\) is normalized velocity in +roll direction (yrot) \\(r_z\\) is normalized velocity in +yaw direction (zrot) By multiplying this target motion by the DoF matrix, \\(D\\) , a speed vector \\(s\\) is obtained where each element of \\(s\\) corresponds to a specific thruster (by index). \\(s = D t_l\\) Consider the example where \\(t_l = \\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 0\\end{pmatrix}^T\\) . This should cause the vehicle to move at full possible speed forward (relative to the vehicle's orientation). \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix} = \\begin{pmatrix}-1 \\\\ -1 \\\\ +1 \\\\ +1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix}\\) In the above example, it is trivial to see that this is the desired motion. However for a more complex example, a problem appears. Consider \\(t_l = \\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 1\\end{pmatrix}^T\\) . This describes the vehicle both moving forward and yawing at full possible speed. \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 1\\end{pmatrix} = \\begin{pmatrix}0 \\\\ -2 \\\\ 0 \\\\ +2 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix}\\) Notice that the resultant speed vector has motors moving in excess of 100% speed (elements with magnitude greater than 1.0). This is not possible. While a simple solution may seem to be dividing all elements of the vector by the one with the largest magnitude. This results in a scaled speed vector \\(\\hat{s}\\) \\(\\hat{s} = s \\div \\text{absmax}(s)\\) this will not work well in all cases. Consider \\(t_l = \\begin{pmatrix}0 & 1 & 1 & 1 & 1 & 1\\end{pmatrix}^T\\) . \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 1 \\\\ 1 \\\\ 1 \\\\ 1\\end{pmatrix} = \\begin{pmatrix}0 \\\\ -2 \\\\ 0 \\\\ +2 \\\\ -3 \\\\ -1 \\\\ -1 \\\\ +1\\end{pmatrix}\\) and \\(\\hat{s} = s \\div \\text{absmax}(s) = s \\div 3 = \\begin{pmatrix}0 \\\\ -0.67 \\\\ 0 \\\\ +0.67 \\\\ -1 \\\\ -0.33 \\\\ -0.33 \\\\ +0.33\\end{pmatrix}\\) While this has resulted in an possible set of thruster speeds, these are not optimal. Look at the example vehicle diagram. Notice that thrusters 1-4 and 5-8 control different motions. In the previous example, thrusters 1-4 were slowed down more than necessary, because thruster 5 was too large of a value. This is not ideal as the vehicle's maximum speed becomes artificially limited. Instead, the following \\(\\hat{s}\\) is ideal. This is scaling down the thrusters within each group (1-4 and 5-8) separately. \\(\\hat{s} = \\begin{pmatrix}0 \\\\ -1 \\\\ 0 \\\\ +1 \\\\ -1 \\\\ -0.33 \\\\ -0.33 \\\\ +0.33\\end{pmatrix}\\) Groupings of thrusters on the example vehicle are easy to observe, however this is not always true. Thus, achieving optimal scaling for any system (any DoF matrix) requires a more sophisticated method to determine groupings and scale speeds. Thruster groupings are determined by \"overlap\" between thrusters. Two thrusters, \\(i\\) and \\(j\\) are said to overlap if they have a non-zero entry in the same column of the DoF matrix ( \\(D\\) ) for at least one column. This is easier to calculate using a contribution matrix , \\(C\\) , defined as \\(D \\neq 0\\) . This results in a binary form of the DoF matrix. For the above example \\(C = \\left[\\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\neq 0 \\right] = \\begin{pmatrix} 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ \\end{pmatrix}\\) Then for each thruster \\(i\\) an overlap vector \\(o_i\\) can be constructed as follows \\(o_i = C (c^i)^T\\) where \\(c^i\\) is the \\(i\\) th row of \\(C\\) . Thus, \\(o_i\\) is an 8 element vector where each element corresponds to a thruster (by index). Element \\(j\\) of \\(o_i\\) can either be \\(1\\) or a \\(0\\) . \\(1\\) indicates that thrusters \\(i\\) and \\(j\\) overlap. For example, \\(o_0 = C (c^0)^T = \\begin{pmatrix} 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 \\end{pmatrix}^T\\) This shows that thruster index 0 (T1) overlaps with indices 0, 1, 2, and 3 (T1, T2, T3, T4). By calculating and storing these overlap vectors for each thruster ( \\(\\left\\{o_i\\right\\}_{i=0}^7\\) ), this effectively forms a lookup table to determine thruster overlap. While this is not the most memory efficient option, it reduces computation time, which is important since this will run very frequently on a microcontroller. Using overlap vectors, the following algorithm can be used to scale motor speeds: Find the thruster ( i ) with the largest magnitude speed Iterate over thruster i 's overlap vector For any thruster i overlaps with ( j ), divide thruster j 's speed by the magnitude of thruster i 's speed Repeat until the largest magnitude does not exceed 1.0 while true // m is value, i is index m, i = max(abs(speed_vector)) if m <= 1.0 // Done scaling break endif // Iterate over all thrusters (0-7 inclusive) for j=0...7 if overlap_vector[i][j] == 1 // i and j overlap. Divide j's speed by m. speed_vector[j] /= m endif endfor endwhile This algorithm results in optimal speed scaling by only reducing the speed of thrusters that share DoF contributions. GLOBAL Mode Motion GLOBAL mode is very similar to LOCAL mode, however, motion is described partially relative to the world instead of the robot. Specifically, motion of the vehicle is compensated for vehicle pitch and roll (but not yaw). This results in a coordinate system defined by the axes gx , gy , and gz . Note that the world coordinate system is defined as wx , wy , wz . Notably, if the vehicle is pitched 180 degrees, the gx-gy plane becomes aligned to the back of the vehicle ensuring consistent motion. This is shown in the animation below. In GLOBAL mode, the user provides the control board with a global target motion vector , \\(t_g\\) with 6 elements. This target motion vector is a concatenation of two 3 dimensional vectors. The first, a set of translations along gx , gy , and gz . Second a set of rotations to affect vehicle pitch, roll, and yaw. These are referred to by the following names x : Speed in gx direction (translation) y : Speed in gy direction (translation) z : Speed in gz direction (translation) p : Speed at which the vehicle's pitch should increase (negative for decrease pitch). Aka \"pitch_spd\" r : Speed at which the vehicle's roll should increase (negative for decrease pitch). Aka \"roll_spd\" h : Speed at which the vehicle's yaw should increase (negative for decrease pitch). Aka \"yaw_spd\" \\(t_g = \\begin{pmatrix} x & y & z & p & r & h \\end{pmatrix}\\) It is necessary to transform each DoF's motion into motions in the vehicle's DoFs. These speeds can then be passed to LOCAL mode. WARNING: GLOBAL mode is impacted by gimbal lock issues with euler angles. This occurs when the vehicle's pitch is +/- 90 degrees. In this scenario the meaning of \"increase / decrease pitch\" is ambiguous. The vehicle will take the zero-roll route in this scenario. Thus, GLOBAL mode may produce undesirable motion if the roll is non-zero and you pitch through +/- 90. A potential solution for this could be some form of motion hysteresis to handle moving through gimbal lock orientations, however this is not implemented as of now. Translation DoFs The translation DoFs are easily transformed using gravity vectors. By applying a quaternion based rotation matrix to the base gravity vector, \\(g_b = \\begin{pmatrix}0 & 0 & -1\\end{pmatrix}\\) , the following solution is determined for the current gravity vector, \\(g_c\\) given the vehicle's orientation quaternion, \\(q\\) . \\(\\begin{pmatrix} 2*(q.x*q.z+q.w*q.y) \\\\ 2*(q.w*q.x-q.y*q.z) \\\\ -(q.w)^2+(q.x)^2+(q.y)^2-(q.z)^2\\end{pmatrix}^T\\) The minimal rotation from \\(g_b\\) to \\(g_c\\) is then calculated. Let this rotation be called \\(q_{rot}\\) . This rotation will generally include no yaw component, unless the vehicle is upside down and facing backwards (eg pitch of 180 degrees) in which case it will contain a yaw component of 180 degrees. This is desirable as it ensures a continuous definition of what gy is even while the vehicle is flipping via pitch. This quaternion can then be applied to speeds in the gx , gy , gz basis ( x , y , and z here) to rotate them onto the vehicle basis. Thus, this \"converts\" translation speeds from GLOBAL to LOCAL mode DoFs. However, it is not ideal to transform the translation vector all at once. It is best to do it in three stages to allow proper upscaling as needed (explained below). Thus for each global mode translation vector \\(\\begin{pmatrix} 0 & y & 0 \\end{pmatrix}\\) , \\(\\begin{pmatrix} 0 & 0 & z \\end{pmatrix}\\) , and \\(\\begin{pmatrix} x & 0 & 0 \\end{pmatrix}\\) rotate it by \\(q_{rot}\\) to obtain \\(t_x\\) , \\(t_y\\) , and \\(t_z\\) respectively (note that \\(\\left\\{s, v\\right\\}\\) is a quaternion with scalar s and vector v). \\(\\left\\{0, t_x\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} x & 0 & 0 \\end{pmatrix} \\right\\} q_{rot}^*\\) \\(\\left\\{0, t_y\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} 0 & y & 0 \\end{pmatrix} \\right\\} q_{rot}^*\\) \\(\\left\\{0, t_z\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} 0 & 0 & z \\end{pmatrix} \\right\\} q_{rot}^*\\) Each of \\(t_x\\) , \\(t_y\\) , and \\(t_z\\) are speeds in LOCAL mode DoFs, however they may be slower than desired. Consider a speed of 1.0 along gy . If the vehicle were rotated 45 degrees (pitch) the resultant \\(t_y\\) would be \\(t_y = \\begin{pmatrix} 0.0 & 0.7071 & 0.7071 \\end{pmatrix}\\) . This is not as fast as possible in the correct direction. Instead \\(t_y\\) should be \\(t_y = \\begin{pmatrix} 0.0 & 1.0 & 1.0 \\end{pmatrix}\\) . In other words, the largest element of \\(t_y\\) should be the speed along gy . Thus, each of \\(t_x\\) , \\(t_y\\) , \\(t_z\\) needs to be upscaled (note that it will never need to be downscaled; it only needs to be upscaled because one DoF may now be spread between multiple). let \\(m\\) be the magnitude of the element of \\(t_x\\) with the largest magnitude Normalize \\(t_x\\) so that largest element is 1.0: \\(t_x = t_x / m\\) Scale normalized \\(t_x\\) by speed \\(x\\) (gx speed): \\(t_x = t_x * x\\) Repeat this for \\(t_y\\) and \\(t_z\\) using \\(y\\) and \\(z\\) speeds respectively. Once all three \\(t\\) vectors are scaled, they can be combined to create the net LOCAL mode translation vector, \\(l\\) \\(l = t_x + t_y + t_z\\) There are two potential issues with \\(l\\) \\(l\\) is a set of proportionally related speeds to result in the desired motion. However, the vehicle may not be capable of the same speeds in each of it's DoFs. Thus, the ratios between \\(l\\) 's elements may be incorrect. \\(l\\) is a sum of three vectors (each with elements no larger than a magnitude of 1), thus it may have elements with a magnitude greater than 1. Issue 1 should be handled first as correcting it may \"fix\" issue 2. Handling issue 2 first could result in downscaling speeds more than necessary. Handling issue 1 requires the user to provide a little more information about the vehicle: relative speeds in each DoF. These can be used to calculate downscaling factors to slow down the faster directions (note: speeding up the slower directions would result in impossible speeds, but would be handled by solving issue 2; regardless it is less ideal). These downscaling factors are calculated from \"RELDOF\" information provided by the user (see messages page of user guide). Here we will referr to the scale factors as \\(m_x\\) , \\(m_y\\) , \\(m_z\\) , \\(m_{rx}\\) , \\(m_{ry}\\) , and \\(m_{rz}\\) for the x, y, z, xrot, yrot, and zrot DoFs respectively (note that these are vehicle DoFs). Thus, the simplest option would be to let \\(l = \\begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\\end{pmatrix}\\) . However, this may downscale more than necessary. Consider the slowest direction to have a speed of 0 in l. In this case, since the slowest direction is unused, we are downscaling too much. Thus, the following algorithm is used to select the ideal downscaling factors by \"ignoring\" the downscaling required for unused DoFs (DoFs with a speed of 0). // Zero downscale factors for unused DoFs if abs(l.x) == 0 m_x = 0 endif if abs(l.y) == 0 m_y = 0 endif if abs(l.z) == 0 m_z = 0 endif // Rebalance scale factors so largest remaining is 1.0 m_max = max(m_x, max(m_y, m_z)); m_x = m_x / m_max; m_y = m_y / m_max; m_z = m_z / m_max; \\(l\\) is then downscaled as \\(l = \\begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\\end{pmatrix}\\) using the scale factors calculated using the above algorithm. Finally, issue 2 must be handled if any element of \\(l\\) still has a magnitude greater than 1.0. Thus, Let \\(m\\) be the magnitude of the element of \\(l\\) with the largest magnitude If \\(m\\) is less than or equal to 1, do not change $l If \\(m\\) is greater than 1 \\(l = l / m\\) This resultant \\(l\\) is a set of speeds that can be passed to LOCAL mode as it's x , y , and z speeds. Rotation DoFs: Converting the GLOBAL mode rotations (increase/decrease pitch, roll, yaw) to motions about DoFs is a little harder. It requires decomposing the quaternion into euler angles, then calculating three quaternions describing one euler rotation each. In other words, given the vehicle's current rotation \\(q\\) we need to find \\(q_{pitch}\\) , \\(q_{roll}\\) and \\(q_{yaw}\\) such that (based on the euler angle convention used by the control board) \\(q = q_{yaw} q_{pitch} q_{roll}\\) This can be done by converting \\(q\\) to a set of euler angles \\(e = \\begin{pmatrix}pitch & roll & yaw\\end{pmatrix}\\) then constructing the following and converting each to a quaternion \\(e_{pitch} = \\begin{pmatrix}pitch & 0 & 0\\end{pmatrix} \\rightarrow q_{pitch}\\) \\(e_{roll} = \\begin{pmatrix}0 & roll & 0\\end{pmatrix} \\rightarrow q_{roll}\\) \\(e_{yaw} = \\begin{pmatrix}0 & 0 & yaw\\end{pmatrix} \\rightarrow q_{yaw}\\) However, the euler angles obtained from \\(q\\) may not be correct for this use case. An equivalent angle go \\(e\\) (although improper) is \\(e_{alt} = \\begin{pmatrix} \\pi - pitch & roll - \\pi & yaw - \\pi \\end{pmatrix}\\) . We need to compensate for first roll then pitch. Thus, we need the euler angle with minimal roll component. This will be referred to here as \\(e_b\\) (which is either \\(e\\) or \\(_{alt}\\) ). Then, given \\(s\\) vectors describing motion to change the vehicle's pitch, roll, or yaw \\(s_{pitch} = \\begin{pmatrix}p & 0 & 0\\end{pmatrix}\\) \\(s_{roll} = \\begin{pmatrix}0 & r & 0\\end{pmatrix}\\) \\(s_{yaw} = \\begin{pmatrix}0 & 0 & h\\end{pmatrix}\\) We need to transform these onto the robot's axes as \\(w\\) vectors. For roll this is trivial as roll is about the vehicle's y axis. For pitch, this requires undoing roll first (rotate by \\(q_{roll}*\\) ) and for yaw this requires undoing roll then pitch. Thus \\(w_{roll} = s_{roll}\\) \\(\\left\\{0, w_{pitch}\\right\\} = q_{roll}^* \\left\\{0, s_{pitch}\\right\\} q_{roll}\\) \\(\\left\\{0, w_{yaw}\\right\\} = q_{pitch}^* q_{roll}^* \\left\\{0, s_{yaw}\\right\\} q_{roll} q_{pitch}\\) These vectors are angular speeds about the vehicle's x, y, and z axes. Thus, just as for translations Upscale each \\(w\\) vector using p , r , and h speeds (can skip for roll as this vector is never rotated) Sum the three \\(w\\) vectors into a net \\(w\\) vector Adjust \\(w\\) for relative DoF speeds (using \\(m_{rx}\\) , \\(m_{ry}\\) , \\(m_{rz}\\) ) Downscale \\(w\\) if needed so all elements are less than 1 Then \\(w\\) is the xrot, yrot, and zrot parts of the LOCAL mode target. Orientation Hold (OHOLD) Mode Motion Orientation hold mode uses closed-loop control for vehicle orientation in 3D space. This is achieved using three PID controllers xrot PID: Controls rotation about vehicle x-axis yrot PID: Controls rotation about vehicle y-axis zrot PID: Controls rotation about vehicle z-axis Note that in some code, these PIDs are referred to as \"SASSIST\" PIDs. However, they are used for OHOLD too! The inputs to OHOLD mode are as follows x : Translation along gx-axis (same as in GLOBAL mode) y : Translation along gy-axis (same as in GLOBAL mode) z : Translation along gz-axis (same as in GLOBAL mode) e_t : Target orientation as euler angles Optionally, a yaw speed ( h ) can be provided. In this case, PIDs will not adjust the vehicle's yaw (heading). Instead the h value works similar to GLOBAL mode (it is a rate of change of vehicle yaw). This effectively abstracts a 2D plane in which the vehicle operates. This is the same gx-gy plane described in the GLOBAL mode section. There are two variants of OHOLD mode Variant 1 (OHOLD1): Speed for yaw ( h ) is used instead of PID control Variant 2 (OHOLD2): PID is used for yaw (yaw speed / h is ignored) Variant 2 is actually then simpler variant. Variant 2 adds some additional complexity for orientation control. Translation DoFs The user provided x and y along with the z from the depth PID are handled the same way by SASSIST mode as they are in GLOBAL mode to obtain the final LOCAL mode translation vector \\(l\\) . OHOLD2 Orientation Control While translation for OHOLD is nearly identical to GLOBAL mode, rotation DoFs are very different. Orientation is controlled with a set of 3 PID controllers that work in LOCAL DoFs. Thus it is necessary to determine the rotations necessary about the vehicle's axes to achieve the desired orientation. We are given a target orientation as euler angles, \\(e_t\\) . This can be converted to a target orientation quaternion, \\(q_t\\) using the formulas derived in later sections. Note: For OHOLD1 things after this point are the same (see section below for details on how to get \\(q_t\\) for OHOLD1). The vehicle's current orientation (as a quaternion) is also available from the IMU as \\(q_c\\) . We need to calculate a quaternion \\(q_d\\) that represents the minimal rotation from \\(q_c\\) to \\(q_t\\) . However, importantly we want \\(q_d\\) to be a rotation in the vehicle's basis. Recall that right multiplication of quaternions are applied in the vehicle's basis. Thus, to describe the target orientation as the current orientation plus a rotation in the vehicle's basis \\(q_t = q_c q_d\\) Therefore \\(q_d = q_c^* q_t\\) However, this angle may not be minimal. Recall that \\(q\\) and \\(-q\\) represent the same orientation. Thus another solution to this problem would be \\(q_d = (-q_c)^* q_t\\) The minimal rotation is the one where \\(q_c\\) and \\(q_t\\) are on the same half of the unit quaternion hypersphere (meaning their dot product is not negative). Thus If \\(q_c \\cdot q_t < 0\\) then \\(q_d = (-q_c)^* q_t\\) Otherwise \\(q_d = q_c^* q_t\\) Then convert \\(q_d\\) to axis-angle representation. The following algorithm is used to do so for numeric stability reasons (the common formulas using asin are not numerically stable) let \\(q_d = \\left\\{s, v\\right\\}\\) \\(\\theta = 2 atan2(|v|, s)\\) \\(n = v / |v|\\) if \\(|v| > 0\\) else \\(n = v = 0\\) This axis ( \\(n\\) ) will be a unit vector. Thus it represents proportions of the rotation about each of the vehicle's axes. The angle ( \\(\\theta\\) ) is the magnitude of the rotation that must be taken. Thus, the product of \\(n\\) and \\(\\theta\\) is proportional to the error in angle about each of the vehicle's axes. \\(e = \\theta n\\) This error vector, \\(e\\) , contains the errors to be provided to each orientation PID (xrot, yrot, zrot). The output of these PIDs are angular velocity percentages (-1.0 to 1.0) about each of the vehicle's axes: \\(w = \\begin{pmatrix} w_x & w_y & w_z \\end{pmatrix}\\) Note: for OHOLD 1 there is an extra step here to calculate the correct w (see section below). Finally, it is necessary to downscale the \\(w\\) vectors just as in GLOBAL mode First downscale using \\(m_{rx}\\) , \\(m_{ry}\\) and \\(m_{rz}\\) using the algorithm described in the GLOBAL mode section Then downscale the vector so that all elements are less than 1. While this is never needed for OHOLD2, it is needed for OHOLD1 and it is simpler branching logic (code implementation) to just always check if downscaling is needed. This \\(w\\) can then be passed to LOCAL mode along with the LOCAL translation vector \\(l\\) obtained earlier. OHOLD1 Orientation Control OHOLD1 uses much of the same process to control the vehicle's orientation as OHOLD2, however it is necessary to decouple yaw from pitch and roll. In other words, we want to construct a target quaternion using the user provided pitch and roll, but matching the vehicle's current yaw. The most intuitive option would be to decompose \\(q_c\\) into euler angles and obtain the yaw from those. However, this does not account for the fact that the yaw can be altered by pitch or roll. For example, (p=115, r=0, h=90) would be decomposed as (p=65, r=180, h=-90). Here the heading is 180 degrees off. This would be a significant issue. Thus instead, the target quaternion can be decomposed using the swing-twist decomposition of quaternion rotations. We twist about the world z axis (since this is the axis the vehicle initially yawed about). The vehicle's yaw can be calculated from this twist quaternion (using euler conversion formula). The target quaternion is constructed from the target pitch and roll provided by the user and the yaw provided by the twist quaternion as described above. This target quaternion is \\(q_t\\) and is used the same way described for OHOLD2. After calculating \\(w\\) as described for OHOLD2, there is one extra step before handling DoF scaling. The yaw speed ( h ) provided by the user must be transformed to the vehicle's axes and added to the \\(w\\) from the PIDs. This can be done similar to GLOBAL mode \\(\\left\\{0, w_{yaw}\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix}0 & 0 & h\\end{pmatrix}\\right\\} q_{rot}^*\\) \\(w_{yaw}\\) will then be upscaled just as in GLOBAL mode. Then \\(w = w + w_{yaw}\\) . This is then downscaled as described for OHOLD2. Stability Assist (SASSIST) Mode Stability assist mode (SASSIST) is simply OHOLD mode, with the addition of closed-loop control for vehicle depth. Thus, instead of providing a z speed (for motion along the gz-axis), a PID controller is used to acheive the target depth. Inputs to this mode are the same as OHOLD mode, however instead of z , d_t is provided. d_t : Target depth (meters; negative for below the surface) The PID used to maintain vehicle depth is referred to as the Depth PID. This PID uses the current vehicle depth (provided by a depth sensor) to calculate a z speed in the gz-axis, then provides it as an input to OHOLD mode. All other inputs from SASSIST are passed through to OHOLD mode as provided. Thus, there are two variants of SASSIST mode (variant 1 and variant 2) built ontop of the corresponding variants of OHOLD mode. Depth Hold (DHOLD) Mode Motion Depth hold (DHOLD) mode is GLOBAL mode but using a PID to control the vehicle's depth. The same depth PID used for SASSIST mode is used for DHOLD mode. This mode uses the vehicle's current depth and a user provided target depth to calculate the depth PIDs error. The depth PID provides the gz-axis speed. Other speeds ( x , y , p , r , and h ) are provided by the user and mean the same thing as in GLOBAL mode. The calculated z along with user-provided speeds are passed directly to GLOBAL mode after calculating z using the depth PID. Note that this mode does not maintain vehicle orientation. In practice, this control mode is rarely useful. Sensor Processing TODO: Euler angle accumulation Other Derivations TODO: Euler / Quaternion conversion TODO: Gravity vector calculation TODO: Angle between vectors TODO: Min diff quat between two quats","title":"Math"},{"location":"firmware/math/#math","text":"","title":"Math"},{"location":"firmware/math/#coordinate-system-definition","text":"The control board uses a coordinate system that is somewhat non-standard. The coordinate system is right handed +y is forward, +z is up, +x is right Pitch is about x, roll is about y, yaw is about z While this coordinate system may seem strange to some (especially anyone who has worked with planes), the name of axes doesn't really matter.","title":"Coordinate System Definition"},{"location":"firmware/math/#6-degree-of-freedom-motion-control","text":"","title":"6 Degree of Freedom Motion Control"},{"location":"firmware/math/#nomenclature-convention-notes","text":"Matrices are assumed to be zero indexed (not 1 indexed). This means the first element of a matrix \\(M\\) is \\(m_{00}\\) not \\(m_{11}\\) . This is because the math will be implemented in C (which uses zero indexed arrays). Thruster numbers (1-8) are used by the control board's user facing components. However, this math will use thruster indices (0-7) where index = number - 1 . This is b Rotations about axis (angular velocities / DoFs) are referred to as \"xrot\" (about x), \"yrot\" (about y), and \"zrot\" (about z). The terms \"pitch\", \"roll\", and \"yaw\" are used to describe the vehicle's orientation in space. \"Pitch\" is angular velocity about the x-axis, \"roll\" is angular rotation about the y-axis, and \"yaw\" is angular velocity about the z-axis. Velocities in DoFs are normalized (meaning -1.0 to 1.0).","title":"Nomenclature & Convention Notes"},{"location":"firmware/math/#system-assumptions","text":"Vehicle is capable of motion exclusively in each of 6 degrees of freedom (DoFs). These are three translational DoFs, and three rotational DoFs. The vehicle's speed in positive and negative directions are roughly equal for each DoF. Thruster orientations are fixed. Gimbaled thruster vehicles are not supported. At most 8 thrusters (less is fine) System has 3D orientation information System has depth information System does not have translational position information","title":"System Assumptions"},{"location":"firmware/math/#example-vehicle","text":"The examples on this page will use AquaPack robotics's SeaWolf VIII robot. This robot's thruster configuration is as shown below. The arrows indicate the direction a thruster moves water (meaning the arrows are opposite the direction the thruster excerpts force on the vehicle). The arrows indicate the direction the thruster moves water when powered in the positive direction. These arrows are opposite the direction force is excerpted on the vehicle. Note that the diagram above uses thruster numbers, not indices.","title":"Example Vehicle"},{"location":"firmware/math/#dof-matrix","text":"The DoF Matrix , \\(D\\) , is constructed based on the vehicle's thruster configuration. Rows of the matrix correspond to thrusters (by index). And columns of the matrix correspond to vehicle relative DoFs. Thus, this is an 8x6 matrix. Columns correspond to DoFs in the following order (0-5): x, y, z, pitch, roll, yaw. Note that pitch, roll, and yaw are written using the single variable notation p, r, h (h = heading = yaw). \\(D = \\left(\\begin{array}{c|c|c|c|c|c} d_0 & d_1 & d_2 & d_3 & d_4 & d_5 \\end{array}\\right) = \\left(\\begin{array}{c|c|c|c|c|c} d_x & d_y & d_z & d_p & d_r & d_h \\end{array}\\right)\\) Each column of the DoF matrix, \\(d_i\\) is a set of thruster speeds that result in motion exclusively in the column's DoF. Additionally, the resultant motion should be the maximum possible speed, and in the positive direction. Each \\(d_i\\) is an 8 element column vector, with elements corresponding to thrusters (by index). All speeds should be normalized (between -1.0 and 1.0) For the example vehicle shown above, the following is the DoF matrix \\(D = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix}\\) Consider the first column: \\(d_0 = d_x\\) . This column's thruster speeds should result in the vehicle moving as fast as possible in the +x direction (only). This is achieved by setting T2, T4 (index 1, 3) to the positive direction and T1, T3 (index 0, 2) to the negative direction at full speed (recall that the arrows are opposite the direction the thruster excerpts force on the vehicle). Thus \\(d_x = \\begin{pmatrix}-1 & +1 & -1 & +1 & 0 & 0 & 0 & 0\\end{pmatrix}^T\\) Note that when constructing the DoF matrix for your vehicle, you should assume an ideal system and environment.","title":"DoF Matrix"},{"location":"firmware/math/#local-mode-motion","text":"In LOCAL mode, motion is specified as a set of speeds in vehicle relative DoFs. The user provides the control board with a local target motion vector ( \\(t_l\\) ) where each element corresponds to a DoF. \\(t_l = \\begin{pmatrix} x & y & z & r_x & r_y & r_z \\end{pmatrix}^T\\) \\(x\\) is normalized velocity in +x direction \\(y\\) is normalized velocity in +y direction \\(z\\) is normalized velocity in +y direction \\(r_x\\) is normalized velocity in +pitch direction (xrot) \\(r_y\\) is normalized velocity in +roll direction (yrot) \\(r_z\\) is normalized velocity in +yaw direction (zrot) By multiplying this target motion by the DoF matrix, \\(D\\) , a speed vector \\(s\\) is obtained where each element of \\(s\\) corresponds to a specific thruster (by index). \\(s = D t_l\\) Consider the example where \\(t_l = \\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 0\\end{pmatrix}^T\\) . This should cause the vehicle to move at full possible speed forward (relative to the vehicle's orientation). \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix} = \\begin{pmatrix}-1 \\\\ -1 \\\\ +1 \\\\ +1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix}\\) In the above example, it is trivial to see that this is the desired motion. However for a more complex example, a problem appears. Consider \\(t_l = \\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 1\\end{pmatrix}^T\\) . This describes the vehicle both moving forward and yawing at full possible speed. \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 1\\end{pmatrix} = \\begin{pmatrix}0 \\\\ -2 \\\\ 0 \\\\ +2 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix}\\) Notice that the resultant speed vector has motors moving in excess of 100% speed (elements with magnitude greater than 1.0). This is not possible. While a simple solution may seem to be dividing all elements of the vector by the one with the largest magnitude. This results in a scaled speed vector \\(\\hat{s}\\) \\(\\hat{s} = s \\div \\text{absmax}(s)\\) this will not work well in all cases. Consider \\(t_l = \\begin{pmatrix}0 & 1 & 1 & 1 & 1 & 1\\end{pmatrix}^T\\) . \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 1 \\\\ 1 \\\\ 1 \\\\ 1\\end{pmatrix} = \\begin{pmatrix}0 \\\\ -2 \\\\ 0 \\\\ +2 \\\\ -3 \\\\ -1 \\\\ -1 \\\\ +1\\end{pmatrix}\\) and \\(\\hat{s} = s \\div \\text{absmax}(s) = s \\div 3 = \\begin{pmatrix}0 \\\\ -0.67 \\\\ 0 \\\\ +0.67 \\\\ -1 \\\\ -0.33 \\\\ -0.33 \\\\ +0.33\\end{pmatrix}\\) While this has resulted in an possible set of thruster speeds, these are not optimal. Look at the example vehicle diagram. Notice that thrusters 1-4 and 5-8 control different motions. In the previous example, thrusters 1-4 were slowed down more than necessary, because thruster 5 was too large of a value. This is not ideal as the vehicle's maximum speed becomes artificially limited. Instead, the following \\(\\hat{s}\\) is ideal. This is scaling down the thrusters within each group (1-4 and 5-8) separately. \\(\\hat{s} = \\begin{pmatrix}0 \\\\ -1 \\\\ 0 \\\\ +1 \\\\ -1 \\\\ -0.33 \\\\ -0.33 \\\\ +0.33\\end{pmatrix}\\) Groupings of thrusters on the example vehicle are easy to observe, however this is not always true. Thus, achieving optimal scaling for any system (any DoF matrix) requires a more sophisticated method to determine groupings and scale speeds. Thruster groupings are determined by \"overlap\" between thrusters. Two thrusters, \\(i\\) and \\(j\\) are said to overlap if they have a non-zero entry in the same column of the DoF matrix ( \\(D\\) ) for at least one column. This is easier to calculate using a contribution matrix , \\(C\\) , defined as \\(D \\neq 0\\) . This results in a binary form of the DoF matrix. For the above example \\(C = \\left[\\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\neq 0 \\right] = \\begin{pmatrix} 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ \\end{pmatrix}\\) Then for each thruster \\(i\\) an overlap vector \\(o_i\\) can be constructed as follows \\(o_i = C (c^i)^T\\) where \\(c^i\\) is the \\(i\\) th row of \\(C\\) . Thus, \\(o_i\\) is an 8 element vector where each element corresponds to a thruster (by index). Element \\(j\\) of \\(o_i\\) can either be \\(1\\) or a \\(0\\) . \\(1\\) indicates that thrusters \\(i\\) and \\(j\\) overlap. For example, \\(o_0 = C (c^0)^T = \\begin{pmatrix} 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 \\end{pmatrix}^T\\) This shows that thruster index 0 (T1) overlaps with indices 0, 1, 2, and 3 (T1, T2, T3, T4). By calculating and storing these overlap vectors for each thruster ( \\(\\left\\{o_i\\right\\}_{i=0}^7\\) ), this effectively forms a lookup table to determine thruster overlap. While this is not the most memory efficient option, it reduces computation time, which is important since this will run very frequently on a microcontroller. Using overlap vectors, the following algorithm can be used to scale motor speeds: Find the thruster ( i ) with the largest magnitude speed Iterate over thruster i 's overlap vector For any thruster i overlaps with ( j ), divide thruster j 's speed by the magnitude of thruster i 's speed Repeat until the largest magnitude does not exceed 1.0 while true // m is value, i is index m, i = max(abs(speed_vector)) if m <= 1.0 // Done scaling break endif // Iterate over all thrusters (0-7 inclusive) for j=0...7 if overlap_vector[i][j] == 1 // i and j overlap. Divide j's speed by m. speed_vector[j] /= m endif endfor endwhile This algorithm results in optimal speed scaling by only reducing the speed of thrusters that share DoF contributions.","title":"LOCAL Mode Motion"},{"location":"firmware/math/#global-mode-motion","text":"GLOBAL mode is very similar to LOCAL mode, however, motion is described partially relative to the world instead of the robot. Specifically, motion of the vehicle is compensated for vehicle pitch and roll (but not yaw). This results in a coordinate system defined by the axes gx , gy , and gz . Note that the world coordinate system is defined as wx , wy , wz . Notably, if the vehicle is pitched 180 degrees, the gx-gy plane becomes aligned to the back of the vehicle ensuring consistent motion. This is shown in the animation below. In GLOBAL mode, the user provides the control board with a global target motion vector , \\(t_g\\) with 6 elements. This target motion vector is a concatenation of two 3 dimensional vectors. The first, a set of translations along gx , gy , and gz . Second a set of rotations to affect vehicle pitch, roll, and yaw. These are referred to by the following names x : Speed in gx direction (translation) y : Speed in gy direction (translation) z : Speed in gz direction (translation) p : Speed at which the vehicle's pitch should increase (negative for decrease pitch). Aka \"pitch_spd\" r : Speed at which the vehicle's roll should increase (negative for decrease pitch). Aka \"roll_spd\" h : Speed at which the vehicle's yaw should increase (negative for decrease pitch). Aka \"yaw_spd\" \\(t_g = \\begin{pmatrix} x & y & z & p & r & h \\end{pmatrix}\\) It is necessary to transform each DoF's motion into motions in the vehicle's DoFs. These speeds can then be passed to LOCAL mode. WARNING: GLOBAL mode is impacted by gimbal lock issues with euler angles. This occurs when the vehicle's pitch is +/- 90 degrees. In this scenario the meaning of \"increase / decrease pitch\" is ambiguous. The vehicle will take the zero-roll route in this scenario. Thus, GLOBAL mode may produce undesirable motion if the roll is non-zero and you pitch through +/- 90. A potential solution for this could be some form of motion hysteresis to handle moving through gimbal lock orientations, however this is not implemented as of now.","title":"GLOBAL Mode Motion"},{"location":"firmware/math/#translation-dofs","text":"The translation DoFs are easily transformed using gravity vectors. By applying a quaternion based rotation matrix to the base gravity vector, \\(g_b = \\begin{pmatrix}0 & 0 & -1\\end{pmatrix}\\) , the following solution is determined for the current gravity vector, \\(g_c\\) given the vehicle's orientation quaternion, \\(q\\) . \\(\\begin{pmatrix} 2*(q.x*q.z+q.w*q.y) \\\\ 2*(q.w*q.x-q.y*q.z) \\\\ -(q.w)^2+(q.x)^2+(q.y)^2-(q.z)^2\\end{pmatrix}^T\\) The minimal rotation from \\(g_b\\) to \\(g_c\\) is then calculated. Let this rotation be called \\(q_{rot}\\) . This rotation will generally include no yaw component, unless the vehicle is upside down and facing backwards (eg pitch of 180 degrees) in which case it will contain a yaw component of 180 degrees. This is desirable as it ensures a continuous definition of what gy is even while the vehicle is flipping via pitch. This quaternion can then be applied to speeds in the gx , gy , gz basis ( x , y , and z here) to rotate them onto the vehicle basis. Thus, this \"converts\" translation speeds from GLOBAL to LOCAL mode DoFs. However, it is not ideal to transform the translation vector all at once. It is best to do it in three stages to allow proper upscaling as needed (explained below). Thus for each global mode translation vector \\(\\begin{pmatrix} 0 & y & 0 \\end{pmatrix}\\) , \\(\\begin{pmatrix} 0 & 0 & z \\end{pmatrix}\\) , and \\(\\begin{pmatrix} x & 0 & 0 \\end{pmatrix}\\) rotate it by \\(q_{rot}\\) to obtain \\(t_x\\) , \\(t_y\\) , and \\(t_z\\) respectively (note that \\(\\left\\{s, v\\right\\}\\) is a quaternion with scalar s and vector v). \\(\\left\\{0, t_x\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} x & 0 & 0 \\end{pmatrix} \\right\\} q_{rot}^*\\) \\(\\left\\{0, t_y\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} 0 & y & 0 \\end{pmatrix} \\right\\} q_{rot}^*\\) \\(\\left\\{0, t_z\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} 0 & 0 & z \\end{pmatrix} \\right\\} q_{rot}^*\\) Each of \\(t_x\\) , \\(t_y\\) , and \\(t_z\\) are speeds in LOCAL mode DoFs, however they may be slower than desired. Consider a speed of 1.0 along gy . If the vehicle were rotated 45 degrees (pitch) the resultant \\(t_y\\) would be \\(t_y = \\begin{pmatrix} 0.0 & 0.7071 & 0.7071 \\end{pmatrix}\\) . This is not as fast as possible in the correct direction. Instead \\(t_y\\) should be \\(t_y = \\begin{pmatrix} 0.0 & 1.0 & 1.0 \\end{pmatrix}\\) . In other words, the largest element of \\(t_y\\) should be the speed along gy . Thus, each of \\(t_x\\) , \\(t_y\\) , \\(t_z\\) needs to be upscaled (note that it will never need to be downscaled; it only needs to be upscaled because one DoF may now be spread between multiple). let \\(m\\) be the magnitude of the element of \\(t_x\\) with the largest magnitude Normalize \\(t_x\\) so that largest element is 1.0: \\(t_x = t_x / m\\) Scale normalized \\(t_x\\) by speed \\(x\\) (gx speed): \\(t_x = t_x * x\\) Repeat this for \\(t_y\\) and \\(t_z\\) using \\(y\\) and \\(z\\) speeds respectively. Once all three \\(t\\) vectors are scaled, they can be combined to create the net LOCAL mode translation vector, \\(l\\) \\(l = t_x + t_y + t_z\\) There are two potential issues with \\(l\\) \\(l\\) is a set of proportionally related speeds to result in the desired motion. However, the vehicle may not be capable of the same speeds in each of it's DoFs. Thus, the ratios between \\(l\\) 's elements may be incorrect. \\(l\\) is a sum of three vectors (each with elements no larger than a magnitude of 1), thus it may have elements with a magnitude greater than 1. Issue 1 should be handled first as correcting it may \"fix\" issue 2. Handling issue 2 first could result in downscaling speeds more than necessary. Handling issue 1 requires the user to provide a little more information about the vehicle: relative speeds in each DoF. These can be used to calculate downscaling factors to slow down the faster directions (note: speeding up the slower directions would result in impossible speeds, but would be handled by solving issue 2; regardless it is less ideal). These downscaling factors are calculated from \"RELDOF\" information provided by the user (see messages page of user guide). Here we will referr to the scale factors as \\(m_x\\) , \\(m_y\\) , \\(m_z\\) , \\(m_{rx}\\) , \\(m_{ry}\\) , and \\(m_{rz}\\) for the x, y, z, xrot, yrot, and zrot DoFs respectively (note that these are vehicle DoFs). Thus, the simplest option would be to let \\(l = \\begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\\end{pmatrix}\\) . However, this may downscale more than necessary. Consider the slowest direction to have a speed of 0 in l. In this case, since the slowest direction is unused, we are downscaling too much. Thus, the following algorithm is used to select the ideal downscaling factors by \"ignoring\" the downscaling required for unused DoFs (DoFs with a speed of 0). // Zero downscale factors for unused DoFs if abs(l.x) == 0 m_x = 0 endif if abs(l.y) == 0 m_y = 0 endif if abs(l.z) == 0 m_z = 0 endif // Rebalance scale factors so largest remaining is 1.0 m_max = max(m_x, max(m_y, m_z)); m_x = m_x / m_max; m_y = m_y / m_max; m_z = m_z / m_max; \\(l\\) is then downscaled as \\(l = \\begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\\end{pmatrix}\\) using the scale factors calculated using the above algorithm. Finally, issue 2 must be handled if any element of \\(l\\) still has a magnitude greater than 1.0. Thus, Let \\(m\\) be the magnitude of the element of \\(l\\) with the largest magnitude If \\(m\\) is less than or equal to 1, do not change $l If \\(m\\) is greater than 1 \\(l = l / m\\) This resultant \\(l\\) is a set of speeds that can be passed to LOCAL mode as it's x , y , and z speeds.","title":"Translation DoFs"},{"location":"firmware/math/#rotation-dofs","text":"Converting the GLOBAL mode rotations (increase/decrease pitch, roll, yaw) to motions about DoFs is a little harder. It requires decomposing the quaternion into euler angles, then calculating three quaternions describing one euler rotation each. In other words, given the vehicle's current rotation \\(q\\) we need to find \\(q_{pitch}\\) , \\(q_{roll}\\) and \\(q_{yaw}\\) such that (based on the euler angle convention used by the control board) \\(q = q_{yaw} q_{pitch} q_{roll}\\) This can be done by converting \\(q\\) to a set of euler angles \\(e = \\begin{pmatrix}pitch & roll & yaw\\end{pmatrix}\\) then constructing the following and converting each to a quaternion \\(e_{pitch} = \\begin{pmatrix}pitch & 0 & 0\\end{pmatrix} \\rightarrow q_{pitch}\\) \\(e_{roll} = \\begin{pmatrix}0 & roll & 0\\end{pmatrix} \\rightarrow q_{roll}\\) \\(e_{yaw} = \\begin{pmatrix}0 & 0 & yaw\\end{pmatrix} \\rightarrow q_{yaw}\\) However, the euler angles obtained from \\(q\\) may not be correct for this use case. An equivalent angle go \\(e\\) (although improper) is \\(e_{alt} = \\begin{pmatrix} \\pi - pitch & roll - \\pi & yaw - \\pi \\end{pmatrix}\\) . We need to compensate for first roll then pitch. Thus, we need the euler angle with minimal roll component. This will be referred to here as \\(e_b\\) (which is either \\(e\\) or \\(_{alt}\\) ). Then, given \\(s\\) vectors describing motion to change the vehicle's pitch, roll, or yaw \\(s_{pitch} = \\begin{pmatrix}p & 0 & 0\\end{pmatrix}\\) \\(s_{roll} = \\begin{pmatrix}0 & r & 0\\end{pmatrix}\\) \\(s_{yaw} = \\begin{pmatrix}0 & 0 & h\\end{pmatrix}\\) We need to transform these onto the robot's axes as \\(w\\) vectors. For roll this is trivial as roll is about the vehicle's y axis. For pitch, this requires undoing roll first (rotate by \\(q_{roll}*\\) ) and for yaw this requires undoing roll then pitch. Thus \\(w_{roll} = s_{roll}\\) \\(\\left\\{0, w_{pitch}\\right\\} = q_{roll}^* \\left\\{0, s_{pitch}\\right\\} q_{roll}\\) \\(\\left\\{0, w_{yaw}\\right\\} = q_{pitch}^* q_{roll}^* \\left\\{0, s_{yaw}\\right\\} q_{roll} q_{pitch}\\) These vectors are angular speeds about the vehicle's x, y, and z axes. Thus, just as for translations Upscale each \\(w\\) vector using p , r , and h speeds (can skip for roll as this vector is never rotated) Sum the three \\(w\\) vectors into a net \\(w\\) vector Adjust \\(w\\) for relative DoF speeds (using \\(m_{rx}\\) , \\(m_{ry}\\) , \\(m_{rz}\\) ) Downscale \\(w\\) if needed so all elements are less than 1 Then \\(w\\) is the xrot, yrot, and zrot parts of the LOCAL mode target.","title":"Rotation DoFs:"},{"location":"firmware/math/#orientation-hold-ohold-mode-motion","text":"Orientation hold mode uses closed-loop control for vehicle orientation in 3D space. This is achieved using three PID controllers xrot PID: Controls rotation about vehicle x-axis yrot PID: Controls rotation about vehicle y-axis zrot PID: Controls rotation about vehicle z-axis Note that in some code, these PIDs are referred to as \"SASSIST\" PIDs. However, they are used for OHOLD too! The inputs to OHOLD mode are as follows x : Translation along gx-axis (same as in GLOBAL mode) y : Translation along gy-axis (same as in GLOBAL mode) z : Translation along gz-axis (same as in GLOBAL mode) e_t : Target orientation as euler angles Optionally, a yaw speed ( h ) can be provided. In this case, PIDs will not adjust the vehicle's yaw (heading). Instead the h value works similar to GLOBAL mode (it is a rate of change of vehicle yaw). This effectively abstracts a 2D plane in which the vehicle operates. This is the same gx-gy plane described in the GLOBAL mode section. There are two variants of OHOLD mode Variant 1 (OHOLD1): Speed for yaw ( h ) is used instead of PID control Variant 2 (OHOLD2): PID is used for yaw (yaw speed / h is ignored) Variant 2 is actually then simpler variant. Variant 2 adds some additional complexity for orientation control.","title":"Orientation Hold (OHOLD) Mode Motion"},{"location":"firmware/math/#translation-dofs_1","text":"The user provided x and y along with the z from the depth PID are handled the same way by SASSIST mode as they are in GLOBAL mode to obtain the final LOCAL mode translation vector \\(l\\) .","title":"Translation DoFs"},{"location":"firmware/math/#ohold2-orientation-control","text":"While translation for OHOLD is nearly identical to GLOBAL mode, rotation DoFs are very different. Orientation is controlled with a set of 3 PID controllers that work in LOCAL DoFs. Thus it is necessary to determine the rotations necessary about the vehicle's axes to achieve the desired orientation. We are given a target orientation as euler angles, \\(e_t\\) . This can be converted to a target orientation quaternion, \\(q_t\\) using the formulas derived in later sections. Note: For OHOLD1 things after this point are the same (see section below for details on how to get \\(q_t\\) for OHOLD1). The vehicle's current orientation (as a quaternion) is also available from the IMU as \\(q_c\\) . We need to calculate a quaternion \\(q_d\\) that represents the minimal rotation from \\(q_c\\) to \\(q_t\\) . However, importantly we want \\(q_d\\) to be a rotation in the vehicle's basis. Recall that right multiplication of quaternions are applied in the vehicle's basis. Thus, to describe the target orientation as the current orientation plus a rotation in the vehicle's basis \\(q_t = q_c q_d\\) Therefore \\(q_d = q_c^* q_t\\) However, this angle may not be minimal. Recall that \\(q\\) and \\(-q\\) represent the same orientation. Thus another solution to this problem would be \\(q_d = (-q_c)^* q_t\\) The minimal rotation is the one where \\(q_c\\) and \\(q_t\\) are on the same half of the unit quaternion hypersphere (meaning their dot product is not negative). Thus If \\(q_c \\cdot q_t < 0\\) then \\(q_d = (-q_c)^* q_t\\) Otherwise \\(q_d = q_c^* q_t\\) Then convert \\(q_d\\) to axis-angle representation. The following algorithm is used to do so for numeric stability reasons (the common formulas using asin are not numerically stable) let \\(q_d = \\left\\{s, v\\right\\}\\) \\(\\theta = 2 atan2(|v|, s)\\) \\(n = v / |v|\\) if \\(|v| > 0\\) else \\(n = v = 0\\) This axis ( \\(n\\) ) will be a unit vector. Thus it represents proportions of the rotation about each of the vehicle's axes. The angle ( \\(\\theta\\) ) is the magnitude of the rotation that must be taken. Thus, the product of \\(n\\) and \\(\\theta\\) is proportional to the error in angle about each of the vehicle's axes. \\(e = \\theta n\\) This error vector, \\(e\\) , contains the errors to be provided to each orientation PID (xrot, yrot, zrot). The output of these PIDs are angular velocity percentages (-1.0 to 1.0) about each of the vehicle's axes: \\(w = \\begin{pmatrix} w_x & w_y & w_z \\end{pmatrix}\\) Note: for OHOLD 1 there is an extra step here to calculate the correct w (see section below). Finally, it is necessary to downscale the \\(w\\) vectors just as in GLOBAL mode First downscale using \\(m_{rx}\\) , \\(m_{ry}\\) and \\(m_{rz}\\) using the algorithm described in the GLOBAL mode section Then downscale the vector so that all elements are less than 1. While this is never needed for OHOLD2, it is needed for OHOLD1 and it is simpler branching logic (code implementation) to just always check if downscaling is needed. This \\(w\\) can then be passed to LOCAL mode along with the LOCAL translation vector \\(l\\) obtained earlier.","title":"OHOLD2 Orientation Control"},{"location":"firmware/math/#ohold1-orientation-control","text":"OHOLD1 uses much of the same process to control the vehicle's orientation as OHOLD2, however it is necessary to decouple yaw from pitch and roll. In other words, we want to construct a target quaternion using the user provided pitch and roll, but matching the vehicle's current yaw. The most intuitive option would be to decompose \\(q_c\\) into euler angles and obtain the yaw from those. However, this does not account for the fact that the yaw can be altered by pitch or roll. For example, (p=115, r=0, h=90) would be decomposed as (p=65, r=180, h=-90). Here the heading is 180 degrees off. This would be a significant issue. Thus instead, the target quaternion can be decomposed using the swing-twist decomposition of quaternion rotations. We twist about the world z axis (since this is the axis the vehicle initially yawed about). The vehicle's yaw can be calculated from this twist quaternion (using euler conversion formula). The target quaternion is constructed from the target pitch and roll provided by the user and the yaw provided by the twist quaternion as described above. This target quaternion is \\(q_t\\) and is used the same way described for OHOLD2. After calculating \\(w\\) as described for OHOLD2, there is one extra step before handling DoF scaling. The yaw speed ( h ) provided by the user must be transformed to the vehicle's axes and added to the \\(w\\) from the PIDs. This can be done similar to GLOBAL mode \\(\\left\\{0, w_{yaw}\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix}0 & 0 & h\\end{pmatrix}\\right\\} q_{rot}^*\\) \\(w_{yaw}\\) will then be upscaled just as in GLOBAL mode. Then \\(w = w + w_{yaw}\\) . This is then downscaled as described for OHOLD2.","title":"OHOLD1 Orientation Control"},{"location":"firmware/math/#stability-assist-sassist-mode","text":"Stability assist mode (SASSIST) is simply OHOLD mode, with the addition of closed-loop control for vehicle depth. Thus, instead of providing a z speed (for motion along the gz-axis), a PID controller is used to acheive the target depth. Inputs to this mode are the same as OHOLD mode, however instead of z , d_t is provided. d_t : Target depth (meters; negative for below the surface) The PID used to maintain vehicle depth is referred to as the Depth PID. This PID uses the current vehicle depth (provided by a depth sensor) to calculate a z speed in the gz-axis, then provides it as an input to OHOLD mode. All other inputs from SASSIST are passed through to OHOLD mode as provided. Thus, there are two variants of SASSIST mode (variant 1 and variant 2) built ontop of the corresponding variants of OHOLD mode.","title":"Stability Assist (SASSIST) Mode"},{"location":"firmware/math/#depth-hold-dhold-mode-motion","text":"Depth hold (DHOLD) mode is GLOBAL mode but using a PID to control the vehicle's depth. The same depth PID used for SASSIST mode is used for DHOLD mode. This mode uses the vehicle's current depth and a user provided target depth to calculate the depth PIDs error. The depth PID provides the gz-axis speed. Other speeds ( x , y , p , r , and h ) are provided by the user and mean the same thing as in GLOBAL mode. The calculated z along with user-provided speeds are passed directly to GLOBAL mode after calculating z using the depth PID. Note that this mode does not maintain vehicle orientation. In practice, this control mode is rarely useful.","title":"Depth Hold (DHOLD) Mode Motion"},{"location":"firmware/math/#sensor-processing","text":"TODO: Euler angle accumulation","title":"Sensor Processing"},{"location":"firmware/math/#other-derivations","text":"TODO: Euler / Quaternion conversion TODO: Gravity vector calculation TODO: Angle between vectors TODO: Min diff quat between two quats","title":"Other Derivations"},{"location":"firmware/overview/","text":"Overview TODO: Project Structure and Build System TODO: Generator projects TODO: System architecture","title":"Overview"},{"location":"firmware/overview/#overview","text":"TODO: Project Structure and Build System TODO: Generator projects TODO: System architecture","title":"Overview"},{"location":"firmware/simhijack/","text":"Simulation Support TODO","title":"Simulation Support"},{"location":"firmware/simhijack/#simulation-support","text":"TODO","title":"Simulation Support"},{"location":"hardware/sensors/","text":"Off-Board Sensors The control board has an integrated IMU (gyro + accel) and is not designed to support off-board IMUs. However, for full functionality, vehicle depth data is also required. This data is provided by an off-board depth sensor. Currently, only one such sensor is supported: MS5837-30BA pressure sensor based depth sensors. The sensor used during development and tested with the control board is BlueRobotics' Bar30 Sensor . The depth sensor is to be connected to the control board via the \"Depth I2C\" header. See the pinout images on the v1 or v2 hardware pages for more details. If using the BlueRobotics sensor, you will need to cut off the connector it comes with and install \"DuPont\" connectors.","title":"Off-Board Sensors"},{"location":"hardware/sensors/#off-board-sensors","text":"The control board has an integrated IMU (gyro + accel) and is not designed to support off-board IMUs. However, for full functionality, vehicle depth data is also required. This data is provided by an off-board depth sensor. Currently, only one such sensor is supported: MS5837-30BA pressure sensor based depth sensors. The sensor used during development and tested with the control board is BlueRobotics' Bar30 Sensor . The depth sensor is to be connected to the control board via the \"Depth I2C\" header. See the pinout images on the v1 or v2 hardware pages for more details. If using the BlueRobotics sensor, you will need to cut off the connector it comes with and install \"DuPont\" connectors.","title":"Off-Board Sensors"},{"location":"hardware/v1/","text":"AUV Control Board v1 Note: Control Board v1 Units must update Adafruit's bootloader before first use. This only needs to be done once, but it must be done. Follow Adafruit's instructions here . DO NOT SKIP THIS!!! Note that some pictures show a version with a pressure / temperature sensor. This is no longer used and should not be included when building the board. The default coordinate system as defined by the IMU is shown below. Note that this is a right hand coordinate system. The red arrows define axes. Rotation about these axes is in the right hand direction (indicated by green arrows). Notice that the green arrows are on top of the red axis arrows, thus a \"left to right\" arrow is left to right across the top of the axis. The axis configuration can be changed to match any plane-aligned mounting position of the control board on a robot. See the BNO055 datasheet for more information. Components 1x Adafruit ItsyBitsy M4 1x Adafruit BNO055 Breakout STEMMA QT version can be substituted, however be aware that the pin order is different. Protoboard (2.54mm spacing; 24 by 18 holes; 5cm by 7cm) These can be found from many vendors Solid Core Wire (22AWG) Female Pin Headers (2.54mm pitch) Two 1x14 headers One 1x6 header One 1x4 header Note: These can be cut between pins carefully Male pin headers (2.54mm pitch) Two 1x8 headers One 1x4 header Two 1x2 headers (optional; for debug interface) Note: These can easily be cut / broken between pins. Two 10K resistors (through hole, 1/4 W) Wiring Diagram Assembly Instructions Solder header strips in the positions shown below. The female headers avoid soldering breakouts / dev boards directly to protoboard which allows easily replacing components if needed (or reusing them for other purposes later). After soldering, breakouts can be populated to make identifying pins easier. The crossed out header can be omitted as it was formerly used for the pressure / temp sensor (not used anymore). Wire the protoboard according to the wiring diagram. Note that the first image shows the pressure / temperature sensor (this should be omitted). The first photo also does not show the pullup resistors or debug headers. The second picture is closer to what a fully assembled Control Board v1 should look like. Hot glue can be used to ensure the resistor leads do not short anything. This photo is an older assembly that includes the temp / pressure sensor (should be omitted) and excludes the pullup resistors and debug headers (should be included). This photo shows the pullup resistors and debug headers Back side wiring","title":"Version 1"},{"location":"hardware/v1/#auv-control-board-v1","text":"Note: Control Board v1 Units must update Adafruit's bootloader before first use. This only needs to be done once, but it must be done. Follow Adafruit's instructions here . DO NOT SKIP THIS!!! Note that some pictures show a version with a pressure / temperature sensor. This is no longer used and should not be included when building the board. The default coordinate system as defined by the IMU is shown below. Note that this is a right hand coordinate system. The red arrows define axes. Rotation about these axes is in the right hand direction (indicated by green arrows). Notice that the green arrows are on top of the red axis arrows, thus a \"left to right\" arrow is left to right across the top of the axis. The axis configuration can be changed to match any plane-aligned mounting position of the control board on a robot. See the BNO055 datasheet for more information.","title":"AUV Control Board v1"},{"location":"hardware/v1/#components","text":"1x Adafruit ItsyBitsy M4 1x Adafruit BNO055 Breakout STEMMA QT version can be substituted, however be aware that the pin order is different. Protoboard (2.54mm spacing; 24 by 18 holes; 5cm by 7cm) These can be found from many vendors Solid Core Wire (22AWG) Female Pin Headers (2.54mm pitch) Two 1x14 headers One 1x6 header One 1x4 header Note: These can be cut between pins carefully Male pin headers (2.54mm pitch) Two 1x8 headers One 1x4 header Two 1x2 headers (optional; for debug interface) Note: These can easily be cut / broken between pins. Two 10K resistors (through hole, 1/4 W)","title":"Components"},{"location":"hardware/v1/#wiring-diagram","text":"","title":"Wiring Diagram"},{"location":"hardware/v1/#assembly-instructions","text":"Solder header strips in the positions shown below. The female headers avoid soldering breakouts / dev boards directly to protoboard which allows easily replacing components if needed (or reusing them for other purposes later). After soldering, breakouts can be populated to make identifying pins easier. The crossed out header can be omitted as it was formerly used for the pressure / temp sensor (not used anymore). Wire the protoboard according to the wiring diagram. Note that the first image shows the pressure / temperature sensor (this should be omitted). The first photo also does not show the pullup resistors or debug headers. The second picture is closer to what a fully assembled Control Board v1 should look like. Hot glue can be used to ensure the resistor leads do not short anything. This photo is an older assembly that includes the temp / pressure sensor (should be omitted) and excludes the pullup resistors and debug headers (should be included). This photo shows the pullup resistors and debug headers Back side wiring","title":"Assembly Instructions"},{"location":"hardware/v2/","text":"AUV Control Board v2 The default coordinate system as defined by the IMU is shown below. Note that this is a right hand coordinate system. The red arrows define axes. Rotation about these axes is in the right hand direction (indicated by green arrows). Notice that the green arrows are on top of the red axis arrows, thus a \"left to right\" arrow is left to right across the top of the axis. The axis configuration can be changed to match any plane-aligned mounting position of the control board on a robot. See the BNO055 datasheet for more information. Components 1x WeAct Studio Black Pill (w / STM32F411 notSTM32F401 ) Note: There are many counterfeit boards sold. These should be avoided. Adafruit Store Aliexpress (Offical WeAct Studio Store) 1x Adafruit BNO055 Breakout STEMMA QT version can be substituted, however be aware that the pin order is different. Protoboard (2.54mm spacing; 24 by 18 holes; 5cm by 7cm) These can be found from many vendors Solid Core Wire (22AWG) Female Pin Headers (2.54mm pitch) Two 1x20 headers One 1x6 header One 1x4 header Note: These can be cut between pins carefully Male pin headers (2.54mm pitch) Two 1x8 headers One 1x4 header Note: These can easily be cut / broken between pins. Resistors (through hole, 1/4 W) Two 10K resistors Two 10 Ohm resistors One 100 Ohm resistor One QBL8RGB60D0-2897 RGB LED Wiring Diagram Assembly Instructions Solder header strips in the positions shown below. The female headers avoid soldering breakouts / dev boards directly to protoboard which allows easily replacing components if needed (or reusing them for other purposes later). After soldering, breakouts can be populated to make identifying pins easier. The location of the LED is also indicated here. Wire the protoboard according to the wiring diagram.","title":"Version 2"},{"location":"hardware/v2/#auv-control-board-v2","text":"The default coordinate system as defined by the IMU is shown below. Note that this is a right hand coordinate system. The red arrows define axes. Rotation about these axes is in the right hand direction (indicated by green arrows). Notice that the green arrows are on top of the red axis arrows, thus a \"left to right\" arrow is left to right across the top of the axis. The axis configuration can be changed to match any plane-aligned mounting position of the control board on a robot. See the BNO055 datasheet for more information.","title":"AUV Control Board v2"},{"location":"hardware/v2/#components","text":"1x WeAct Studio Black Pill (w / STM32F411 notSTM32F401 ) Note: There are many counterfeit boards sold. These should be avoided. Adafruit Store Aliexpress (Offical WeAct Studio Store) 1x Adafruit BNO055 Breakout STEMMA QT version can be substituted, however be aware that the pin order is different. Protoboard (2.54mm spacing; 24 by 18 holes; 5cm by 7cm) These can be found from many vendors Solid Core Wire (22AWG) Female Pin Headers (2.54mm pitch) Two 1x20 headers One 1x6 header One 1x4 header Note: These can be cut between pins carefully Male pin headers (2.54mm pitch) Two 1x8 headers One 1x4 header Note: These can easily be cut / broken between pins. Resistors (through hole, 1/4 W) Two 10K resistors Two 10 Ohm resistors One 100 Ohm resistor One QBL8RGB60D0-2897 RGB LED","title":"Components"},{"location":"hardware/v2/#wiring-diagram","text":"","title":"Wiring Diagram"},{"location":"hardware/v2/#assembly-instructions","text":"Solder header strips in the positions shown below. The female headers avoid soldering breakouts / dev boards directly to protoboard which allows easily replacing components if needed (or reusing them for other purposes later). After soldering, breakouts can be populated to make identifying pins easier. The location of the LED is also indicated here. Wire the protoboard according to the wiring diagram.","title":"Assembly Instructions"},{"location":"user_guide/calibration/","text":"Sensor Calibration BNO055 (IMU) Calibration Calibration Script It is highly recommended to use the calibration script to calibrate the BNO055. Even if you are not using the python interface to the control board, the calibration script can be used to manage the calibration state. Launch the script by using the following command (note that python3 along with the pyserial library must be installed). Run this command in the iface folder. Replace [PORT] with the serial port of the control board. python3 launch.py example/bno055_calibrate.py -p [PORT] If a calibration is currently saved to the control board, the script will show that configuration and prompt you to erase it. You must erase any stored configuration before you can recalibrate the BNO055. Refusing to erase the calibration will result in the script exiting with no further action taken (this can be used as a way to view the currently saved calibration). If the above prompt is not shown, there is no calibration currently saved on the control board. Next, the script will prompt you to run either \"guided\" or \"manual\" calibration. Guided calibration provides instructions to use the BNO055's automatic calibration routine. This is recommended. Manual calibration allows a user to manually enter values for each calibration constant. Select guided mode. Next, instructions on the calibration process will be provided. Read and follow the instructions. Then press enter to begin calibration. The script will then periodically show you the calibration status of both sensor. Perform the described motions until the calibration status of both sensors shows as \"3\". When both values show \"3\", the script will automatically retrieve the calibration generated by the BNO055. It will be shown and you will be prompted to save the calibration to the control board. If you choose to save the calibration, it will be applied each time the control board resets or powers on until the calibration is erased or changed. This is generally what you will want to do . Choosing not to save the calibration will not alter the calibration state of the device until reset. The BNO055 will retain it's calibration and re-running the calibration script (guided) will result in the calibration immediately being \"good\" (all sensors show \"3\"). However, resetting the control board (or power cycling it) will cause the calibration to be lost. Details This section provides details on how BNO055 calibration works. If attempting to implement a calibration routine yourself instead of using the calibration script, this information will be useful. Otherwise, probably not. The BNO055 IMU is calibrated using a set of \"calibration constants\". These constants can be stored to the control board so that they are applied to the sensor when the control board powers up. If not calibration constants are stored on the control board, the IMU will instead run an automatic calibration routine in the background. This routine can be used to generate calibration constants for its sensors. When a set of calibration constants is saved to the control board, they will be applied to the BNO055 each time the control board is powered on or is reset. When the control board applies a saved calibration to the BNO055, this disables the BNO055's automatic calibration routine. Thus, it is necessary to erase any calibration stored on the control board before recalibrating the BNO055. Note that the act of saving a calibration to or erasing a calibration from the control board will reconfigure the BNO055. Thus, saving a calibration to the control board will disable the BNO055's calibration routine whereas erasing a calibration from the control board will enable the BNO055's calibration routine. This means that a power cycle of the control board is not required after saving / erasing a calibration. There are two places calibration constants could be located Saved on the control board (referred to as the stored calibration constants) On the BNO055 (referred to as the live calibration constants) The stored calibration constants are written to the BNO055 when the control board starts. Thus, if any constants are saved, the live calibration constants will be the same as the stored calibration constants. However, if there are no stored calibration constants (they are erased or have never been stored), the live calibration constants will be generated by the BNO055's automatic calibration routine. The live calibration constants are only valid when the BNO055's calibration status register indicates a status code of 3 for each sensor in use (accelerometer and gyroscope). The control board provides commands to manage both the stored calibration constants as well as read the live calibration constants and calibration status directly from the BNO055. The python interface scripts provide functions to do so as well. The read_stored_bno055_calibration , erase_stored_bno055_calibration , and store_bno055_calibration functions manage the stored calibration constants. The bno055_read_calibration and bno055_read_calibration_status functions will read the live calibration constants and the calibration status form the BNO055. Erase any stored calibration. This will reset the IMU without applying any calibration. Thus, the live calibration constants will be generated by the BNO055's calibration routine. Reset the BNO055 (this is done via command to the control board). This will ensure that any calibration already generated is erased. Read the BNO055's calibration status register. Bitwise operations can be used to determine the calibration status of individual sensors. Each sensor is 2-bits of data. See the BNO055 datasheet for more information on the CALIB_STAT register. For the control board, only the accelerometer (ACC) and gyroscope (GYR) calibration status is relevant. Each of these will be a value from 0-3. A value of 3 indicates a good calibration. Perform the required operations described in the BNO055 datasheet for gyro and accelerometer calibration. Once the BNO055's calibration status reads 3 for both sensors, the calibration is complete. Read the live calibration constants from the BNO055 Store the calibration constants read in the previous step to the control board. The BNO055 will be reset, but the calibration will be applied. This same calibration that was stored to the control board will be applied to the BNO055 each time the control board is reset (or powered on). When to Re-Calibrate You should always recalibrate if you change the physical sensor in use on the control board (swap a different BNO055) or if you are using a different control board. Calibration constants will vary between sensors, thus constants from one control board cannot be used on another. Likewise, if you change the sensor on your control board the old constants will no longer be valid. Additionally, significant operating environment changes (pressure, temperature, elevation, etc) can cause enough of a change in sensor behavior to require re-calibration. Similarly, a change of the position of the sensor in the vehicle could require re-calibration. It is recommended to recalibrate if any such changes seem to result in degradation of sensor performance. If the BNO055 axis configuration used by your vehicle changes, the BNO055 should be recalibrated. MS5837 (Depth Sensor) Calibration The depth sensor is really just a pressure & temperature sensor. The depth is calculated using two \"constant\" values: the density of the water the vehicle is operating in and the atmospheric pressure at the surface of the water. For the MS5837 atmospheric pressure must be provided in Pascals and the fluid density must be provided in kg / m^3. The default values are Atmospheric Pressure: 101325 Pa Fluid Density: 997 kg / m^3 These default values are applied on control board reset / power on. MS5837 calibration is not persistent (it cannot be stored on the control board like the BNO055 can). These values can be adjusted using a command sent to the control board or by using the python interface script. The fluid density must be provided by the user (there is no good way to experimentally determine this on the vehicle). However, the pressure can be measured while the vehicle (and sensor) are in air at / above the surface. The example interface script ms5837_calibration.py demonstrates calibration of the depth sensor. Again, since the calibration is not persistent it must be applied by command / interface script function each time the device is started. Note that depth sensor calibration is not persistent due to this being of limited use. The atmospheric conditions the vehicle is operating in will change with time. Thus, any persistent configuration would not be trusted and would be frequently overwritten.","title":"Sensor Calibration"},{"location":"user_guide/calibration/#sensor-calibration","text":"","title":"Sensor Calibration"},{"location":"user_guide/calibration/#bno055-imu-calibration","text":"","title":"BNO055 (IMU) Calibration"},{"location":"user_guide/calibration/#calibration-script","text":"It is highly recommended to use the calibration script to calibrate the BNO055. Even if you are not using the python interface to the control board, the calibration script can be used to manage the calibration state. Launch the script by using the following command (note that python3 along with the pyserial library must be installed). Run this command in the iface folder. Replace [PORT] with the serial port of the control board. python3 launch.py example/bno055_calibrate.py -p [PORT] If a calibration is currently saved to the control board, the script will show that configuration and prompt you to erase it. You must erase any stored configuration before you can recalibrate the BNO055. Refusing to erase the calibration will result in the script exiting with no further action taken (this can be used as a way to view the currently saved calibration). If the above prompt is not shown, there is no calibration currently saved on the control board. Next, the script will prompt you to run either \"guided\" or \"manual\" calibration. Guided calibration provides instructions to use the BNO055's automatic calibration routine. This is recommended. Manual calibration allows a user to manually enter values for each calibration constant. Select guided mode. Next, instructions on the calibration process will be provided. Read and follow the instructions. Then press enter to begin calibration. The script will then periodically show you the calibration status of both sensor. Perform the described motions until the calibration status of both sensors shows as \"3\". When both values show \"3\", the script will automatically retrieve the calibration generated by the BNO055. It will be shown and you will be prompted to save the calibration to the control board. If you choose to save the calibration, it will be applied each time the control board resets or powers on until the calibration is erased or changed. This is generally what you will want to do . Choosing not to save the calibration will not alter the calibration state of the device until reset. The BNO055 will retain it's calibration and re-running the calibration script (guided) will result in the calibration immediately being \"good\" (all sensors show \"3\"). However, resetting the control board (or power cycling it) will cause the calibration to be lost.","title":"Calibration Script"},{"location":"user_guide/calibration/#details","text":"This section provides details on how BNO055 calibration works. If attempting to implement a calibration routine yourself instead of using the calibration script, this information will be useful. Otherwise, probably not. The BNO055 IMU is calibrated using a set of \"calibration constants\". These constants can be stored to the control board so that they are applied to the sensor when the control board powers up. If not calibration constants are stored on the control board, the IMU will instead run an automatic calibration routine in the background. This routine can be used to generate calibration constants for its sensors. When a set of calibration constants is saved to the control board, they will be applied to the BNO055 each time the control board is powered on or is reset. When the control board applies a saved calibration to the BNO055, this disables the BNO055's automatic calibration routine. Thus, it is necessary to erase any calibration stored on the control board before recalibrating the BNO055. Note that the act of saving a calibration to or erasing a calibration from the control board will reconfigure the BNO055. Thus, saving a calibration to the control board will disable the BNO055's calibration routine whereas erasing a calibration from the control board will enable the BNO055's calibration routine. This means that a power cycle of the control board is not required after saving / erasing a calibration. There are two places calibration constants could be located Saved on the control board (referred to as the stored calibration constants) On the BNO055 (referred to as the live calibration constants) The stored calibration constants are written to the BNO055 when the control board starts. Thus, if any constants are saved, the live calibration constants will be the same as the stored calibration constants. However, if there are no stored calibration constants (they are erased or have never been stored), the live calibration constants will be generated by the BNO055's automatic calibration routine. The live calibration constants are only valid when the BNO055's calibration status register indicates a status code of 3 for each sensor in use (accelerometer and gyroscope). The control board provides commands to manage both the stored calibration constants as well as read the live calibration constants and calibration status directly from the BNO055. The python interface scripts provide functions to do so as well. The read_stored_bno055_calibration , erase_stored_bno055_calibration , and store_bno055_calibration functions manage the stored calibration constants. The bno055_read_calibration and bno055_read_calibration_status functions will read the live calibration constants and the calibration status form the BNO055. Erase any stored calibration. This will reset the IMU without applying any calibration. Thus, the live calibration constants will be generated by the BNO055's calibration routine. Reset the BNO055 (this is done via command to the control board). This will ensure that any calibration already generated is erased. Read the BNO055's calibration status register. Bitwise operations can be used to determine the calibration status of individual sensors. Each sensor is 2-bits of data. See the BNO055 datasheet for more information on the CALIB_STAT register. For the control board, only the accelerometer (ACC) and gyroscope (GYR) calibration status is relevant. Each of these will be a value from 0-3. A value of 3 indicates a good calibration. Perform the required operations described in the BNO055 datasheet for gyro and accelerometer calibration. Once the BNO055's calibration status reads 3 for both sensors, the calibration is complete. Read the live calibration constants from the BNO055 Store the calibration constants read in the previous step to the control board. The BNO055 will be reset, but the calibration will be applied. This same calibration that was stored to the control board will be applied to the BNO055 each time the control board is reset (or powered on).","title":"Details"},{"location":"user_guide/calibration/#when-to-re-calibrate","text":"You should always recalibrate if you change the physical sensor in use on the control board (swap a different BNO055) or if you are using a different control board. Calibration constants will vary between sensors, thus constants from one control board cannot be used on another. Likewise, if you change the sensor on your control board the old constants will no longer be valid. Additionally, significant operating environment changes (pressure, temperature, elevation, etc) can cause enough of a change in sensor behavior to require re-calibration. Similarly, a change of the position of the sensor in the vehicle could require re-calibration. It is recommended to recalibrate if any such changes seem to result in degradation of sensor performance. If the BNO055 axis configuration used by your vehicle changes, the BNO055 should be recalibrated.","title":"When to Re-Calibrate"},{"location":"user_guide/calibration/#ms5837-depth-sensor-calibration","text":"The depth sensor is really just a pressure & temperature sensor. The depth is calculated using two \"constant\" values: the density of the water the vehicle is operating in and the atmospheric pressure at the surface of the water. For the MS5837 atmospheric pressure must be provided in Pascals and the fluid density must be provided in kg / m^3. The default values are Atmospheric Pressure: 101325 Pa Fluid Density: 997 kg / m^3 These default values are applied on control board reset / power on. MS5837 calibration is not persistent (it cannot be stored on the control board like the BNO055 can). These values can be adjusted using a command sent to the control board or by using the python interface script. The fluid density must be provided by the user (there is no good way to experimentally determine this on the vehicle). However, the pressure can be measured while the vehicle (and sensor) are in air at / above the surface. The example interface script ms5837_calibration.py demonstrates calibration of the depth sensor. Again, since the calibration is not persistent it must be applied by command / interface script function each time the device is started. Note that depth sensor calibration is not persistent due to this being of limited use. The atmospheric conditions the vehicle is operating in will change with time. Thus, any persistent configuration would not be trusted and would be frequently overwritten.","title":"MS5837 (Depth Sensor) Calibration"},{"location":"user_guide/comm_protocol/","text":"Communication Protocol Communication with the control board relies on sending messages between the control board and PC. This section focuses on how messages are sent, not what messages are sent. Hardware Communication Layer Messages are sent to the control board over the MCU's builtin USB port. The control board acts as a USB ACM CDC device. In practice, this means that it shows up as a serial (UART) port on the computer it is connected to. However, baud rate settings are irrelevant (and changing baud rates has no effect). As such, messages are sent to / received from the control board using \"UART\" with an undefined baud rate*. It is still necessary to set a baud rate when opening a UART port (as that information is provided to the device on the other side), but the rate is unused. Additionally, it is expected to operate in 8N1 data mode (8 data bits, no parity, 1 stop bit). A stream of data is sent to the control board over this port. This data is interpreted as described below. * NOTE: The baud rate 1200 is an exception. Openening then closing the port at this baud rate is used to trigger the control board to reboot to its bootloader. Do not use 1200 baud! Message Format and Construction The messages sent to / received from the control board have a specific format. Each message transfers a raw set of bytes (unsigned byte array). This set of bytes is the \"payload data\" of the message. The \"payload data\" is the data that is actually being send via the message. Messages are limited to a maximum payload size of 96 bytes. To be able to identify what data is part of a single message, it is necessary to add some additional information around the payload. The control board uses a special byte to indicate the start of a message ( START_BYTE ) and another one to identify the end of a message ( END_BYTE ). Since the payload could itself contain a start or end byte, there is also an escape byte ( ESCAPE_BYTE ) used to escape a START_BYTE , END_BYTE , or an ESCAPE_BYTE in the payload. START_BYTE becomes ESCAPE_BYTE , START_BYTE END_BYTE becomes ESCAPE_BYTE , END_BYTE ESCAPE_BYTE becomes ESCAPE_BYTE , ESCAPE_BYTE This is similar to escaping a quote in a string using a backslash. For the control board: START_BYTE = 253 (unsigned 8-bit) = -3 (signed 8-bit) END_BYTE = 254 (unsigned 8-bit) = -2 (signed 8-bit) ESCAPE_BYTE = 255 (unsigned 8-bit) = -1 (signed 8-bit) In addition to the control bytes and the payload, each message contains two other pieces of information: First, each message includes a prepended* ID number (16-bit unsigned big-endian integer). These ID numbers are required to be unique only in one direction . This means that two messages sent from the PC to the control board cannot have the same id. Likewise, two messages sent from the control board to the PC cannot have the same id. However, a message sent from PC to control board can have the same id as another message sent from control board to PC. Note that in practice, eventually (after 65535 messages sent one way) ID numbers must eventually repeat. This is acceptable as long as no two messages that are sent \"close together\" have the same id. Effectively, no two \"active\" messages in a single direction may have the same ID (what \"active\" means can vary, but in practice by the time 60,000 messages have been sent, old messages can be assumed inactive). When sending messages from PC to control board, it is required to restrict message ID between 0 and 59999 (inclusive). This is due to how the simulator is implemented. IDs 60000-65535 are reserved for simulator use. Note that IDs generated by the control board will not be restricted to this range. Second, each message has a CRC appended* to it. This is a 16-bit CRC using the CCITT-FALSE algorithm. It is appended* to the message big endian. The CRC is calculated on the concatenation of the message id bytes and the raw (unescaped) payload bytes. Just like the payload data, when prepending or appending message id or crc, it is necessary to escape bytes that are equal to control bytes (start, end, escape). * Note that append and prepend still mean contained between control (start and end) bytes. This results in message construction looking like the following (the \"payload\" is the raw message being sent).","title":"Communication Protocol"},{"location":"user_guide/comm_protocol/#communication-protocol","text":"Communication with the control board relies on sending messages between the control board and PC. This section focuses on how messages are sent, not what messages are sent.","title":"Communication Protocol"},{"location":"user_guide/comm_protocol/#hardware-communication-layer","text":"Messages are sent to the control board over the MCU's builtin USB port. The control board acts as a USB ACM CDC device. In practice, this means that it shows up as a serial (UART) port on the computer it is connected to. However, baud rate settings are irrelevant (and changing baud rates has no effect). As such, messages are sent to / received from the control board using \"UART\" with an undefined baud rate*. It is still necessary to set a baud rate when opening a UART port (as that information is provided to the device on the other side), but the rate is unused. Additionally, it is expected to operate in 8N1 data mode (8 data bits, no parity, 1 stop bit). A stream of data is sent to the control board over this port. This data is interpreted as described below. * NOTE: The baud rate 1200 is an exception. Openening then closing the port at this baud rate is used to trigger the control board to reboot to its bootloader. Do not use 1200 baud!","title":"Hardware Communication Layer"},{"location":"user_guide/comm_protocol/#message-format-and-construction","text":"The messages sent to / received from the control board have a specific format. Each message transfers a raw set of bytes (unsigned byte array). This set of bytes is the \"payload data\" of the message. The \"payload data\" is the data that is actually being send via the message. Messages are limited to a maximum payload size of 96 bytes. To be able to identify what data is part of a single message, it is necessary to add some additional information around the payload. The control board uses a special byte to indicate the start of a message ( START_BYTE ) and another one to identify the end of a message ( END_BYTE ). Since the payload could itself contain a start or end byte, there is also an escape byte ( ESCAPE_BYTE ) used to escape a START_BYTE , END_BYTE , or an ESCAPE_BYTE in the payload. START_BYTE becomes ESCAPE_BYTE , START_BYTE END_BYTE becomes ESCAPE_BYTE , END_BYTE ESCAPE_BYTE becomes ESCAPE_BYTE , ESCAPE_BYTE This is similar to escaping a quote in a string using a backslash. For the control board: START_BYTE = 253 (unsigned 8-bit) = -3 (signed 8-bit) END_BYTE = 254 (unsigned 8-bit) = -2 (signed 8-bit) ESCAPE_BYTE = 255 (unsigned 8-bit) = -1 (signed 8-bit) In addition to the control bytes and the payload, each message contains two other pieces of information: First, each message includes a prepended* ID number (16-bit unsigned big-endian integer). These ID numbers are required to be unique only in one direction . This means that two messages sent from the PC to the control board cannot have the same id. Likewise, two messages sent from the control board to the PC cannot have the same id. However, a message sent from PC to control board can have the same id as another message sent from control board to PC. Note that in practice, eventually (after 65535 messages sent one way) ID numbers must eventually repeat. This is acceptable as long as no two messages that are sent \"close together\" have the same id. Effectively, no two \"active\" messages in a single direction may have the same ID (what \"active\" means can vary, but in practice by the time 60,000 messages have been sent, old messages can be assumed inactive). When sending messages from PC to control board, it is required to restrict message ID between 0 and 59999 (inclusive). This is due to how the simulator is implemented. IDs 60000-65535 are reserved for simulator use. Note that IDs generated by the control board will not be restricted to this range. Second, each message has a CRC appended* to it. This is a 16-bit CRC using the CCITT-FALSE algorithm. It is appended* to the message big endian. The CRC is calculated on the concatenation of the message id bytes and the raw (unescaped) payload bytes. Just like the payload data, when prepending or appending message id or crc, it is necessary to escape bytes that are equal to control bytes (start, end, escape). * Note that append and prepend still mean contained between control (start and end) bytes. This results in message construction looking like the following (the \"payload\" is the raw message being sent).","title":"Message Format and Construction"},{"location":"user_guide/general_use/","text":"Using Control Board TODO General Procedure Connecting to Control Board System configuration (motor matrix, thruster inversions) Sensor configuration Validating sensor connectivity Modes of operation & setting speed Motor Watchdog Reading Sensor data Example program using python interface script LED Indicator Info","title":"Using Control board"},{"location":"user_guide/general_use/#using-control-board","text":"TODO General Procedure Connecting to Control Board System configuration (motor matrix, thruster inversions) Sensor configuration Validating sensor connectivity Modes of operation & setting speed Motor Watchdog Reading Sensor data Example program using python interface script LED Indicator Info","title":"Using Control Board"},{"location":"user_guide/messages/","text":"Messages This section describes what specific messages are sent to / received from the control board and what they do / mean. This does not address how messages are constructed or sent. For such information, see Communication Protocol . Note: The Communication Protocol page uses the term \"payload\" for the data being transferred and \"message\" for the formatted / fully constructed set of data. Here, what we refer to as \"messages\" are actually the \"payload\" data, not the constructed data. Types of Messages Commands : Messages instructing an action be taken. These messages must be acknowledged upon receipt. The acknowledgement typically contains no data. Sent from PC to control board. Queries : Messages requesting information. These messages must be acknowledged upon receipt. The acknowledgement will contain the requested information. Sent from PC to control board. Acknowledgements : A very specific type of message acknowledging receipt of another message (with an error code and optional data). Sent from control board to PC. Status Messages : Unprompted messages containing information about state / data changes. Sent from control board to PC. Message Definition Conventions In the following sections, the following standard is used to describe message contents: Each message's contents are shown as a comma separated list of bytes. Each comma separated item is a single byte, with one exception for parameters (as described below) Parameters are shown inside square brackets. Parameters represent a value that will be described in more detail below the message structure information. Parameters can be multiple bytes (even though they take only one entry between commas). ASCII characters are shown in single quotes. These are single byte unsigned ASCII characters. Numbers are not contained within any symbols. Numbers may be in hex (prefix 0x), binary (prefix 0b), or decimal (no prefix). Commands Configuration Commands Motor Matrix Set Motor matrix set command is used to set a single row of the motor matrix. It has the following format 'M', 'M', 'A', 'T', 'S', [thruster_num], [x], [y], [z], [pitch], [roll], [yaw] [thruster_num] : A single byte who'se unsigned value is the thruster number the row data should be set for (1-8). [x] , [y] , [z] , [pitch] , [roll] , [yaw] : Columns of the motor matrix row being set. Each is a 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Motor Matrix Update Motor matrix update command is used to inform the control board the motor matrix has changed. Causes the control board to perform some calculations with the new motor matrix. This should be sent after writing all rows of the motor matrix that should change using the motor matrix set command. The motor matrix update command has the following format 'M', 'M', 'A', 'T', 'U' This message will be acknowledged. The acknowledge message will contain no result data. Thruster Inversion Set Thruster inversion set command is used to invert the positive and negative direction of thrusters. It has the following format 'T', 'I', 'N', 'V', [inv] [inv] : A single byte where each bit represents the inversion status of a thruster. The MSB (bit 7) corresponds to thruster 8 and the LSB corresponds to thruster 1 (bit + 1 = thruster). A bit value of 1 means the thruster is inverted. A bit value of 0 means the thruster is not inverted. This message will be acknowledged. The acknowledge message will contain no result data. Relative DoF Speed Set Used to set relative speeds of motion in each DoF. There are two groups: linear (x, y, z) and angular (xrot, yrot, zrot). Within each group, use 1.0 for the fastest DoF. Other DoFs in the group are percentages of the fastest speed (from -1.0 to 1.0). This message has the following format 'R', 'E', 'L', 'D', 'O', 'F', [x], [y], [z], [xrot], [yrot], [zrot] [x] , [y] , [z] , [xrot] , [yrot] , [zrot] : 32-bit little endian floats. This message will be acknowledged. The acknowledge message will contain no result data. BNO055 IMU Axis Configure Command Used to configure the BNO055 IMU's axis orientation. Note: This will also reset the accumulated euler angles to zero . 'B', 'N', 'O', '0', '5', '5', 'A', [config] [config] : A single byte. The value of this byte is between 0 and 7 (inclusive) representing on of the BNO055 axis configs (P0 to P7) as described in the BNO055 datasheet. Note: Changing the axis config changes IMU mode. Thus, there will be a brief time afterwards where the IMU may report zeros for all data. This message will be acknowledged. The acknowledge message will contain no result data. Stability Assist Mode PID Tune Command Used to tune stability assist mode PID controllers. Note that the rotation PIDs (xrot, yrot, and zrot) are also used in OHOLD mode. Likewise, the depth PID is also used in DHOLD mode. The command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', 'T', 'N', [which], [kp], [ki], [kd], [limit], [invert] [which] indicates which PID to tune ('X' = xrot, 'Y' = yrot, 'Z' = zrot, 'D' = depth hold). [kp] , [ki] , [kd] are proportional, integral, derivative, and feed-forward gains (32-bit float little endian). [limit] Is the PID controller's max output (limits max speed in the controlled DoF). Must be between 0.0 and 1.0. 32-bit float little endian. [invert] Set to one to invert PID output. Zero otherwise. Motor Control Commands Raw Speed Set Used to set motor speeds in RAW mode. This command has the following format. 'R', 'A', 'W', [speed_1], [speed_2], [speed_3], [speed_4], [speed_5], [speed_6], [speed_7], [speed_8] [speed_n] : The speed of thruster n from -1.0 to 1.0. A 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Local Speed Set Used to set motor speeds in LOCAL mode. This command has the following format 'L', 'O', 'C', 'A', 'L', [x], [y], [z], [xrot], [yrot], [zrot] [x] , [y] , [z] , [xrot] , [yrot] , [zrot] : Speed for each DoF relative to the robot -1.0 to 1.0. A 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Global Speed Set Used to set motor speeds in GLOBAL mode. This command has the following format 'G', 'L', 'O', 'B', 'A', 'L', [x], [y], [z], [pitch_spd], [roll_spd], [yaw_spd] [x] , [y] , [z] : Speed for each \"world-relative\" (pitch and roll compensated) DoF -1.0 to 1.0. 32-bit float (little endian). [pitch_spd] , [roll_spd] , [yaw_spd] : Rate of change of vehicle pitch, roll, and yaw -1.0 to 1.0. This message will be acknowledged. The acknowledge message will contain no result data. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Orientation Hold Speed Set (Variant 1) Used to set motor speeds in ORIENTATION_HOLD mode using a speed for yaw. This command has the following format 'O', 'H', 'O', 'L', 'D', '1', [x], [y], [z], [yaw_spd], [target_pitch], [target_roll] Each value is a 32-bit float little endian. Pitch and roll are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). x, y, z, and yaw_spd are x, y, z, and yaw_spd just as in global mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Orientation Hold Speed Set (Variant 2) Used to set motor speeds in ORIENTATION_HOLD mode using a PID to maintain a target yaw. This command has the following format 'O', 'H', 'O', 'L', 'D', '2', [x], [y], [z], [target_pitch], [target_roll], [target_yaw] Each value is a 32-bit float little endian. Target Pitch, roll, and yaw are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). x, y, and z are speeds in the x, y, and z DoFs the same as in GLOBAL mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Stability Assist Speed Set (Variant 1) Used to set motor speeds in STABILITY_ASSIST mode using a speed for yaw. This command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', '1', [x], [y], [yaw_spd], [target_pitch], [target_roll], [target_depth] Each value is a 32-bit float little endian. Pitch and roll are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). Depth is in meters where negative numbers are below the surface. x, y, and yaw_spd are x, y, and yaw_spd just as in global mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Stability Assist Speed Set (Variant 2) Used to set motor speeds in STABILITY_ASSIST mode using a PID to maintain a target yaw. This command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', '2', [x], [y], [target_pitch], [target_roll], [target_yaw], [target_depth] Each value is a 32-bit float little endian. Target Pitch, roll, and yaw are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). Depth is in meters where negative numbers are below the surface. x and y are speeds in the x and y DoFs the same as in GLOBAL mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Depth Hold Speed Set Used to set motor speeds in DEPTH_HOLD mode. This command has the following format 'D', 'H', 'O', 'L', 'D', [x], [y], [pitch_spd], [roll_spd], [yaw_spd], [target_depth] Each value is a 32-bit float. Everything except [target_depth] is a speed (same as GLOBAL mode speeds). Target depth is in meters (negative for below surface). Other Commands Feed Motor Watchdog Used to feed the motor watchdog so it does not kill the motors. This command has the following format 'W', 'D', 'G', 'F' This message will be acknowledged. The acknowledge message will contain no result data. BNO055 Periodic Read Used to enable / disable periodic reading of BNO055 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use IMU data. 'B', 'N', 'O', '0', '5', '5', 'P', [enable] [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled. This message will be acknowledged. The acknowledge message will contain no result data. MS5837 Periodic Read Used to enable / disable periodic reading of MS5837 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use depth sensor data. 'M', 'S', '5', '8', '3', '7', 'P', [enable] [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled. This message will be acknowledged. The acknowledge message will contain no result data. Reset Command This command is used to rest the control board itself. This will reset the microcontroller on the control board, thus the USB device will disconnect and reconnect (note that if your program still holds the port when this happens, the USB device will likely be assigned a different port number). 'R', 'E', 'S', 'E', 'T', 0x0D, 0x1E This message is not acknowledged. Simulator Hijack Command This command is used by the simulator to hijack a real control board. This allows the simulator to pass certain information to and receive certain information from the control board. This enables testing of the actual firmware and reproducing bugs under simulation. The command has the following format. 'S', 'I', 'M', 'H', 'I', 'J', 'A', 'C', 'K', [hijack] [hijack] is an 8-bit integer (unsigned) with a value of 1 or 0. If 1, the control board is put into simulator hijack mode. If 0, it is removed from simulator hijack mode. This message will be acknowledged. The acknowledge message will contain no result data. Save BNO055 Stored Calibration Command This command is used to store a set of calibration constants for the BNO055 to the control board. This will write the \"stored calibration constants\". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'S', [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] Each value is a signed 16-bit integer. The meaning of each value is described in the BNO055 datasheet. This message will be acknowledged. The acknowledge message will contain no result data. Erase BNO055 Stored Calibration Command This command is used to erase calibration constants for the BNO055 from the control board. This will erase the \"stored calibration constants\". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'E' This message will be acknowledged. The acknowledge message will contain no result data. Write MS5837 Calibration Command This command is used to write the values of the MS5837 calibration constants. The command has the following format 'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'S', [atm_pressure], [fluid_density] Both values are 32-bit little endian floats. This message will be acknowledged. The acknowledge message will contain no result data. Reset BNO055 Command This command is used to reset / reconfigure the BNO055. This is typically used to clear any auto generated calibration constants. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'R', 'S', 'T' This message will be acknowledged. The acknowledge message will contain no result data. Queries Version Info Query Get the version info from the control board. 'C', 'B', 'V', 'E', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format [cb_ver],[fw_ver_major],[fw_ver_minor],[fw_ver_revision],[fw_ver_type],[fw_ver_build] Each value is an unsigned 8-bit integer. All are simply interpreted as numbers, except for fw_ver_type , which is an ASCII character. cb_ver : Version of the control board hardware (CBv1 or CBv2 = 1 or 2; 0 = SimCB in simulator) fw_ver_major : Major version of firmware running on the control board fw_ver_minor : Minor version of firmware running on the control board fw_ver_revision : Revision version of firmware running on the control board fw_ver_type : Type of firmware release. 'a' = alpha, 'b' = beta, 'c' = release candidate (rc), ' ' (space) = full release fw_ver_build : Build number for pre-release firmware. Should be ignored for fw_ver_type release (' ') Sensor Status Query Gets the status of all sensors (BNO055 and MS5837). 'S', 'S', 'T', 'A', 'T' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format [status_byte] [status_byte] : Single byte containing bits for the status of all sensors. Bit 0 (LSB) is BNO055 status. Bit 1 is MS5837 status. A status bit of 1 indicates the sensor is \"ready\" (connected and can be used). A status bit of 0 indicates the sensor is \"not ready\". BNO055 Read Reads BNO055 IMU data once. 'B', 'N', 'O', '0', '5', '5', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the BNO055 data status message. [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw] Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles. MS5837 Read Reads MS5837 data once. 'M', 'S', '5', '8', '3', '7', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the MS5837 data status message. [depth_m], [pressure_pa], [temp_c] depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water). Last Reset Cause Query Get error code for last system reset cause of the control board. Generally not useful for end users, except for reporting errors. Mainly a debug / development tool. See error codes in firmware source debug.h . 'R', 'S', 'T', 'W', 'H', 'Y' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [error_code] error_code is a 32-bit integer (signed), little endian. Read BNO055 Stored Calibration Query This command is used to read a set of calibration constants for the BNO055 from the control board. This will read the \"stored calibration constants\". The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [valid], [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] valid is an 8-bit integer. A value of 0 indicates that no calibration is stored on the control board (other values have no meaning). A value of 1 indicates that a calibration is stored (other values are that calibration). All other values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Read BNO055 Live Calibration Status Query This command is used to read the status of the BNO055's calibration routine. Note that this reads the value directly from the BNO055. This value is meaningless if a calibration was manually applied to the sensor. Thus, this is only useful if any \"stored calibration constants\" are first erased. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'C', 'S' This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. If acknowledged with no error, the response will contain data in the following format. [status] status is an 8-bit integer. The value of status is the value of the BNO055's CALIB_STAT register. The meaning of this number is described in the BNO055 datasheet. Read BNO055 Live Calibration Values Query This command is used to read a set of calibration constants from the BNO055. This will read the \"live calibration constants\" directly from the BNO055. Note that the calibration constants are only valid if the calibration status from the BNO055 is 3 for the accelerometer and gyroscope. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'C', 'V' This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. If acknowledged with no error, the response will contain data in the following format. [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] All values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Read MS5837 Calibration Query This command is used to read the values of the MS5837 calibration constants. The command has the following format 'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'G' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [atm_pressure], [fluid_density] Both values are 32-bit little endian floats. Acknowledgements An acknowledgement message has the following format 'A', 'C', 'K', [ack_id], [error_code], [result] [ack_id] : The ID of the message being acknowledged. Unsigned 16-bit integer (big endian). [error_code] : A single byte error code. 0 = None: No error. 1 = Unknown Message: Control board does not recognize the message. 2 = Invalid arguments: Message is recognized, but arguments are invalid 3 = Invalid Command: Command is known, but is not valid at this time. 255 = Reserved: Control board will not use this code. Typically used as timeout. [result] : Optional data of variable size attached to the acknowledge message. Its size, format, and meaning depends on the message being acknowledged. Status Messages Motor Watchdog Status Motor watch status message is used by the control board to notify the PC about changes to motor (watchdog) state. It has the following format 'W', 'D', 'G', 'S', [status] [status] is a single byte. A value of 1 indicates the motors are enabled. A value of zero indicates the motors are currently killed by the watchdog. BNO055 Data Status Used by the control board to periodically send IMU data to the PC. Only sent when BNO055 periodic reads are enabled via the BNO055 periodic read command. The message has the following format 'B', 'N', 'O', '0', '5', '5', 'D', [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw] Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles. MS5837 Data Status Used by the control board to periodically send IMU data to the PC. Only sent when BNO055 periodic reads are enabled via the BNO055 periodic read command. The message has the following format 'M', 'S', '5', '8', '3', '7', 'D', [depth_m], [pressure_pa], [temp_c] depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water). Debug Status Messages Used only during development. These will not occur on release builds of the firmware. These are arbitrary messages sent by the control board to the PC for the firmware developer's use during development. They have the following format 'D', 'E', 'B', 'U', 'G', [msg] msg is an ascii string. Debug Data Status Messages Used only during development. These will not occur on release builds of the firmware. These are arbitrary messages sent by the control board to the PC for the firmware developer's use during development. They have the following format 'D', 'B', 'G', 'D', 'A', 'T', [msg] msg is arbitrary data.","title":"Messages"},{"location":"user_guide/messages/#messages","text":"This section describes what specific messages are sent to / received from the control board and what they do / mean. This does not address how messages are constructed or sent. For such information, see Communication Protocol . Note: The Communication Protocol page uses the term \"payload\" for the data being transferred and \"message\" for the formatted / fully constructed set of data. Here, what we refer to as \"messages\" are actually the \"payload\" data, not the constructed data.","title":"Messages"},{"location":"user_guide/messages/#types-of-messages","text":"Commands : Messages instructing an action be taken. These messages must be acknowledged upon receipt. The acknowledgement typically contains no data. Sent from PC to control board. Queries : Messages requesting information. These messages must be acknowledged upon receipt. The acknowledgement will contain the requested information. Sent from PC to control board. Acknowledgements : A very specific type of message acknowledging receipt of another message (with an error code and optional data). Sent from control board to PC. Status Messages : Unprompted messages containing information about state / data changes. Sent from control board to PC.","title":"Types of Messages"},{"location":"user_guide/messages/#message-definition-conventions","text":"In the following sections, the following standard is used to describe message contents: Each message's contents are shown as a comma separated list of bytes. Each comma separated item is a single byte, with one exception for parameters (as described below) Parameters are shown inside square brackets. Parameters represent a value that will be described in more detail below the message structure information. Parameters can be multiple bytes (even though they take only one entry between commas). ASCII characters are shown in single quotes. These are single byte unsigned ASCII characters. Numbers are not contained within any symbols. Numbers may be in hex (prefix 0x), binary (prefix 0b), or decimal (no prefix).","title":"Message Definition Conventions"},{"location":"user_guide/messages/#commands","text":"","title":"Commands"},{"location":"user_guide/messages/#configuration-commands","text":"Motor Matrix Set Motor matrix set command is used to set a single row of the motor matrix. It has the following format 'M', 'M', 'A', 'T', 'S', [thruster_num], [x], [y], [z], [pitch], [roll], [yaw] [thruster_num] : A single byte who'se unsigned value is the thruster number the row data should be set for (1-8). [x] , [y] , [z] , [pitch] , [roll] , [yaw] : Columns of the motor matrix row being set. Each is a 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Motor Matrix Update Motor matrix update command is used to inform the control board the motor matrix has changed. Causes the control board to perform some calculations with the new motor matrix. This should be sent after writing all rows of the motor matrix that should change using the motor matrix set command. The motor matrix update command has the following format 'M', 'M', 'A', 'T', 'U' This message will be acknowledged. The acknowledge message will contain no result data. Thruster Inversion Set Thruster inversion set command is used to invert the positive and negative direction of thrusters. It has the following format 'T', 'I', 'N', 'V', [inv] [inv] : A single byte where each bit represents the inversion status of a thruster. The MSB (bit 7) corresponds to thruster 8 and the LSB corresponds to thruster 1 (bit + 1 = thruster). A bit value of 1 means the thruster is inverted. A bit value of 0 means the thruster is not inverted. This message will be acknowledged. The acknowledge message will contain no result data. Relative DoF Speed Set Used to set relative speeds of motion in each DoF. There are two groups: linear (x, y, z) and angular (xrot, yrot, zrot). Within each group, use 1.0 for the fastest DoF. Other DoFs in the group are percentages of the fastest speed (from -1.0 to 1.0). This message has the following format 'R', 'E', 'L', 'D', 'O', 'F', [x], [y], [z], [xrot], [yrot], [zrot] [x] , [y] , [z] , [xrot] , [yrot] , [zrot] : 32-bit little endian floats. This message will be acknowledged. The acknowledge message will contain no result data. BNO055 IMU Axis Configure Command Used to configure the BNO055 IMU's axis orientation. Note: This will also reset the accumulated euler angles to zero . 'B', 'N', 'O', '0', '5', '5', 'A', [config] [config] : A single byte. The value of this byte is between 0 and 7 (inclusive) representing on of the BNO055 axis configs (P0 to P7) as described in the BNO055 datasheet. Note: Changing the axis config changes IMU mode. Thus, there will be a brief time afterwards where the IMU may report zeros for all data. This message will be acknowledged. The acknowledge message will contain no result data. Stability Assist Mode PID Tune Command Used to tune stability assist mode PID controllers. Note that the rotation PIDs (xrot, yrot, and zrot) are also used in OHOLD mode. Likewise, the depth PID is also used in DHOLD mode. The command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', 'T', 'N', [which], [kp], [ki], [kd], [limit], [invert] [which] indicates which PID to tune ('X' = xrot, 'Y' = yrot, 'Z' = zrot, 'D' = depth hold). [kp] , [ki] , [kd] are proportional, integral, derivative, and feed-forward gains (32-bit float little endian). [limit] Is the PID controller's max output (limits max speed in the controlled DoF). Must be between 0.0 and 1.0. 32-bit float little endian. [invert] Set to one to invert PID output. Zero otherwise.","title":"Configuration Commands"},{"location":"user_guide/messages/#motor-control-commands","text":"Raw Speed Set Used to set motor speeds in RAW mode. This command has the following format. 'R', 'A', 'W', [speed_1], [speed_2], [speed_3], [speed_4], [speed_5], [speed_6], [speed_7], [speed_8] [speed_n] : The speed of thruster n from -1.0 to 1.0. A 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Local Speed Set Used to set motor speeds in LOCAL mode. This command has the following format 'L', 'O', 'C', 'A', 'L', [x], [y], [z], [xrot], [yrot], [zrot] [x] , [y] , [z] , [xrot] , [yrot] , [zrot] : Speed for each DoF relative to the robot -1.0 to 1.0. A 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Global Speed Set Used to set motor speeds in GLOBAL mode. This command has the following format 'G', 'L', 'O', 'B', 'A', 'L', [x], [y], [z], [pitch_spd], [roll_spd], [yaw_spd] [x] , [y] , [z] : Speed for each \"world-relative\" (pitch and roll compensated) DoF -1.0 to 1.0. 32-bit float (little endian). [pitch_spd] , [roll_spd] , [yaw_spd] : Rate of change of vehicle pitch, roll, and yaw -1.0 to 1.0. This message will be acknowledged. The acknowledge message will contain no result data. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Orientation Hold Speed Set (Variant 1) Used to set motor speeds in ORIENTATION_HOLD mode using a speed for yaw. This command has the following format 'O', 'H', 'O', 'L', 'D', '1', [x], [y], [z], [yaw_spd], [target_pitch], [target_roll] Each value is a 32-bit float little endian. Pitch and roll are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). x, y, z, and yaw_spd are x, y, z, and yaw_spd just as in global mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Orientation Hold Speed Set (Variant 2) Used to set motor speeds in ORIENTATION_HOLD mode using a PID to maintain a target yaw. This command has the following format 'O', 'H', 'O', 'L', 'D', '2', [x], [y], [z], [target_pitch], [target_roll], [target_yaw] Each value is a 32-bit float little endian. Target Pitch, roll, and yaw are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). x, y, and z are speeds in the x, y, and z DoFs the same as in GLOBAL mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Stability Assist Speed Set (Variant 1) Used to set motor speeds in STABILITY_ASSIST mode using a speed for yaw. This command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', '1', [x], [y], [yaw_spd], [target_pitch], [target_roll], [target_depth] Each value is a 32-bit float little endian. Pitch and roll are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). Depth is in meters where negative numbers are below the surface. x, y, and yaw_spd are x, y, and yaw_spd just as in global mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Stability Assist Speed Set (Variant 2) Used to set motor speeds in STABILITY_ASSIST mode using a PID to maintain a target yaw. This command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', '2', [x], [y], [target_pitch], [target_roll], [target_yaw], [target_depth] Each value is a 32-bit float little endian. Target Pitch, roll, and yaw are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). Depth is in meters where negative numbers are below the surface. x and y are speeds in the x and y DoFs the same as in GLOBAL mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Depth Hold Speed Set Used to set motor speeds in DEPTH_HOLD mode. This command has the following format 'D', 'H', 'O', 'L', 'D', [x], [y], [pitch_spd], [roll_spd], [yaw_spd], [target_depth] Each value is a 32-bit float. Everything except [target_depth] is a speed (same as GLOBAL mode speeds). Target depth is in meters (negative for below surface).","title":"Motor Control Commands"},{"location":"user_guide/messages/#other-commands","text":"Feed Motor Watchdog Used to feed the motor watchdog so it does not kill the motors. This command has the following format 'W', 'D', 'G', 'F' This message will be acknowledged. The acknowledge message will contain no result data. BNO055 Periodic Read Used to enable / disable periodic reading of BNO055 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use IMU data. 'B', 'N', 'O', '0', '5', '5', 'P', [enable] [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled. This message will be acknowledged. The acknowledge message will contain no result data. MS5837 Periodic Read Used to enable / disable periodic reading of MS5837 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use depth sensor data. 'M', 'S', '5', '8', '3', '7', 'P', [enable] [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled. This message will be acknowledged. The acknowledge message will contain no result data. Reset Command This command is used to rest the control board itself. This will reset the microcontroller on the control board, thus the USB device will disconnect and reconnect (note that if your program still holds the port when this happens, the USB device will likely be assigned a different port number). 'R', 'E', 'S', 'E', 'T', 0x0D, 0x1E This message is not acknowledged. Simulator Hijack Command This command is used by the simulator to hijack a real control board. This allows the simulator to pass certain information to and receive certain information from the control board. This enables testing of the actual firmware and reproducing bugs under simulation. The command has the following format. 'S', 'I', 'M', 'H', 'I', 'J', 'A', 'C', 'K', [hijack] [hijack] is an 8-bit integer (unsigned) with a value of 1 or 0. If 1, the control board is put into simulator hijack mode. If 0, it is removed from simulator hijack mode. This message will be acknowledged. The acknowledge message will contain no result data. Save BNO055 Stored Calibration Command This command is used to store a set of calibration constants for the BNO055 to the control board. This will write the \"stored calibration constants\". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'S', [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] Each value is a signed 16-bit integer. The meaning of each value is described in the BNO055 datasheet. This message will be acknowledged. The acknowledge message will contain no result data. Erase BNO055 Stored Calibration Command This command is used to erase calibration constants for the BNO055 from the control board. This will erase the \"stored calibration constants\". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'E' This message will be acknowledged. The acknowledge message will contain no result data. Write MS5837 Calibration Command This command is used to write the values of the MS5837 calibration constants. The command has the following format 'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'S', [atm_pressure], [fluid_density] Both values are 32-bit little endian floats. This message will be acknowledged. The acknowledge message will contain no result data. Reset BNO055 Command This command is used to reset / reconfigure the BNO055. This is typically used to clear any auto generated calibration constants. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'R', 'S', 'T' This message will be acknowledged. The acknowledge message will contain no result data.","title":"Other Commands"},{"location":"user_guide/messages/#queries","text":"Version Info Query Get the version info from the control board. 'C', 'B', 'V', 'E', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format [cb_ver],[fw_ver_major],[fw_ver_minor],[fw_ver_revision],[fw_ver_type],[fw_ver_build] Each value is an unsigned 8-bit integer. All are simply interpreted as numbers, except for fw_ver_type , which is an ASCII character. cb_ver : Version of the control board hardware (CBv1 or CBv2 = 1 or 2; 0 = SimCB in simulator) fw_ver_major : Major version of firmware running on the control board fw_ver_minor : Minor version of firmware running on the control board fw_ver_revision : Revision version of firmware running on the control board fw_ver_type : Type of firmware release. 'a' = alpha, 'b' = beta, 'c' = release candidate (rc), ' ' (space) = full release fw_ver_build : Build number for pre-release firmware. Should be ignored for fw_ver_type release (' ') Sensor Status Query Gets the status of all sensors (BNO055 and MS5837). 'S', 'S', 'T', 'A', 'T' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format [status_byte] [status_byte] : Single byte containing bits for the status of all sensors. Bit 0 (LSB) is BNO055 status. Bit 1 is MS5837 status. A status bit of 1 indicates the sensor is \"ready\" (connected and can be used). A status bit of 0 indicates the sensor is \"not ready\". BNO055 Read Reads BNO055 IMU data once. 'B', 'N', 'O', '0', '5', '5', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the BNO055 data status message. [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw] Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles. MS5837 Read Reads MS5837 data once. 'M', 'S', '5', '8', '3', '7', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the MS5837 data status message. [depth_m], [pressure_pa], [temp_c] depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water). Last Reset Cause Query Get error code for last system reset cause of the control board. Generally not useful for end users, except for reporting errors. Mainly a debug / development tool. See error codes in firmware source debug.h . 'R', 'S', 'T', 'W', 'H', 'Y' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [error_code] error_code is a 32-bit integer (signed), little endian. Read BNO055 Stored Calibration Query This command is used to read a set of calibration constants for the BNO055 from the control board. This will read the \"stored calibration constants\". The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [valid], [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] valid is an 8-bit integer. A value of 0 indicates that no calibration is stored on the control board (other values have no meaning). A value of 1 indicates that a calibration is stored (other values are that calibration). All other values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Read BNO055 Live Calibration Status Query This command is used to read the status of the BNO055's calibration routine. Note that this reads the value directly from the BNO055. This value is meaningless if a calibration was manually applied to the sensor. Thus, this is only useful if any \"stored calibration constants\" are first erased. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'C', 'S' This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. If acknowledged with no error, the response will contain data in the following format. [status] status is an 8-bit integer. The value of status is the value of the BNO055's CALIB_STAT register. The meaning of this number is described in the BNO055 datasheet. Read BNO055 Live Calibration Values Query This command is used to read a set of calibration constants from the BNO055. This will read the \"live calibration constants\" directly from the BNO055. Note that the calibration constants are only valid if the calibration status from the BNO055 is 3 for the accelerometer and gyroscope. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'C', 'V' This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. If acknowledged with no error, the response will contain data in the following format. [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] All values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Read MS5837 Calibration Query This command is used to read the values of the MS5837 calibration constants. The command has the following format 'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'G' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [atm_pressure], [fluid_density] Both values are 32-bit little endian floats.","title":"Queries"},{"location":"user_guide/messages/#acknowledgements","text":"An acknowledgement message has the following format 'A', 'C', 'K', [ack_id], [error_code], [result] [ack_id] : The ID of the message being acknowledged. Unsigned 16-bit integer (big endian). [error_code] : A single byte error code. 0 = None: No error. 1 = Unknown Message: Control board does not recognize the message. 2 = Invalid arguments: Message is recognized, but arguments are invalid 3 = Invalid Command: Command is known, but is not valid at this time. 255 = Reserved: Control board will not use this code. Typically used as timeout. [result] : Optional data of variable size attached to the acknowledge message. Its size, format, and meaning depends on the message being acknowledged.","title":"Acknowledgements"},{"location":"user_guide/messages/#status-messages","text":"Motor Watchdog Status Motor watch status message is used by the control board to notify the PC about changes to motor (watchdog) state. It has the following format 'W', 'D', 'G', 'S', [status] [status] is a single byte. A value of 1 indicates the motors are enabled. A value of zero indicates the motors are currently killed by the watchdog. BNO055 Data Status Used by the control board to periodically send IMU data to the PC. Only sent when BNO055 periodic reads are enabled via the BNO055 periodic read command. The message has the following format 'B', 'N', 'O', '0', '5', '5', 'D', [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw] Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles. MS5837 Data Status Used by the control board to periodically send IMU data to the PC. Only sent when BNO055 periodic reads are enabled via the BNO055 periodic read command. The message has the following format 'M', 'S', '5', '8', '3', '7', 'D', [depth_m], [pressure_pa], [temp_c] depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water). Debug Status Messages Used only during development. These will not occur on release builds of the firmware. These are arbitrary messages sent by the control board to the PC for the firmware developer's use during development. They have the following format 'D', 'E', 'B', 'U', 'G', [msg] msg is an ascii string. Debug Data Status Messages Used only during development. These will not occur on release builds of the firmware. These are arbitrary messages sent by the control board to the PC for the firmware developer's use during development. They have the following format 'D', 'B', 'G', 'D', 'A', 'T', [msg] msg is arbitrary data.","title":"Status Messages"},{"location":"user_guide/preparing_board/","text":"Preparing a Control Board TODO: Flashing firmware Wiring Testing USB","title":"Preparing a Control Board"},{"location":"user_guide/preparing_board/#preparing-a-control-board","text":"TODO: Flashing firmware Wiring Testing USB","title":"Preparing a Control Board"},{"location":"user_guide/simulator/","text":"Using the Simulator TODO","title":"Using the Simulator"},{"location":"user_guide/simulator/#using-the-simulator","text":"TODO","title":"Using the Simulator"}]} \ No newline at end of file +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"AUV Control Board Documentation AUV Control Board is a low-cost open-source motion controller for (Autonomous) Underwater Vehicles. It is designed for vehicles with fixed position thrusters and supports motion in 6 degrees of freedom. There are currently two versions of control board. Neither version has more features than the other. Version 2 was developed to be able to use a different chip / board that was easier to get. Version 1 uses an Adafruit ItsyBitsy M4 (Microchip ATSAMD51 chip) Version 2 use a WeAct Studio Black Pill (STMicroelectronics STM32F4) The firmware is capable of running on both versions. License Both the firmware and provided demo and interface scripts are licensed under the GNU General Public License version 3 or later (GPL-3.0-or-later). Note that the firmware includes third party components licensed under their own terms.","title":"Home"},{"location":"#auv-control-board-documentation","text":"AUV Control Board is a low-cost open-source motion controller for (Autonomous) Underwater Vehicles. It is designed for vehicles with fixed position thrusters and supports motion in 6 degrees of freedom. There are currently two versions of control board. Neither version has more features than the other. Version 2 was developed to be able to use a different chip / board that was easier to get. Version 1 uses an Adafruit ItsyBitsy M4 (Microchip ATSAMD51 chip) Version 2 use a WeAct Studio Black Pill (STMicroelectronics STM32F4) The firmware is capable of running on both versions.","title":"AUV Control Board Documentation"},{"location":"#license","text":"Both the firmware and provided demo and interface scripts are licensed under the GNU General Public License version 3 or later (GPL-3.0-or-later). Note that the firmware includes third party components licensed under their own terms.","title":"License"},{"location":"none/","text":"Coming Soon.","title":"None"},{"location":"devs/build/","text":"Build and Flash Firmware Note: Run all commands shown in the firmware folder of the repo. Building Install the Required Tools: CMake (version 3.20.0 or newer) Ninja GNU Arm Toolchain Make sure cmake , ninja , and arm-none-eabi-gcc are in your PATH . Build the firmware using the commands below. Replace [preset] with v1 or v2 (depending on which version you are building firmware for). Replace [config] with debug , release , minsizerel , or relwithdebinfo . cmake --preset=[preset] cmake --build --preset=[preset]-[config] Flashing Note: Control Board v1 Units must update Adafruit's bootloader before first use. This only needs to be done once, but it must be done. Follow Adafruit's instructions here . DO NOT SKIP THIS!!! Version Flash Method Tool Alias Required software v1 sam-ba (via bootloader) bossa BOSSA (specifically bossac / bossa-cli) v1 uf2conv (via bootloader) uf2conv None v2 dfu-util (via bootloader) dfu-util dfu-util v2 STM32CubeProgrammer DFU (via bootloader) stm32-dfu STM32CubeProgrammer Note: required tool ( bossac , dfu-util , STM32_Programmer_CLI ) must be in your PATH . Before flashing, the chip needs to enter its bootloader (unless using a debug probe such as the stlink2 to flash) If a board is already flashed, it can be rebooted into its bootloader using the reboot_bootloader.py script in the firmware directory. Otherwise, use the hardware method described below. v1: Press the reset button twice quickly (double press). v2: Hold the BOOT button. While holding it, press and release the NRST button. Then release the boot button. Note: Sometimes reboot to bootloader mode \"fails\". On v1, this usually means it fails to attach USB (LED remains red not green.) On v2 this usually means it doesn't show up as a USB device. In either case, just try to enter again using the same button combination above. To flash, run the flash.py script. It is a wrapper that will call one of the above tools python3 flash.py [version] [config] -u [tool] [version] is either v1 or v2 [config] is the configuration you want to flash (same as configuration built: Debug , Release , MinSizeRel , or RelWithDebInfo ) [tool] is one of the above upload tool aliases. Flashing Remotely Sometimes, it is useful to flash firmware without connecting directly to the control board. Typically, this is done in-system where the embedded computer using the control board (Jetson, Raspberry Pi, etc) is used to flash the control board without gaining physical access to the control board. Instead an ssh connection to the remote computer is used. There are a few requirements to be able to flash remotely The control board must already be running some version of the firmware. This is necessary to be able to enter bootloader mode without access to the buttons on the board. The remote computer must have a flash tool installed. For v1 this should be bossac and for v2 this should be dfu-util . These are available as packages for most Linux distributions ( bossa-cli and dfu-util respectively for Debian and Ubuntu based systems). You must have ssh (and by extension scp) access to the remote system (typically via ethernet tether) On the build computer (laptop, etc) build the firmware as described above. Then, login to the remote system via ssh and create a directory to hold control board flash stuff (name can be changed as desired) # Run on remote computer (via ssh) cd ~ mkdir cboard-flash Then on the build computer, use scp to copy the flash.py and reboot_bootloader.py scripts to this folder # Run on build laptop scp firmware/flash.py user@remote_ip:cboard-flash/ scp firmware/reboot_bootloader.py user@remote_ip:cboard-flash/ Next copy the build folder. You can just copy the binaries themselves, but the folder hierarchy must be maintained. # Run on remote computer (via ssh) # Delete old build folder first rm -r ~/cboard-flash/build # Run on build laptop scp -r firmware/build user@remote_ip:cboard-flash/ Finally, reboot the control board to bootloader and flash # Run on remote computer (via ssh) ./reboot_bootloader.py [port] ./flash.py [version] [config] -p [port] Note: Sometimes reboot to bootloader mode \"fails\". On v1, this usually means it fails to attach USB (LED remains red not green.) On v2 this usually means it doesn't show up as a USB device. In either case, you will loose USB communication to the board. In this case, a power cycle is required to \"fix\" the board before trying to enter the bootloader again. While inconvenient, it is still usually easier to power cycle the vehicle than to unseal it.","title":"Build and Flash Firmware"},{"location":"devs/build/#build-and-flash-firmware","text":"Note: Run all commands shown in the firmware folder of the repo.","title":"Build and Flash Firmware"},{"location":"devs/build/#building","text":"Install the Required Tools: CMake (version 3.20.0 or newer) Ninja GNU Arm Toolchain Make sure cmake , ninja , and arm-none-eabi-gcc are in your PATH . Build the firmware using the commands below. Replace [preset] with v1 or v2 (depending on which version you are building firmware for). Replace [config] with debug , release , minsizerel , or relwithdebinfo . cmake --preset=[preset] cmake --build --preset=[preset]-[config]","title":"Building"},{"location":"devs/build/#flashing","text":"Note: Control Board v1 Units must update Adafruit's bootloader before first use. This only needs to be done once, but it must be done. Follow Adafruit's instructions here . DO NOT SKIP THIS!!! Version Flash Method Tool Alias Required software v1 sam-ba (via bootloader) bossa BOSSA (specifically bossac / bossa-cli) v1 uf2conv (via bootloader) uf2conv None v2 dfu-util (via bootloader) dfu-util dfu-util v2 STM32CubeProgrammer DFU (via bootloader) stm32-dfu STM32CubeProgrammer Note: required tool ( bossac , dfu-util , STM32_Programmer_CLI ) must be in your PATH . Before flashing, the chip needs to enter its bootloader (unless using a debug probe such as the stlink2 to flash) If a board is already flashed, it can be rebooted into its bootloader using the reboot_bootloader.py script in the firmware directory. Otherwise, use the hardware method described below. v1: Press the reset button twice quickly (double press). v2: Hold the BOOT button. While holding it, press and release the NRST button. Then release the boot button. Note: Sometimes reboot to bootloader mode \"fails\". On v1, this usually means it fails to attach USB (LED remains red not green.) On v2 this usually means it doesn't show up as a USB device. In either case, just try to enter again using the same button combination above. To flash, run the flash.py script. It is a wrapper that will call one of the above tools python3 flash.py [version] [config] -u [tool] [version] is either v1 or v2 [config] is the configuration you want to flash (same as configuration built: Debug , Release , MinSizeRel , or RelWithDebInfo ) [tool] is one of the above upload tool aliases.","title":"Flashing"},{"location":"devs/build/#flashing-remotely","text":"Sometimes, it is useful to flash firmware without connecting directly to the control board. Typically, this is done in-system where the embedded computer using the control board (Jetson, Raspberry Pi, etc) is used to flash the control board without gaining physical access to the control board. Instead an ssh connection to the remote computer is used. There are a few requirements to be able to flash remotely The control board must already be running some version of the firmware. This is necessary to be able to enter bootloader mode without access to the buttons on the board. The remote computer must have a flash tool installed. For v1 this should be bossac and for v2 this should be dfu-util . These are available as packages for most Linux distributions ( bossa-cli and dfu-util respectively for Debian and Ubuntu based systems). You must have ssh (and by extension scp) access to the remote system (typically via ethernet tether) On the build computer (laptop, etc) build the firmware as described above. Then, login to the remote system via ssh and create a directory to hold control board flash stuff (name can be changed as desired) # Run on remote computer (via ssh) cd ~ mkdir cboard-flash Then on the build computer, use scp to copy the flash.py and reboot_bootloader.py scripts to this folder # Run on build laptop scp firmware/flash.py user@remote_ip:cboard-flash/ scp firmware/reboot_bootloader.py user@remote_ip:cboard-flash/ Next copy the build folder. You can just copy the binaries themselves, but the folder hierarchy must be maintained. # Run on remote computer (via ssh) # Delete old build folder first rm -r ~/cboard-flash/build # Run on build laptop scp -r firmware/build user@remote_ip:cboard-flash/ Finally, reboot the control board to bootloader and flash # Run on remote computer (via ssh) ./reboot_bootloader.py [port] ./flash.py [version] [config] -p [port] Note: Sometimes reboot to bootloader mode \"fails\". On v1, this usually means it fails to attach USB (LED remains red not green.) On v2 this usually means it doesn't show up as a USB device. In either case, you will loose USB communication to the board. In this case, a power cycle is required to \"fix\" the board before trying to enter the bootloader again. While inconvenient, it is still usually easier to power cycle the vehicle than to unseal it.","title":"Flashing Remotely"},{"location":"devs/buildsimcb/","text":"Build and Run SimCB Install the Required Tools: CMake (version 3.20.0 or newer) Ninja C Toolchain for your system MSVC on windows Apple Clang on macOS GNU Toolchain (GCC) on Linux Make sure cmake and ninja are in your PATH . Build the firmware using the commands below. Replace [preset] with simcb-win , simcb-macos , or simcb-linux . Replace [config] with debug , release , minsizerel , or relwithdebinfo . cmake --preset=[preset] cmake --build --preset=[preset]-[config] The SimCB binary will exist in build/[preset]/[config]/SimCB[.exe]","title":"Build and Run SimCB"},{"location":"devs/buildsimcb/#build-and-run-simcb","text":"Install the Required Tools: CMake (version 3.20.0 or newer) Ninja C Toolchain for your system MSVC on windows Apple Clang on macOS GNU Toolchain (GCC) on Linux Make sure cmake and ninja are in your PATH . Build the firmware using the commands below. Replace [preset] with simcb-win , simcb-macos , or simcb-linux . Replace [config] with debug , release , minsizerel , or relwithdebinfo . cmake --preset=[preset] cmake --build --preset=[preset]-[config] The SimCB binary will exist in build/[preset]/[config]/SimCB[.exe]","title":"Build and Run SimCB"},{"location":"devs/details/","text":"Implementation Details TODO: EEPROM Emulation SAMD51 (CBv1) Using NVMCTRL SmartEEPROM Must configure size using NVMCTRL user page (requires write of fuses / config bits then device reset; XC32 compiler uses pragma config to configur this. I see no other way with GCC) Note that as such, configuring fuse settings with MCC Standalone generator will do no good (it assumes XC32) Linker script modified to shorten rom to avoid the data for eeprom STM32 (CBv2) Using code from https://github.com/STMicroelectronics/STM32CubeF4/tree/master/Projects/STM32F411RE-Nucleo/Applications/EEPROM/EEPROM_Emulation as st_eeprom.h/c Using sectors 1 and 2 isr_vectors must be in sector 0, most of this sector is wasted Flash used by program starts at sector 3. Leaves 464k flash for use. Note that since flash is split, flashing must occur in two stages (boot and main) using dfu-util TODO: Threading model mutexes, dataflow details TODO: Generator use and import process details TODO: Calibration of BNO055 (described from code perspective - see calibration page for user facing docs) EEPROM calibration status signature for valid check Erase only invalidates signature (potentially fewer writes) If calibration constants are valid, they are applied during bno055 configuration. Applying constants will prevent BNO055 from running calibration routine in bg. Applied calibration is assumed to be good by the sensor (CALIB_STAT has no relevance). Stored calibration commands Erase triggers reset of sensor causing constants to be lost and auto calibration routine to run Store triggers reset of sensor using the newly stored constants (thus no auto calibration runs) Load will send the stuff from eeprom There are separate BNO055 commands to read its calibration status register and the values from its calibration registers (these are only valid if CALIB_STAT is 3 for sensors used and BNO055 is in config mode) What are each of the calibration constants (offsets, radius)","title":"Firmware Details"},{"location":"devs/details/#implementation-details","text":"TODO: EEPROM Emulation SAMD51 (CBv1) Using NVMCTRL SmartEEPROM Must configure size using NVMCTRL user page (requires write of fuses / config bits then device reset; XC32 compiler uses pragma config to configur this. I see no other way with GCC) Note that as such, configuring fuse settings with MCC Standalone generator will do no good (it assumes XC32) Linker script modified to shorten rom to avoid the data for eeprom STM32 (CBv2) Using code from https://github.com/STMicroelectronics/STM32CubeF4/tree/master/Projects/STM32F411RE-Nucleo/Applications/EEPROM/EEPROM_Emulation as st_eeprom.h/c Using sectors 1 and 2 isr_vectors must be in sector 0, most of this sector is wasted Flash used by program starts at sector 3. Leaves 464k flash for use. Note that since flash is split, flashing must occur in two stages (boot and main) using dfu-util TODO: Threading model mutexes, dataflow details TODO: Generator use and import process details TODO: Calibration of BNO055 (described from code perspective - see calibration page for user facing docs) EEPROM calibration status signature for valid check Erase only invalidates signature (potentially fewer writes) If calibration constants are valid, they are applied during bno055 configuration. Applying constants will prevent BNO055 from running calibration routine in bg. Applied calibration is assumed to be good by the sensor (CALIB_STAT has no relevance). Stored calibration commands Erase triggers reset of sensor causing constants to be lost and auto calibration routine to run Store triggers reset of sensor using the newly stored constants (thus no auto calibration runs) Load will send the stuff from eeprom There are separate BNO055 commands to read its calibration status register and the values from its calibration registers (these are only valid if CALIB_STAT is 3 for sensors used and BNO055 is in config mode) What are each of the calibration constants (offsets, radius)","title":"Implementation Details"},{"location":"devs/developing/","text":"Firmware Development VSCode Setup Often, it is useful to have some sort of code editor / IDE setup. The project is already setup to work easily with Visual Studio Code (VSCode), provided a few extensions are installed. After installing VSCode Install the C/C++ and CMake Tools , and Cortex-Debug extensions for working with the firmware ( firmware/ subfolder) Install the Python extension for working with the interface scripts ( iface/ subfolder) To use, just open one of the folders (eg firmware/ or iface/ ) in VSCode. The useful extensions should automatically load when you open the folders (you may have to open a source code file in some cases). For debugging, a launch.json file is included in the firmware/ project (in .vscode subfolder). This defines debug configurations both for firmware running on physical hardware and for the supported SimCB configurations. Note: Launching a debug session from VSCode will not build the firmware. Make sure to build the \"Debug\" configuration before launching! Debugging Firmware Debugging on Physical System This section discusses debugging of the firmware running on actual hardware (Control Board v1 or v2). Debugging firmware running on a microcontroller requires a \"debug probe\". The supported probes for each version of the control board are listed in the table below. ControlBoard Version Debugger / Debug Probe Config Name in tools/debug v1 CMSIS-DAP* cb_v1_via_cmsisdap.cfg v2 ST-LINK v2 cb_v2_via_stlink2.cfg *The PicoProbe firmware can be used to turn a low cost Raspberry Pi Pico board into a CMSIS-DAP compliant debug probe. You must wire the debug probe to the microcontroller on the control board correctly. Both v1 and v2 use a SWD debug interface. Examples of wiring the debug probes to each are shown below. TODO: Wiring diagrams for v1 and v2 Note: NEVER power the system from both the debug probe and USB at the same time. Generally, power the system over USB and do not connect the power (3.3V) from the debug probe. Signals and GND must be connected from the debug probe. Make sure that OpenOCD must be installed and in the PATH . On Linux systems, you will likely also need to install gdb-multiarch and symlink it to arm-none-eabi-gdb . On ubuntu, this is done with the following commands sudo apt install gdb-multiarch sudo ln -s `which gdb-multiarch` /usr/local/bin/arm-none-eabi-gdb Then, use VSCode to run the correct debug session for the hardware version and debug probe you are using. Alternatively, you may use openocd and gdb directly if you are not using vscode. The config files listed in the table above are just OpenOCD configs. Debugging SimCB For windows, you can attach the windows debugger without any issues / modifications. For macos / linux (using FreeRTOS posix port) the debugger must have some additional configuration to avoid impacting signals used by the port to facilitate context switches. LLDB (macOS): process handle SIGUSR1 --notify false --pass true --stop false process handle SIGALRM --notify false --pass true --stop false GDB (Linux): handle SIGUSR1 nostop noignore noprint handle SIGALRM nostop noignore noprint Generator Projects Note: All paths in this section are relative to the firmware/ subdirectory. Each version of ControlBoard has an associated \"Generator\" project. These are projects used with the chip manufacturer's tools to generate startup / configuration / library code for the chip. You will only need to modify the generator projects to alter configuration of the chip / components of included libraries from these tools. The firmware copies code generated from these tools into the thridparty folder, thus you don't need the generators to modify the firmware, unless you are modifying generated components. Running the Generator(s) Control Board v1 Control Board v1 uses an Adafruit ItsyBitsy M4 board (Microchip ATSAMD51G19A chip). The generator used for this project is MCC Standalone . This tool is available for download on Windows, macOS, and Linux. Tested with v5.2.1 of MCC Standalone. Once installed Launch the application Make sure the Harmony Content Path is set in Tools > Options . This should only need to be done once per computer. Recommended path is ~/.mcc/harmony/v3 . On windows, use C:\\Users\\[YOUR_USERNAME]\\.mcc\\harmony\\v3 (replace [YOUR_USERNAME] ). File > Load Configuration Choose generator_projects/ControlBoard_v1/firmware/ControlBoard_v1/ControlBoard_v1.mc3 You will likely be prompted to install MPLAB Harmony content. Install it. Click \"Generate\" in the top left. Control Board v2 Control Board v2 uses a WeAct Studio Black Pill board (STMicro STM32F411CEU chip). The generator used for this project is STM32CubeMX . This tool is available for download on Windows, macOS, and Linux. Tested with v6.6.1 of STM32CubeMX. Once installed Launch the application File > Load Project Choose generator_projects/ControlBoard_v2/ControlBoard_v2.ioc Install any required packs (as prompted) Click \"Generate Code\" in the top right. Importing from Generators After running the generator (as described above) the generated code must be imported to the project. The import process is mostly just copying generated files, however some files are modified slightly. The import process is handled by the import_from_generator.py script. If additional components are added in the generator projects, this script may need to be modified to import additional components. When this script is run, it will prompt a version of control board to import generated code for. This script must be run after each time the generator project is modified and code is re-generated. To run the importer, run python3 import_from_generator.py then choose which project your are importing. Firmware Development using the Simulator The simulator can be very helpful for developing or debugging firmware. The simulator is capable of connecting to a real control board or SimCB binaries. Thus the firmware can be tested in a simulation environment to validate math or behavior of a vehicle. Testing with the simulator can also be done while the control board firmware is running with a debugger attached (true for both a real control board and SimCB). Testing firmware on a physical control board (v1, v2) with the simulator is fairly simple. It is assumed that you will want the debugger running too: Connect the control board to your PC via USB Connect a debug probe to the control board (do NOT connect power) Build and run the firmware under debugger In the simulator, choose the UART port for the control board Interface scripts can be run as usual using launch.py , but add the -s flag so it will connect to the simulator instead of the physical control board. If you want to debug SimCB using the simulated environment TODO: Requires more work on simulator UI! NYI! There are a few things to know about running under simulation (these are true when the firmware is hijacked by simulator) Sensor drivers are disabled while under simulator control. Additionally, the active sensors will always be reported as the \"SIM\" sensors. This is true even if a real sensor is connected to a physical control board under simulation. When controlled by the simulator, the control board will not generate PWM signals on thruster pins. The thruster pins will maintain a pulse corresponding to no motion (or no pulse at all if no thruster pwm parameters have been applied). Developing Python Interface Scripts TODO","title":"Development Workflows"},{"location":"devs/developing/#firmware-development","text":"","title":"Firmware Development"},{"location":"devs/developing/#vscode-setup","text":"Often, it is useful to have some sort of code editor / IDE setup. The project is already setup to work easily with Visual Studio Code (VSCode), provided a few extensions are installed. After installing VSCode Install the C/C++ and CMake Tools , and Cortex-Debug extensions for working with the firmware ( firmware/ subfolder) Install the Python extension for working with the interface scripts ( iface/ subfolder) To use, just open one of the folders (eg firmware/ or iface/ ) in VSCode. The useful extensions should automatically load when you open the folders (you may have to open a source code file in some cases). For debugging, a launch.json file is included in the firmware/ project (in .vscode subfolder). This defines debug configurations both for firmware running on physical hardware and for the supported SimCB configurations. Note: Launching a debug session from VSCode will not build the firmware. Make sure to build the \"Debug\" configuration before launching!","title":"VSCode Setup"},{"location":"devs/developing/#debugging-firmware","text":"","title":"Debugging Firmware"},{"location":"devs/developing/#debugging-on-physical-system","text":"This section discusses debugging of the firmware running on actual hardware (Control Board v1 or v2). Debugging firmware running on a microcontroller requires a \"debug probe\". The supported probes for each version of the control board are listed in the table below. ControlBoard Version Debugger / Debug Probe Config Name in tools/debug v1 CMSIS-DAP* cb_v1_via_cmsisdap.cfg v2 ST-LINK v2 cb_v2_via_stlink2.cfg *The PicoProbe firmware can be used to turn a low cost Raspberry Pi Pico board into a CMSIS-DAP compliant debug probe. You must wire the debug probe to the microcontroller on the control board correctly. Both v1 and v2 use a SWD debug interface. Examples of wiring the debug probes to each are shown below. TODO: Wiring diagrams for v1 and v2 Note: NEVER power the system from both the debug probe and USB at the same time. Generally, power the system over USB and do not connect the power (3.3V) from the debug probe. Signals and GND must be connected from the debug probe. Make sure that OpenOCD must be installed and in the PATH . On Linux systems, you will likely also need to install gdb-multiarch and symlink it to arm-none-eabi-gdb . On ubuntu, this is done with the following commands sudo apt install gdb-multiarch sudo ln -s `which gdb-multiarch` /usr/local/bin/arm-none-eabi-gdb Then, use VSCode to run the correct debug session for the hardware version and debug probe you are using. Alternatively, you may use openocd and gdb directly if you are not using vscode. The config files listed in the table above are just OpenOCD configs.","title":"Debugging on Physical System"},{"location":"devs/developing/#debugging-simcb","text":"For windows, you can attach the windows debugger without any issues / modifications. For macos / linux (using FreeRTOS posix port) the debugger must have some additional configuration to avoid impacting signals used by the port to facilitate context switches. LLDB (macOS): process handle SIGUSR1 --notify false --pass true --stop false process handle SIGALRM --notify false --pass true --stop false GDB (Linux): handle SIGUSR1 nostop noignore noprint handle SIGALRM nostop noignore noprint","title":"Debugging SimCB"},{"location":"devs/developing/#generator-projects","text":"Note: All paths in this section are relative to the firmware/ subdirectory. Each version of ControlBoard has an associated \"Generator\" project. These are projects used with the chip manufacturer's tools to generate startup / configuration / library code for the chip. You will only need to modify the generator projects to alter configuration of the chip / components of included libraries from these tools. The firmware copies code generated from these tools into the thridparty folder, thus you don't need the generators to modify the firmware, unless you are modifying generated components.","title":"Generator Projects"},{"location":"devs/developing/#running-the-generators","text":"","title":"Running the Generator(s)"},{"location":"devs/developing/#control-board-v1","text":"Control Board v1 uses an Adafruit ItsyBitsy M4 board (Microchip ATSAMD51G19A chip). The generator used for this project is MCC Standalone . This tool is available for download on Windows, macOS, and Linux. Tested with v5.2.1 of MCC Standalone. Once installed Launch the application Make sure the Harmony Content Path is set in Tools > Options . This should only need to be done once per computer. Recommended path is ~/.mcc/harmony/v3 . On windows, use C:\\Users\\[YOUR_USERNAME]\\.mcc\\harmony\\v3 (replace [YOUR_USERNAME] ). File > Load Configuration Choose generator_projects/ControlBoard_v1/firmware/ControlBoard_v1/ControlBoard_v1.mc3 You will likely be prompted to install MPLAB Harmony content. Install it. Click \"Generate\" in the top left.","title":"Control Board v1"},{"location":"devs/developing/#control-board-v2","text":"Control Board v2 uses a WeAct Studio Black Pill board (STMicro STM32F411CEU chip). The generator used for this project is STM32CubeMX . This tool is available for download on Windows, macOS, and Linux. Tested with v6.6.1 of STM32CubeMX. Once installed Launch the application File > Load Project Choose generator_projects/ControlBoard_v2/ControlBoard_v2.ioc Install any required packs (as prompted) Click \"Generate Code\" in the top right.","title":"Control Board v2"},{"location":"devs/developing/#importing-from-generators","text":"After running the generator (as described above) the generated code must be imported to the project. The import process is mostly just copying generated files, however some files are modified slightly. The import process is handled by the import_from_generator.py script. If additional components are added in the generator projects, this script may need to be modified to import additional components. When this script is run, it will prompt a version of control board to import generated code for. This script must be run after each time the generator project is modified and code is re-generated. To run the importer, run python3 import_from_generator.py then choose which project your are importing.","title":"Importing from Generators"},{"location":"devs/developing/#firmware-development-using-the-simulator","text":"The simulator can be very helpful for developing or debugging firmware. The simulator is capable of connecting to a real control board or SimCB binaries. Thus the firmware can be tested in a simulation environment to validate math or behavior of a vehicle. Testing with the simulator can also be done while the control board firmware is running with a debugger attached (true for both a real control board and SimCB). Testing firmware on a physical control board (v1, v2) with the simulator is fairly simple. It is assumed that you will want the debugger running too: Connect the control board to your PC via USB Connect a debug probe to the control board (do NOT connect power) Build and run the firmware under debugger In the simulator, choose the UART port for the control board Interface scripts can be run as usual using launch.py , but add the -s flag so it will connect to the simulator instead of the physical control board. If you want to debug SimCB using the simulated environment TODO: Requires more work on simulator UI! NYI! There are a few things to know about running under simulation (these are true when the firmware is hijacked by simulator) Sensor drivers are disabled while under simulator control. Additionally, the active sensors will always be reported as the \"SIM\" sensors. This is true even if a real sensor is connected to a physical control board under simulation. When controlled by the simulator, the control board will not generate PWM signals on thruster pins. The thruster pins will maintain a pulse corresponding to no motion (or no pulse at all if no thruster pwm parameters have been applied).","title":"Firmware Development using the Simulator"},{"location":"devs/developing/#developing-python-interface-scripts","text":"TODO","title":"Developing Python Interface Scripts"},{"location":"devs/math/","text":"Math Coordinate System Definition The control board uses a coordinate system that is somewhat non-standard. The coordinate system is right handed +y is forward, +z is up, +x is right Pitch is about x, roll is about y, yaw is about z While this coordinate system may seem strange to some (especially those who have worked with aircraft), the name of axes doesn't really matter. The definition of pitch, roll, and yaw relative to the front / top of the vehicle remain standard. 6 Degree of Freedom Motion Control Nomenclature, Notation, and Convention Notes Matrices are assumed to be zero indexed (not 1 indexed). This means the first element of a matrix \\(M\\) is \\(m_{00}\\) not \\(m_{11}\\) . This is because the math will be implemented in C (which uses zero indexed arrays). Thruster numbers (1-8) are used by the control board's user facing components. However, this math will use thruster indices (0-7) where index = number - 1 . This is again because arrays in C are zero indexed. Rotations about axes (angular velocities / DoFs) are referred to as \"xrot\" (about x), \"yrot\" (about y), and \"zrot\" (about z). The terms \"pitch\", \"roll\", and \"yaw\" are used to describe the vehicle's orientation in space. These are a set of intrinsic Euler angles (Tait-Bryan angles to be precise) composed in the order yaw then pitch then roll (z-x'-y'' in the control board's coordinate system). Note that while these do follow the standard meanings of pitch, roll, and yaw relative to the vehicle, the axes have non-standard names. Thus, you cannot simply use standard formulas for converting between quaternions and pitch, roll, yaw. Velocities in DoFs are normalized (meaning -1.0 to 1.0). Euler angles are represented as a set of three values \\(\\begin{pmatrix} pitch & roll & yaw\\end{pmatrix}\\) Quaternions are represented either as a combination of a scalar component, \\(s\\) , and a vector component, \\(v\\) as \\(\\{s,v\\}\\) or they may be represented using the names \\(w\\) , \\(x\\) , \\(y\\) , and \\(z\\) . In such a case this is equivelent to \\(\\{w, (x, y, z)\\}\\) . In other words, \\(x\\) , \\(y\\) , and \\(z\\) are the components of the vector \\(v\\) and \\(w=s\\) . The conjugate of a quaternion, \\(q\\) is denoted \\(q^*\\) System Assumptions Vehicle is capable of motion exclusively in each of 6 degrees of freedom (DoFs). These are three translational DoFs, and three rotational DoFs. The vehicle's speed in positive and negative directions are roughly equal for each DoF. Thruster orientations are fixed. Gimbaled thruster vehicles are not supported. At most 8 thrusters (less is fine) System has 3D orientation information System has depth information System does not have translational position information Example Vehicle The examples on this page will use AquaPack robotics's SeaWolf VIII robot . This robot's thruster configuration is as shown below. The arrows indicate the direction the thruster moves water when powered in the positive direction. These arrows are opposite the direction force is excerpted on the vehicle. Note that the diagram above uses thruster numbers, not indices. DoF Matrix The DoF Matrix , \\(D\\) , is constructed based on the vehicle's thruster configuration. Rows of the matrix correspond to thrusters (by index). And columns of the matrix correspond to vehicle-relative DoFs. Thus, this is an 8x6 matrix (8 thrusters, 6 DoFs). Columns correspond to DoFs in the following order (0-5): x, y, z, xrot, yrot, zrot. \\(D = \\left(\\begin{array}{c|c|c|c|c|c} d_0 & d_1 & d_2 & d_3 & d_4 & d_5 \\end{array}\\right) = \\left(\\begin{array}{c|c|c|c|c|c} d_x & d_y & d_z & d_{xr} & d_{yr} & d_{zr} \\end{array}\\right)\\) Each column of the DoF matrix, \\(d_i\\) is a set of thruster speeds that result in motion exclusively in the column's DoF. Additionally, the resultant motion should be the maximum possible speed, and in the positive direction. Each \\(d_i\\) is an 8 element column vector, with elements corresponding to thrusters (by index). All speeds should be normalized (between -1.0 and 1.0) For the example vehicle shown above, the following is the DoF matrix \\(D = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix}\\) Consider the first column: \\(d_0 = d_x\\) . This column's thruster speeds should result in the vehicle moving as fast as possible in the +x direction (only). This is achieved by setting T2, T4 (index 1, 3) to the positive direction and T1, T3 (index 0, 2) to the negative direction at full speed (recall that the arrows are opposite the direction the thruster excerpts force on the vehicle). Thus \\(d_x = \\begin{pmatrix}-1 & +1 & -1 & +1 & 0 & 0 & 0 & 0\\end{pmatrix}^T\\) Note that when constructing the DoF matrix for your vehicle, you should assume an ideal system and environment (ignore variation between thrusters, environmental factors that create motion, etc). LOCAL Mode Motion In LOCAL mode, motion is specified as a set of speeds in vehicle-relative DoFs. The user provides the control board with a local target motion vector ( \\(t_l\\) ) where each element corresponds to a DoF. \\(t_l = \\begin{pmatrix} x & y & z & r_x & r_y & r_z \\end{pmatrix}^T\\) \\(x\\) is normalized velocity in +x direction \\(y\\) is normalized velocity in +y direction \\(z\\) is normalized velocity in +y direction \\(r_x\\) is normalized angular velocity about the +x axis direction (xrot) \\(r_y\\) is normalized angular velocity about the +y axis (yrot) \\(r_z\\) is normalized angular velocity about the +z axis (zrot) By multiplying this target motion by the DoF matrix, \\(D\\) , a speed vector \\(s\\) is obtained where each element of \\(s\\) corresponds to a specific thruster (by index). \\(s = D t_l\\) Consider the example where \\(t_l = \\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 0\\end{pmatrix}^T\\) . This should cause the vehicle to move at full possible speed forward (relative to the vehicle's orientation). \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix} = \\begin{pmatrix}-1 \\\\ -1 \\\\ +1 \\\\ +1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix}\\) In the above example, it is trivial to see that this is the desired motion. However for a more complex example, a problem appears. Consider \\(t_l = \\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 1\\end{pmatrix}^T\\) . This describes the vehicle both moving forward and about the z-axis at full possible speed. \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 1\\end{pmatrix} = \\begin{pmatrix}0 \\\\ -2 \\\\ 0 \\\\ +2 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix}\\) Notice that the resultant speed vector has motors moving in excess of 100% speed (elements with magnitude greater than 1.0). This is not possible. The simple solution would seem to be dividing all elements of the vector by the one with the largest magnitude. This results in a scaled speed vector \\(\\hat{s}\\) \\(\\hat{s} = s \\div \\text{absmax}(s)\\) However, this method will not work well in all cases. Consider \\(t_l = \\begin{pmatrix}0 & 1 & 1 & 1 & 1 & 1\\end{pmatrix}^T\\) . \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 1 \\\\ 1 \\\\ 1 \\\\ 1\\end{pmatrix} = \\begin{pmatrix}0 \\\\ -2 \\\\ 0 \\\\ +2 \\\\ -3 \\\\ -1 \\\\ -1 \\\\ +1\\end{pmatrix}\\) and \\(\\hat{s} = s \\div \\text{absmax}(s) = s \\div 3 = \\begin{pmatrix}0 \\\\ -0.67 \\\\ 0 \\\\ +0.67 \\\\ -1 \\\\ -0.33 \\\\ -0.33 \\\\ +0.33\\end{pmatrix}\\) While this has resulted in an possible set of thruster speeds, these are not optimal. Look at the example vehicle diagram. Notice that thrusters 1-4 and 5-8 control different motions. In the previous example, thrusters 1-4 were slowed down more than necessary, because thruster 5 was too large of a value. This is not ideal as the vehicle's maximum speed becomes artificially limited. Instead, the following \\(\\hat{s}\\) is ideal. This is scaling down the thrusters within each group (1-4 and 5-8) separately. \\(\\hat{s} = \\begin{pmatrix}0 \\\\ -1 \\\\ 0 \\\\ +1 \\\\ -1 \\\\ -0.33 \\\\ -0.33 \\\\ +0.33\\end{pmatrix}\\) Groupings of thrusters on the example vehicle are easy to observe, however this is not always true. Thus, achieving optimal scaling for any system (any DoF matrix) requires a more sophisticated method to determine groupings and scale speeds. Thruster groupings are determined by \"overlap\" between thrusters. Two thrusters, \\(i\\) and \\(j\\) are said to overlap if they have a non-zero entry in the same column of the DoF matrix ( \\(D\\) ) for at least one column. This is easier to calculate using a contribution matrix , \\(C\\) , defined as \\(D \\neq 0\\) . This results in a binary form of the DoF matrix. For the above example \\(C = \\left[\\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\neq 0 \\right] = \\begin{pmatrix} 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ \\end{pmatrix}\\) Then for each thruster \\(i\\) an overlap vector \\(o_i\\) can be constructed as follows \\(o_i = C (c^i)^T\\) where \\(c^i\\) is the \\(i\\) th row of \\(C\\) . Thus, \\(o_i\\) is an 8 element vector where each element corresponds to a thruster (by index). Element \\(j\\) of \\(o_i\\) can either be \\(1\\) or a \\(0\\) . A \\(1\\) indicates that thrusters \\(i\\) and \\(j\\) overlap. For example, \\(o_0 = C (c^0)^T = \\begin{pmatrix} 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 \\end{pmatrix}^T\\) This shows that thruster index 0 (T1) overlaps with indices 0, 1, 2, and 3 (T1, T2, T3, T4). By calculating and storing these overlap vectors for each thruster ( \\(\\left\\{o_i\\right\\}_{i=0}^7\\) ), this effectively forms a lookup table to determine thruster overlap. While this is not the most memory efficient option, it reduces computation time, which is important since this will run very frequently on a microcontroller. Using overlap vectors, the following algorithm can be used to scale motor speeds: Find the thruster ( i ) with the largest magnitude speed Iterate over thruster i 's overlap vector For any thruster, j , with which thruster i overlaps, divide thruster j 's speed by the magnitude of thruster i 's speed Repeat until the largest magnitude does not exceed 1.0 while true // m is value, i is index m, i = max(abs(speed_vector)) if m <= 1.0 // Done scaling break endif // Iterate over all thrusters (0-7 inclusive) for j=0...7 if overlap_vector[i][j] == 1 // i and j overlap. Divide j's speed by m. speed_vector[j] /= m endif endfor endwhile This algorithm results in optimal speed scaling by only reducing the speed of thrusters that share DoF contributions. GLOBAL Mode Motion GLOBAL mode is very similar to LOCAL mode, however, motion is described partially relative to the world instead of the robot. Specifically, motion of the vehicle is compensated for vehicle pitch and roll (but not yaw). This results in a coordinate system defined by the axes gx , gy , and gz . Note that the world coordinate system is defined as wx , wy , wz . Notably, if the vehicle is pitched 180 degrees, the gx-gy plane becomes aligned to the back of the vehicle ensuring consistent motion. This is shown in the animation below. In GLOBAL mode, the user provides the control board with a global target motion vector , \\(t_g\\) with 6 elements. This target motion vector is a concatenation of two 3 dimensional vectors. The first, a set of translations along gx , gy , and gz . Second a set of rotations to affect vehicle pitch, roll, and yaw. These are referred to by the following names x : Speed in gx direction (translation) y : Speed in gy direction (translation) z : Speed in gz direction (translation) p : Speed at which the vehicle's pitch should increase (negative for decrease pitch). Aka \"pitch_spd\" r : Speed at which the vehicle's roll should increase (negative for decrease pitch). Aka \"roll_spd\" h : Speed at which the vehicle's yaw should increase (negative for decrease pitch). Aka \"yaw_spd\" \\(t_g = \\begin{pmatrix} x & y & z & p & r & h \\end{pmatrix}\\) It is necessary to transform each DoF's motion into motions in the vehicle's DoFs. These speeds can then be passed to LOCAL mode. WARNING: GLOBAL mode is impacted by gimbal lock issues with euler angles. This occurs when the vehicle's pitch is +/- 90 degrees. In this scenario the meaning of \"increase / decrease pitch\" is ambiguous. The vehicle will take the zero-roll route in this scenario (an arbitrary choice based on how euler angle conversion is implemented in the firmware). Thus, GLOBAL mode may produce undesirable motion if the roll is non-zero and you pitch through +/- 90. A potential solution for this could be some form of motion hysteresis to handle moving through gimbal lock orientations, however this is not implemented as of now. Translation DoFs The translation DoFs are easily transformed using gravity vectors. By applying a quaternion based rotation matrix to the base gravity vector, \\(g_b = \\begin{pmatrix}0 & 0 & -1\\end{pmatrix}\\) , the following solution is determined for the current gravity vector, \\(g_c\\) given the vehicle's orientation quaternion, \\(q\\) . \\(\\begin{pmatrix} 2*(q.x*q.z+q.w*q.y) \\\\ 2*(q.w*q.x-q.y*q.z) \\\\ -(q.w)^2+(q.x)^2+(q.y)^2-(q.z)^2\\end{pmatrix}^T\\) The minimal rotation from \\(g_b\\) to \\(g_c\\) is then calculated. Let this rotation be called \\(q_{rot}\\) . This rotation will generally include no yaw component, unless the vehicle is upside down and facing backwards (eg pitch of 180 degrees) in which case it will contain a yaw component of 180 degrees. This is desirable as it ensures a continuous definition of what gy is even while the vehicle is flipping via pitch. This quaternion can then be applied to speeds in the gx , gy , gz basis ( x , y , and z here) to rotate them onto the vehicle basis. Thus, this \"converts\" translation speeds from GLOBAL to LOCAL mode DoFs. However, it is not ideal to transform the translation vector all at once. It is best to do it in three stages to allow proper upscaling as needed (explained below). Thus for each global mode translation vector \\(\\begin{pmatrix} x & 0 & 0 \\end{pmatrix}\\) , \\(\\begin{pmatrix} 0 & y & 0 \\end{pmatrix}\\) , and \\(\\begin{pmatrix} 0 & 0 & z \\end{pmatrix}\\) rotate it by \\(q_{rot}\\) to obtain \\(t_x\\) , \\(t_y\\) , and \\(t_z\\) respectively (note that \\(\\left\\{s, v\\right\\}\\) is a quaternion with scalar s and vector v). \\(\\left\\{0, t_x\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} x & 0 & 0 \\end{pmatrix} \\right\\} q_{rot}^*\\) \\(\\left\\{0, t_y\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} 0 & y & 0 \\end{pmatrix} \\right\\} q_{rot}^*\\) \\(\\left\\{0, t_z\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} 0 & 0 & z \\end{pmatrix} \\right\\} q_{rot}^*\\) Each of \\(t_x\\) , \\(t_y\\) , and \\(t_z\\) are speeds in LOCAL mode DoFs, however they may be slower than desired. Consider a speed of 1.0 along gy . If the vehicle were rotated 45 degrees (pitch) the resultant \\(t_y\\) would be \\(t_y = \\begin{pmatrix} 0.0 & 0.7071 & 0.7071 \\end{pmatrix}\\) . This is not as fast as possible in the correct direction. Instead \\(t_y\\) should be \\(t_y = \\begin{pmatrix} 0.0 & 1.0 & 1.0 \\end{pmatrix}\\) . In other words, the largest element of \\(t_y\\) should be the speed along gy . Thus, each of \\(t_x\\) , \\(t_y\\) , \\(t_z\\) needs to be upscaled (note that it will never need to be downscaled; it only needs to be upscaled because one DoF may now be spread between multiple). let \\(m\\) be the magnitude of the element of \\(t_x\\) with the largest magnitude Normalize \\(t_x\\) so that largest element is 1.0: \\(t_x = t_x / m\\) Scale normalized \\(t_x\\) by speed \\(x\\) (gx speed): \\(t_x = t_x * x\\) Repeat this for \\(t_y\\) and \\(t_z\\) using \\(y\\) and \\(z\\) speeds respectively. Once all three \\(t\\) vectors are scaled, they can be combined to create the net LOCAL mode translation vector, \\(l\\) \\(l = t_x + t_y + t_z\\) There are two potential issues with \\(l\\) \\(l\\) is a set of proportionally related speeds to result in the desired motion. However, the vehicle may not be capable of the same speeds in each of it's DoFs. Thus, the ratios between \\(l\\) 's elements may be incorrect. \\(l\\) is a sum of three vectors (each with elements no larger than a magnitude of 1), thus it may have elements with a magnitude greater than 1. Issue 1 should be handled first as correcting it may \"fix\" issue 2. Handling issue 2 first could result in downscaling speeds more than necessary. Handling issue 1 requires the user to provide a little more information about the vehicle: relative speeds in each DoF. These can be used to calculate downscaling factors to slow down the faster directions (note: speeding up the slower directions could result in impossible speeds, but would be handled by solving issue 2; regardless it is less ideal). These downscaling factors are calculated from \"RELDOF\" information provided by the user (see messages page of user guide). This information is a set of constants, which will be referred to the as the scale factors as \\(m_x\\) , \\(m_y\\) , \\(m_z\\) , \\(m_{rx}\\) , \\(m_{ry}\\) , and \\(m_{rz}\\) for the x, y, z, xrot, yrot, and zrot DoFs respectively (note that these are vehicle DoFs). These scale factors makeup two groups: translational ( \\(m_x\\) , \\(m_y\\) , and \\(m_z\\) ) and rotational ( \\(m_{rx}\\) , \\(m_{ry}\\) , and \\(m_{rz}\\) ). Within each group, the fastest DoF's scale factor is set to 1. The other scale factors are a percentage of the fastest DoF in the group. For example, if the vehicle can move at 2m/s along z, 1m/s along y, and 0.5m/s along x then \\(m_x = 0.25\\) , \\(m_y = 0.5\\) , and \\(m_z = 1.0\\) . Given these scale factors, the simplest option would be to let \\(l = \\begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\\end{pmatrix}\\) . However, this may downscale more than necessary. Consider a scenario where the speed vector \\(l\\) has a \\(0\\) in the position of the slowest DoF of the vehicle (eg if the vehicle is slowest in the x direction, the \\(l\\) vector would have a zero in the x position). In this case, since the slowest direction is unused, we are downscaling too much. Thus, the following algorithm is used to select the ideal downscaling factors by \"ignoring\" the downscaling required for unused DoFs (DoFs with a speed of 0). // Zero downscale factors for unused DoFs if abs(l.x) == 0 m_x = 0 endif if abs(l.y) == 0 m_y = 0 endif if abs(l.z) == 0 m_z = 0 endif // Rebalance scale factors so largest remaining is 1.0 m_max = max(m_x, max(m_y, m_z)); m_x = m_x / m_max; m_y = m_y / m_max; m_z = m_z / m_max; \\(l\\) is then downscaled as \\(l = \\begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\\end{pmatrix}\\) using the scale factors calculated using the above algorithm. Finally, issue 2 must be handled if any element of \\(l\\) still has a magnitude greater than 1.0. Thus, Let \\(m\\) be the magnitude of the element of \\(l\\) with the largest magnitude If \\(m\\) is less than or equal to 1, do not change $l If \\(m\\) is greater than 1 \\(l = l / m\\) This resultant \\(l\\) is a set of speeds that can be passed to LOCAL mode as it's x , y , and z speeds. Rotation DoFs: Converting the GLOBAL mode rotations (increase/decrease pitch, roll, yaw) to motions about DoFs is a little harder. It requires decomposing the quaternion into euler angles, then calculating three quaternions describing one euler rotation each. In other words, given the vehicle's current rotation \\(q\\) we need to find \\(q_{pitch}\\) , \\(q_{roll}\\) and \\(q_{yaw}\\) such that (based on the euler angle convention used by the control board) \\(q = q_{yaw} q_{pitch} q_{roll}\\) Note: recall that when composing quaternions to be applied to a body, left-multiplied quaternions are applied in the extrinsic coordinate system (world-basis) whereas right-multiplied quaternions are applied in the intrinsic coordinate system (rotating body's basis). Thus, yaw is about the world-z axis (z), then pitch is about the vehicle's x-axis after yawing (x') and roll is about the vehicle's y-axis after both yawing then pitching (y''). This is consistent with the definitions of the coordinate system and pitch, roll, yaw for the control board. Decomposing the quaternion can be done by converting \\(q\\) to a set of euler angles \\(e = \\begin{pmatrix}pitch & roll & yaw\\end{pmatrix}\\) then constructing the following and converting each to a quaternion \\(e_{pitch} = \\begin{pmatrix}pitch & 0 & 0\\end{pmatrix} \\rightarrow q_{pitch}\\) \\(e_{roll} = \\begin{pmatrix}0 & roll & 0\\end{pmatrix} \\rightarrow q_{roll}\\) \\(e_{yaw} = \\begin{pmatrix}0 & 0 & yaw\\end{pmatrix} \\rightarrow q_{yaw}\\) However, the euler angles obtained from \\(q\\) may not be correct for this use case. An equivalent angle go \\(e\\) (although improper) is \\(e_{alt} = \\begin{pmatrix} \\pi - pitch & roll - \\pi & yaw - \\pi \\end{pmatrix}\\) . We need to compensate for first roll then pitch. Thus, we need the euler angle with minimal roll component. This will be referred to here as \\(e_b\\) , equal to either \\(e\\) or \\(_{alt}\\) , whichever has the smaller magnitude roll component. Then, given \\(s\\) vectors describing motion to change the vehicle's pitch, roll, or yaw \\(s_{pitch} = \\begin{pmatrix}p & 0 & 0\\end{pmatrix}\\) \\(s_{roll} = \\begin{pmatrix}0 & r & 0\\end{pmatrix}\\) \\(s_{yaw} = \\begin{pmatrix}0 & 0 & h\\end{pmatrix}\\) We need to transform these onto the robot's axes as \\(w\\) vectors. For roll this is trivial as roll is about the vehicle's y axis. For pitch, this requires undoing roll first (rotate by \\(q_{roll}^*\\) ) and for yaw this requires undoing roll then pitch. Thus \\(w_{roll} = s_{roll}\\) \\(\\left\\{0, w_{pitch}\\right\\} = q_{roll}^* \\left\\{0, s_{pitch}\\right\\} q_{roll}\\) \\(\\left\\{0, w_{yaw}\\right\\} = q_{pitch}^* q_{roll}^* \\left\\{0, s_{yaw}\\right\\} q_{roll} q_{pitch}\\) These vectors are angular speeds about the vehicle's x, y, and z axes. Thus, just as for translations Upscale each \\(w\\) vector using p , r , and h speeds (can skip for roll as this vector is never rotated) Sum the three \\(w\\) vectors into a net \\(w\\) vector Adjust \\(w\\) for relative DoF speeds (using \\(m_{rx}\\) , \\(m_{ry}\\) , \\(m_{rz}\\) ) Downscale \\(w\\) if needed so all elements are less than 1 Then \\(w\\) is the xrot, yrot, and zrot parts of the LOCAL mode target. Orientation Hold (OHOLD) Mode Motion Orientation hold mode uses closed-loop control for vehicle orientation in 3D space. This is achieved using three PID controllers xrot PID: Controls rotation about vehicle x-axis yrot PID: Controls rotation about vehicle y-axis zrot PID: Controls rotation about vehicle z-axis The inputs to OHOLD mode are as follows x : Translation along gx-axis (same as in GLOBAL mode) y : Translation along gy-axis (same as in GLOBAL mode) z : Translation along gz-axis (same as in GLOBAL mode) e_t : Target orientation as euler angles Optionally, a yaw speed ( h ) can be provided. In this case, PIDs will not adjust the vehicle's yaw (heading). Instead the h value works similar to GLOBAL mode (it is a rate of change of vehicle yaw). This effectively abstracts a 2D plane in which the vehicle operates. This is the same gx-gy plane described in the GLOBAL mode section. There are two variants of OHOLD mode Variant 1 (OHOLD1): Speed for yaw ( h ) is used instead of PID control Variant 2 (OHOLD2): PID is used for yaw (yaw speed h is ignored) Variant 2 is mathematically then simpler variant. Variant 1 adds some additional complexity for orientation control. Translation DoFs The user provided x , y , and z translation speeds are handled the same way by OHOLD mode as they are in GLOBAL mode to obtain the final LOCAL mode translation vector \\(l\\) . OHOLD2 Orientation Control While translation for OHOLD is nearly identical to GLOBAL mode, rotation DoFs are very different. Orientation is controlled with a set of 3 PID controllers that work in LOCAL DoFs. Thus it is necessary to determine the rotations necessary about the vehicle's axes to achieve the desired orientation. We are given a target orientation as euler angles, \\(e_t\\) . This can be converted to a target orientation quaternion, \\(q_t\\) using the formulas derived in later sections. Note: For OHOLD1 obtaining \\(q_t\\) is more complex. See the OHOLD1 section for details. However, what is done with \\(q_t\\) from here on, is the same in OHOLD1 as it is in OHOLD2. The vehicle's current orientation (as a quaternion) is also available from the IMU as \\(q_c\\) . We need to calculate a quaternion \\(q_d\\) that represents the minimal rotation from \\(q_c\\) to \\(q_t\\) . However, importantly we want \\(q_d\\) to be a rotation in the vehicle's basis. Recall that right multiplication of quaternions are applied in the vehicle's basis. Thus, to describe the target orientation as the current orientation plus a rotation in the vehicle's basis \\(q_t = q_c q_d\\) Therefore \\(q_d = q_c^* q_t\\) However, this angle may not be minimal. Recall that \\(q\\) and \\(-q\\) represent the same orientation. Thus another solution to this problem would be \\(q_d = (-q_c)^* q_t\\) The minimal rotation is the one where \\(q_c\\) and \\(q_t\\) are on the same half of the unit quaternion hypersphere (meaning their dot product is not negative). Thus If \\(q_c \\cdot q_t < 0\\) then \\(q_d = (-q_c)^* q_t\\) Otherwise \\(q_d = q_c^* q_t\\) Then convert \\(q_d\\) to axis-angle representation. The following algorithm is used to do so for numeric stability reasons (the common formulas using asin are not numerically stable) let \\(q_d = \\left\\{s, v\\right\\}\\) \\(\\theta = 2 atan2(|v|, s)\\) \\(n = v / |v|\\) if \\(|v| > 0\\) else \\(n = v = (0, 0, 0)\\) This axis ( \\(n\\) ) will be a unit vector. Thus it represents proportions of the rotation about each of the vehicle's axes (this is about vehicle's axes because \\(q_d\\) is in the vehicle basis). The angle ( \\(\\theta\\) ) is the magnitude of the rotation that must be taken. Thus, the product of \\(n\\) and \\(\\theta\\) is proportional to the error in angle about each of the vehicle's axes. \\(e = \\theta n\\) This error vector, \\(e\\) , contains the errors to be provided to each orientation PID (xrot, yrot, zrot). The output of these PIDs are angular velocity percentages (-1.0 to 1.0) about each of the vehicle's axes ( \\(w_i\\) denotes the output of the \\(i\\) rot PID) \\(w = \\begin{pmatrix} w_x & w_y & w_z \\end{pmatrix}\\) Note: for OHOLD 1 there is an extra step here to calculate the correct w (see section below). Finally, it is necessary to downscale the \\(w\\) vectors just as in GLOBAL mode First downscale using \\(m_{rx}\\) , \\(m_{ry}\\) and \\(m_{rz}\\) using the algorithm described in the GLOBAL mode section Then downscale the vector so that all elements are less than 1. While this is never needed for OHOLD2, it is needed for OHOLD1 and it is simpler branching logic (code implementation) to just always check if downscaling is needed. This \\(w\\) can then be passed to LOCAL mode along with the LOCAL translation vector \\(l\\) obtained earlier. OHOLD1 Orientation Control OHOLD1 uses much of the same process to control the vehicle's orientation as OHOLD2, however it is necessary to decouple yaw from pitch and roll. In other words, we want to construct a target quaternion using the user provided pitch and roll, but matching the vehicle's current yaw. The most intuitive option would be to decompose \\(q_c\\) into euler angles and obtain the yaw from those. However, this does not account for the fact that the yaw can be altered by pitch or roll. For example, (p=115, r=0, h=90) would be decomposed as (p=65, r=180, h=-90). Here the heading is 180 degrees off. This would be a significant issue. Thus instead, the target quaternion can be decomposed using the swing-twist decomposition of quaternion rotations. We twist about the world z axis (since this is the axis the vehicle initially yawed about). The vehicle's yaw can be calculated from this twist quaternion (using euler conversion formula). The target quaternion is constructed from the target pitch and roll provided by the user and the yaw provided by the twist quaternion as described above. This target quaternion is \\(q_t\\) and is used the same way described for OHOLD2. After calculating \\(w\\) as described for OHOLD2, there is one extra step before handling DoF scaling. The yaw speed ( h ) provided by the user must be transformed to the vehicle's axes and added to the \\(w\\) from the PIDs. This can be done similar to GLOBAL mode \\(\\left\\{0, w_{yaw}\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix}0 & 0 & h\\end{pmatrix}\\right\\} q_{rot}^*\\) \\(w_{yaw}\\) will then be upscaled just as in GLOBAL mode. Then \\(w = w + w_{yaw}\\) . This is then downscaled as described for OHOLD2. Stability Assist (SASSIST) Mode Stability assist mode (SASSIST) is simply OHOLD mode, with the addition of closed-loop control for vehicle depth. Thus, instead of providing a z speed (for motion along the gz-axis), a PID controller is used to achieve the target depth. Inputs to this mode are the same as OHOLD mode, however instead of z , d_t is provided. d_t : Target depth (meters; negative for below the surface) The PID used to maintain vehicle depth is referred to as the Depth PID. This PID uses the current vehicle depth (provided by a depth sensor) to calculate a z speed in the gz-axis, then provides it as an input to OHOLD mode. All other inputs from SASSIST are passed through to OHOLD mode as provided. Thus, there are two variants of SASSIST mode (variant 1 and variant 2) built ontop of the corresponding variants of OHOLD mode. Accumulated Euler Angles WARNING: Make sure you understand what these are and are not before using them. These are not normal euler angles. They are intended to allow high level software using the control board to a measurement of orientation about an individual axis that accumulates similar to a gyroscope. Generally, use of these is discouraged. They are provided as a \"compatibility bridge\" for code developed for a gyroscope system. WARNING: Accumulated euler angles are not the current orientation of the vehicle as an euler angle! The euler and quaternion values provided by the IMU are not directly useful for tracking multiple rotations of the vehicle. Unlike simply integrating gyroscope data, euler angles (pitch, roll, yaw) and quaternions do not track the number of times the vehicle has rotated about a particular axis. They only track the vehicle's orientation in space. While integrating raw gyro data would provide an accumulating measurement, such a solution would be rotations about the robot's axes. Generally, it is more useful to track the number of times the vehicle has \"pitched\", \"rolled\" or \"yawed\". Note that an accumulated gyro z angle of 500 does not necessarily mean the robot has yawed 500 degrees; the robot could have been oriented at a pitch of 90. Instead, we want to measure how many degrees the vehicle has rotated through. This can be done by tracking changes between subsequent quaternions from the IMU. The idea is to compare each quaternion read from the IMU with the previous quaternion received from the IMU (note that quaternions of all zeros are ignored to avoid issues with invalid IMU data samples after first configuring the sensor). For each quaternion read from the IMU: Calculate the shortest set of rotations from the previous to the current quaternion Convert the shortest angle to euler angles Add the pitch, roll, and yaw from the shortest euler angles to accumulated pitch, roll, and yaw variables Note that if the IMU axis config changes, the accumulated data should be zeroed and the previously read quaternion discarded. This makes the assumption that the smallest rotation between two quaternions is the most probable path the robot took to change its orientation. This is an approximation, however it is a fairly good one as long as sample rate of data is sufficiently high. The specifics of the path are lost, however, if the sample rate is high enough, the length of the path is sufficiently small that and this is a good approximation. The second issue with this approximation has to do with rotations exceeding 180 degrees. The method for determining shortest path between two quaternions will be incorrect if the vehicle rotates more than 180 degrees in any axis (because the shortest path would have involved rotating the other direction). To guarantee rotations between two samples never exceed 180 degrees, the max measured rotation rate of the IMU is considered. For the BNO055 this is 2000 degrees per second. Thus, with a sample period of \\(l\\) milliseconds, the largest angle change between samples is \\(\\frac{2000 \\textrm{ deg}}{1 \\textrm{ sec}} \\cdot \\frac{1 \\textrm{ sec}}{1000 \\textrm{ ms}} \\cdot \\frac{l \\textrm{ ms}}{1 \\textrm{ sample}} = 2l \\textrm{ deg / sample}\\) To ensure that changes of more than 180 degrees do not occur, the following must be satisfied \\(2l < 180 \\rightarrow l < 90 \\textrm{ milliseconds}\\) However, it is possible for some samples from the IMU to be delayed (ie I2C bus busy with another sensor) or lost (I2C failure). Thus, it is necessary to choose a value for \\(l\\) that allows for at least one sample to be lost. When a sample is lost, this doubles the effective time between samples. Thus, it is necessary to half \\(l\\) \\(l < 45 \\textrm{ milliseconds}\\) By further reducing \\(l\\) it is possible to allow for larger delays or more lost samples. The current firmware samples IMU data every 15ms (the max rate supported by the BNO055 in fusion mode is 100Hz = 10ms period). Using \\(l=15 \\textrm{ ms}\\) it is possible for 5 consecutive samples to be lost while still guaranteeing that no more than 90ms passes between valid samples (thus still ensuring no more than 180 degree change between samples). Other Derivations Euler to Quaternion Conversion The euler angle convention used for the control board is an intrinsic set z-x'-y'' (rotate about z, then about new x, then about new y to compose an angle). Using the definitions of front/right/top for this coordinate system, this means that roll is about y, pitch is about x, and yaw is about z. A rotation quaternion, \\(Q\\) , can be composed using three rotations, each being one of the rotations used to construct the euler angle representation (in order). \\(Q = Q_z Q_x Q_y\\) Quaternion multiplication is associative. Recall that right-multiplied quaternions are applied in the vehicle frame during composition. Thus, when viewed left to right: First, apply \\(Q_z\\) in vehicle frame (also equal to world frame since this is the first rotation) Then, apply \\(Q_x\\) in the vehicle frame Finally, apply \\(Q_y\\) in the vehicle frame However, recall that left-multiplied quaternions are applied in the world frame during composition. Thus, when viewed right to left First, apply \\(Q_y\\) in the world frame Then, apply \\(Q_x\\) in the world frame Finally, apply \\(Q_z\\) in the world frame In other words, the following are equivalent Yaw about z , then pitch about x' , then roll about y'' Roll about y , then pitch about x , the yaw about z These use the same pitch, roll, and yaw angles , but the latter applies them about world axes , which have known and trivial direction vectors. Thus, each of pitch, roll, and yaw are a rotation about a known axis (x, y, or z respectively). Converting these from axis-angle from, the quaternions are represented as follows \\(Q_y = \\left\\{ cos(\\frac{roll}{2}), sin(\\frac{roll}{2}) \\begin{pmatrix} 0 \\\\ 1 \\\\ 0 \\end{pmatrix} \\right\\}\\) \\(Q_x = \\left\\{ cos(\\frac{pitch}{2}), sin(\\frac{pitch}{2}) \\begin{pmatrix} 1 \\\\ 0 \\\\ 0 \\end{pmatrix} \\right\\}\\) \\(Q_z = \\left\\{ cos(\\frac{yaw}{2}), sin(\\frac{yaw}{2}) \\begin{pmatrix} 0 \\\\ 0 \\\\ 1 \\end{pmatrix} \\right\\}\\) By multiplying these, Q can be obtained. This is relatively simple to do because each of the vectors defining the quaternion is a trivial vector (this is because of using world axes!) \\(Q = \\left\\{w, \\begin{pmatrix}x \\\\ y \\\\ z\\end{pmatrix}\\right\\} = Q_z Q_x Q_y\\) let \\(cp = cos(\\frac{pitch}{2})\\) , \\(sp = sin(\\frac{pitch}{2})\\) let \\(cr = cos(\\frac{roll}{2})\\) , \\(sr = sin(\\frac{roll}{2})\\) let \\(cy = cos(\\frac{yaw}{2})\\) , \\(sr = sin(\\frac{yaw}{2})\\) \\(w = cy \\cdot cp \\cdot cr - sy \\cdot sp \\cdot sr\\) \\(x = cy \\cdot sp \\cdot cr - sy \\cdot cp \\cdot sr\\) \\(y = sy \\cdot sp \\cdot cr + cy \\cdot cp \\cdot sr\\) \\(z = sy \\cdot cp \\cdot cr + cy \\cdot sp \\cdot sr\\) This provides a set of equations which can be used to convert from an euler angle representation to a quaternion. Quaternion to Euler Conversion Converting from quaternion to euler uses the rotation matrix representation as a go-between. For the intrinsic set of euler angles z-x'-y'' , a rotation matrix can be composed as follows (recall that right multiply applies about vehicle axis). \\(R = R_z(yaw) R_x(pitch) R_y(roll)\\) Similar to the quaternion case described above, note that matrix multiplication is associative. Thus, this can instead be interpreted as rotations about world axes in the opposite order. Thus, each of \\(R_x\\) , \\(R_y\\) , and \\(R_z\\) are rotations about world x, y, and z axes. Such matrices have known forms. When multiplied out, the following representation of R is obtained. let \\(cp = cos(pitch)\\) , \\(sp = sin(pitch)\\) let \\(cr = cos(roll)\\) , \\(sr = sin(roll)\\) let \\(cy = cos(yaw)\\) , \\(sy = sin(yaw)\\) \\(R = \\begin{pmatrix} cy \\cdot cr - sr \\cdot sy \\cdot sp & -sy \\cdot cp & cy \\cdot sr + sy \\cdot sp \\cdot cr \\\\ sy \\cdot cr + sr \\cdot sp \\cdot cy & cy \\cdot cp & sy \\cdot sr - cr \\cdot sp \\cdot cy \\\\ -cp \\cdot sr & sp & cp \\cdot cr \\end{pmatrix}\\) Using entries of this matrix, the following relations can be constructed \\(R_{32} = sin(pitch) \\rightarrow pitch = sin^{-1}(R_{32})\\) \\(\\frac{R_{31}}{R_{33}} = \\frac{-sin(roll)cos(pitch)}{cos(roll)cos(pitch)} = tan(roll) \\rightarrow roll = tan^{-1}(\\frac{-R_{31}}{R_{33}})\\) \\(\\frac{R_{12}}{R_{22}} = \\frac{sin(yaw)cos(pitch)}{cos(yaw)cos(pitch)} = tan(yaw) \\rightarrow yaw = tan^{-1}(\\frac{R_{12}}{R_{22}})\\) A quaternion can also be converted to a rotation matrix with a known form. Using cells from this quaternion-backed rotation matrix yields the following relations \\(pitch = sin^{-1}(2(yz+wx))\\) \\(roll = tan^{-1}(\\frac{2(wy-xz)}{1-2(x^2+y^2)})\\) \\(yaw = tan^{-1}(\\frac{2(xy-wz)}{1-2(x^2 + z^2)})\\) Note that arctangent should be implemented in code using the quadrant aware atan2 to account for quadrants properly and avoid divide by zero issues.","title":"Math"},{"location":"devs/math/#math","text":"","title":"Math"},{"location":"devs/math/#coordinate-system-definition","text":"The control board uses a coordinate system that is somewhat non-standard. The coordinate system is right handed +y is forward, +z is up, +x is right Pitch is about x, roll is about y, yaw is about z While this coordinate system may seem strange to some (especially those who have worked with aircraft), the name of axes doesn't really matter. The definition of pitch, roll, and yaw relative to the front / top of the vehicle remain standard.","title":"Coordinate System Definition"},{"location":"devs/math/#6-degree-of-freedom-motion-control","text":"","title":"6 Degree of Freedom Motion Control"},{"location":"devs/math/#nomenclature-notation-and-convention-notes","text":"Matrices are assumed to be zero indexed (not 1 indexed). This means the first element of a matrix \\(M\\) is \\(m_{00}\\) not \\(m_{11}\\) . This is because the math will be implemented in C (which uses zero indexed arrays). Thruster numbers (1-8) are used by the control board's user facing components. However, this math will use thruster indices (0-7) where index = number - 1 . This is again because arrays in C are zero indexed. Rotations about axes (angular velocities / DoFs) are referred to as \"xrot\" (about x), \"yrot\" (about y), and \"zrot\" (about z). The terms \"pitch\", \"roll\", and \"yaw\" are used to describe the vehicle's orientation in space. These are a set of intrinsic Euler angles (Tait-Bryan angles to be precise) composed in the order yaw then pitch then roll (z-x'-y'' in the control board's coordinate system). Note that while these do follow the standard meanings of pitch, roll, and yaw relative to the vehicle, the axes have non-standard names. Thus, you cannot simply use standard formulas for converting between quaternions and pitch, roll, yaw. Velocities in DoFs are normalized (meaning -1.0 to 1.0). Euler angles are represented as a set of three values \\(\\begin{pmatrix} pitch & roll & yaw\\end{pmatrix}\\) Quaternions are represented either as a combination of a scalar component, \\(s\\) , and a vector component, \\(v\\) as \\(\\{s,v\\}\\) or they may be represented using the names \\(w\\) , \\(x\\) , \\(y\\) , and \\(z\\) . In such a case this is equivelent to \\(\\{w, (x, y, z)\\}\\) . In other words, \\(x\\) , \\(y\\) , and \\(z\\) are the components of the vector \\(v\\) and \\(w=s\\) . The conjugate of a quaternion, \\(q\\) is denoted \\(q^*\\)","title":"Nomenclature, Notation, and Convention Notes"},{"location":"devs/math/#system-assumptions","text":"Vehicle is capable of motion exclusively in each of 6 degrees of freedom (DoFs). These are three translational DoFs, and three rotational DoFs. The vehicle's speed in positive and negative directions are roughly equal for each DoF. Thruster orientations are fixed. Gimbaled thruster vehicles are not supported. At most 8 thrusters (less is fine) System has 3D orientation information System has depth information System does not have translational position information","title":"System Assumptions"},{"location":"devs/math/#example-vehicle","text":"The examples on this page will use AquaPack robotics's SeaWolf VIII robot . This robot's thruster configuration is as shown below. The arrows indicate the direction the thruster moves water when powered in the positive direction. These arrows are opposite the direction force is excerpted on the vehicle. Note that the diagram above uses thruster numbers, not indices.","title":"Example Vehicle"},{"location":"devs/math/#dof-matrix","text":"The DoF Matrix , \\(D\\) , is constructed based on the vehicle's thruster configuration. Rows of the matrix correspond to thrusters (by index). And columns of the matrix correspond to vehicle-relative DoFs. Thus, this is an 8x6 matrix (8 thrusters, 6 DoFs). Columns correspond to DoFs in the following order (0-5): x, y, z, xrot, yrot, zrot. \\(D = \\left(\\begin{array}{c|c|c|c|c|c} d_0 & d_1 & d_2 & d_3 & d_4 & d_5 \\end{array}\\right) = \\left(\\begin{array}{c|c|c|c|c|c} d_x & d_y & d_z & d_{xr} & d_{yr} & d_{zr} \\end{array}\\right)\\) Each column of the DoF matrix, \\(d_i\\) is a set of thruster speeds that result in motion exclusively in the column's DoF. Additionally, the resultant motion should be the maximum possible speed, and in the positive direction. Each \\(d_i\\) is an 8 element column vector, with elements corresponding to thrusters (by index). All speeds should be normalized (between -1.0 and 1.0) For the example vehicle shown above, the following is the DoF matrix \\(D = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix}\\) Consider the first column: \\(d_0 = d_x\\) . This column's thruster speeds should result in the vehicle moving as fast as possible in the +x direction (only). This is achieved by setting T2, T4 (index 1, 3) to the positive direction and T1, T3 (index 0, 2) to the negative direction at full speed (recall that the arrows are opposite the direction the thruster excerpts force on the vehicle). Thus \\(d_x = \\begin{pmatrix}-1 & +1 & -1 & +1 & 0 & 0 & 0 & 0\\end{pmatrix}^T\\) Note that when constructing the DoF matrix for your vehicle, you should assume an ideal system and environment (ignore variation between thrusters, environmental factors that create motion, etc).","title":"DoF Matrix"},{"location":"devs/math/#local-mode-motion","text":"In LOCAL mode, motion is specified as a set of speeds in vehicle-relative DoFs. The user provides the control board with a local target motion vector ( \\(t_l\\) ) where each element corresponds to a DoF. \\(t_l = \\begin{pmatrix} x & y & z & r_x & r_y & r_z \\end{pmatrix}^T\\) \\(x\\) is normalized velocity in +x direction \\(y\\) is normalized velocity in +y direction \\(z\\) is normalized velocity in +y direction \\(r_x\\) is normalized angular velocity about the +x axis direction (xrot) \\(r_y\\) is normalized angular velocity about the +y axis (yrot) \\(r_z\\) is normalized angular velocity about the +z axis (zrot) By multiplying this target motion by the DoF matrix, \\(D\\) , a speed vector \\(s\\) is obtained where each element of \\(s\\) corresponds to a specific thruster (by index). \\(s = D t_l\\) Consider the example where \\(t_l = \\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 0\\end{pmatrix}^T\\) . This should cause the vehicle to move at full possible speed forward (relative to the vehicle's orientation). \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix} = \\begin{pmatrix}-1 \\\\ -1 \\\\ +1 \\\\ +1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix}\\) In the above example, it is trivial to see that this is the desired motion. However for a more complex example, a problem appears. Consider \\(t_l = \\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 1\\end{pmatrix}^T\\) . This describes the vehicle both moving forward and about the z-axis at full possible speed. \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 1\\end{pmatrix} = \\begin{pmatrix}0 \\\\ -2 \\\\ 0 \\\\ +2 \\\\ 0 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix}\\) Notice that the resultant speed vector has motors moving in excess of 100% speed (elements with magnitude greater than 1.0). This is not possible. The simple solution would seem to be dividing all elements of the vector by the one with the largest magnitude. This results in a scaled speed vector \\(\\hat{s}\\) \\(\\hat{s} = s \\div \\text{absmax}(s)\\) However, this method will not work well in all cases. Consider \\(t_l = \\begin{pmatrix}0 & 1 & 1 & 1 & 1 & 1\\end{pmatrix}^T\\) . \\(s = D t_l = \\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\begin{pmatrix}0 \\\\ 1 \\\\ 1 \\\\ 1 \\\\ 1 \\\\ 1\\end{pmatrix} = \\begin{pmatrix}0 \\\\ -2 \\\\ 0 \\\\ +2 \\\\ -3 \\\\ -1 \\\\ -1 \\\\ +1\\end{pmatrix}\\) and \\(\\hat{s} = s \\div \\text{absmax}(s) = s \\div 3 = \\begin{pmatrix}0 \\\\ -0.67 \\\\ 0 \\\\ +0.67 \\\\ -1 \\\\ -0.33 \\\\ -0.33 \\\\ +0.33\\end{pmatrix}\\) While this has resulted in an possible set of thruster speeds, these are not optimal. Look at the example vehicle diagram. Notice that thrusters 1-4 and 5-8 control different motions. In the previous example, thrusters 1-4 were slowed down more than necessary, because thruster 5 was too large of a value. This is not ideal as the vehicle's maximum speed becomes artificially limited. Instead, the following \\(\\hat{s}\\) is ideal. This is scaling down the thrusters within each group (1-4 and 5-8) separately. \\(\\hat{s} = \\begin{pmatrix}0 \\\\ -1 \\\\ 0 \\\\ +1 \\\\ -1 \\\\ -0.33 \\\\ -0.33 \\\\ +0.33\\end{pmatrix}\\) Groupings of thrusters on the example vehicle are easy to observe, however this is not always true. Thus, achieving optimal scaling for any system (any DoF matrix) requires a more sophisticated method to determine groupings and scale speeds. Thruster groupings are determined by \"overlap\" between thrusters. Two thrusters, \\(i\\) and \\(j\\) are said to overlap if they have a non-zero entry in the same column of the DoF matrix ( \\(D\\) ) for at least one column. This is easier to calculate using a contribution matrix , \\(C\\) , defined as \\(D \\neq 0\\) . This results in a binary form of the DoF matrix. For the above example \\(C = \\left[\\begin{pmatrix} -1 & -1 & 0 & 0 & 0 & +1 \\\\ +1 & -1 & 0 & 0 & 0 & -1 \\\\ -1 & +1 & 0 & 0 & 0 & -1 \\\\ +1 & +1 & 0 & 0 & 0 & +1 \\\\ 0 & 0 & -1 & -1 & -1 & 0 \\\\ 0 & 0 & -1 & -1 & +1 & 0 \\\\ 0 & 0 & -1 & +1 & -1 & 0 \\\\ 0 & 0 & -1 & +1 & +1 & 0 \\\\ \\end{pmatrix} \\neq 0 \\right] = \\begin{pmatrix} 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 1 & 1 & 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ 0 & 0 & 1 & 1 & 1 & 0 \\\\ \\end{pmatrix}\\) Then for each thruster \\(i\\) an overlap vector \\(o_i\\) can be constructed as follows \\(o_i = C (c^i)^T\\) where \\(c^i\\) is the \\(i\\) th row of \\(C\\) . Thus, \\(o_i\\) is an 8 element vector where each element corresponds to a thruster (by index). Element \\(j\\) of \\(o_i\\) can either be \\(1\\) or a \\(0\\) . A \\(1\\) indicates that thrusters \\(i\\) and \\(j\\) overlap. For example, \\(o_0 = C (c^0)^T = \\begin{pmatrix} 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 \\end{pmatrix}^T\\) This shows that thruster index 0 (T1) overlaps with indices 0, 1, 2, and 3 (T1, T2, T3, T4). By calculating and storing these overlap vectors for each thruster ( \\(\\left\\{o_i\\right\\}_{i=0}^7\\) ), this effectively forms a lookup table to determine thruster overlap. While this is not the most memory efficient option, it reduces computation time, which is important since this will run very frequently on a microcontroller. Using overlap vectors, the following algorithm can be used to scale motor speeds: Find the thruster ( i ) with the largest magnitude speed Iterate over thruster i 's overlap vector For any thruster, j , with which thruster i overlaps, divide thruster j 's speed by the magnitude of thruster i 's speed Repeat until the largest magnitude does not exceed 1.0 while true // m is value, i is index m, i = max(abs(speed_vector)) if m <= 1.0 // Done scaling break endif // Iterate over all thrusters (0-7 inclusive) for j=0...7 if overlap_vector[i][j] == 1 // i and j overlap. Divide j's speed by m. speed_vector[j] /= m endif endfor endwhile This algorithm results in optimal speed scaling by only reducing the speed of thrusters that share DoF contributions.","title":"LOCAL Mode Motion"},{"location":"devs/math/#global-mode-motion","text":"GLOBAL mode is very similar to LOCAL mode, however, motion is described partially relative to the world instead of the robot. Specifically, motion of the vehicle is compensated for vehicle pitch and roll (but not yaw). This results in a coordinate system defined by the axes gx , gy , and gz . Note that the world coordinate system is defined as wx , wy , wz . Notably, if the vehicle is pitched 180 degrees, the gx-gy plane becomes aligned to the back of the vehicle ensuring consistent motion. This is shown in the animation below. In GLOBAL mode, the user provides the control board with a global target motion vector , \\(t_g\\) with 6 elements. This target motion vector is a concatenation of two 3 dimensional vectors. The first, a set of translations along gx , gy , and gz . Second a set of rotations to affect vehicle pitch, roll, and yaw. These are referred to by the following names x : Speed in gx direction (translation) y : Speed in gy direction (translation) z : Speed in gz direction (translation) p : Speed at which the vehicle's pitch should increase (negative for decrease pitch). Aka \"pitch_spd\" r : Speed at which the vehicle's roll should increase (negative for decrease pitch). Aka \"roll_spd\" h : Speed at which the vehicle's yaw should increase (negative for decrease pitch). Aka \"yaw_spd\" \\(t_g = \\begin{pmatrix} x & y & z & p & r & h \\end{pmatrix}\\) It is necessary to transform each DoF's motion into motions in the vehicle's DoFs. These speeds can then be passed to LOCAL mode. WARNING: GLOBAL mode is impacted by gimbal lock issues with euler angles. This occurs when the vehicle's pitch is +/- 90 degrees. In this scenario the meaning of \"increase / decrease pitch\" is ambiguous. The vehicle will take the zero-roll route in this scenario (an arbitrary choice based on how euler angle conversion is implemented in the firmware). Thus, GLOBAL mode may produce undesirable motion if the roll is non-zero and you pitch through +/- 90. A potential solution for this could be some form of motion hysteresis to handle moving through gimbal lock orientations, however this is not implemented as of now.","title":"GLOBAL Mode Motion"},{"location":"devs/math/#translation-dofs","text":"The translation DoFs are easily transformed using gravity vectors. By applying a quaternion based rotation matrix to the base gravity vector, \\(g_b = \\begin{pmatrix}0 & 0 & -1\\end{pmatrix}\\) , the following solution is determined for the current gravity vector, \\(g_c\\) given the vehicle's orientation quaternion, \\(q\\) . \\(\\begin{pmatrix} 2*(q.x*q.z+q.w*q.y) \\\\ 2*(q.w*q.x-q.y*q.z) \\\\ -(q.w)^2+(q.x)^2+(q.y)^2-(q.z)^2\\end{pmatrix}^T\\) The minimal rotation from \\(g_b\\) to \\(g_c\\) is then calculated. Let this rotation be called \\(q_{rot}\\) . This rotation will generally include no yaw component, unless the vehicle is upside down and facing backwards (eg pitch of 180 degrees) in which case it will contain a yaw component of 180 degrees. This is desirable as it ensures a continuous definition of what gy is even while the vehicle is flipping via pitch. This quaternion can then be applied to speeds in the gx , gy , gz basis ( x , y , and z here) to rotate them onto the vehicle basis. Thus, this \"converts\" translation speeds from GLOBAL to LOCAL mode DoFs. However, it is not ideal to transform the translation vector all at once. It is best to do it in three stages to allow proper upscaling as needed (explained below). Thus for each global mode translation vector \\(\\begin{pmatrix} x & 0 & 0 \\end{pmatrix}\\) , \\(\\begin{pmatrix} 0 & y & 0 \\end{pmatrix}\\) , and \\(\\begin{pmatrix} 0 & 0 & z \\end{pmatrix}\\) rotate it by \\(q_{rot}\\) to obtain \\(t_x\\) , \\(t_y\\) , and \\(t_z\\) respectively (note that \\(\\left\\{s, v\\right\\}\\) is a quaternion with scalar s and vector v). \\(\\left\\{0, t_x\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} x & 0 & 0 \\end{pmatrix} \\right\\} q_{rot}^*\\) \\(\\left\\{0, t_y\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} 0 & y & 0 \\end{pmatrix} \\right\\} q_{rot}^*\\) \\(\\left\\{0, t_z\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix} 0 & 0 & z \\end{pmatrix} \\right\\} q_{rot}^*\\) Each of \\(t_x\\) , \\(t_y\\) , and \\(t_z\\) are speeds in LOCAL mode DoFs, however they may be slower than desired. Consider a speed of 1.0 along gy . If the vehicle were rotated 45 degrees (pitch) the resultant \\(t_y\\) would be \\(t_y = \\begin{pmatrix} 0.0 & 0.7071 & 0.7071 \\end{pmatrix}\\) . This is not as fast as possible in the correct direction. Instead \\(t_y\\) should be \\(t_y = \\begin{pmatrix} 0.0 & 1.0 & 1.0 \\end{pmatrix}\\) . In other words, the largest element of \\(t_y\\) should be the speed along gy . Thus, each of \\(t_x\\) , \\(t_y\\) , \\(t_z\\) needs to be upscaled (note that it will never need to be downscaled; it only needs to be upscaled because one DoF may now be spread between multiple). let \\(m\\) be the magnitude of the element of \\(t_x\\) with the largest magnitude Normalize \\(t_x\\) so that largest element is 1.0: \\(t_x = t_x / m\\) Scale normalized \\(t_x\\) by speed \\(x\\) (gx speed): \\(t_x = t_x * x\\) Repeat this for \\(t_y\\) and \\(t_z\\) using \\(y\\) and \\(z\\) speeds respectively. Once all three \\(t\\) vectors are scaled, they can be combined to create the net LOCAL mode translation vector, \\(l\\) \\(l = t_x + t_y + t_z\\) There are two potential issues with \\(l\\) \\(l\\) is a set of proportionally related speeds to result in the desired motion. However, the vehicle may not be capable of the same speeds in each of it's DoFs. Thus, the ratios between \\(l\\) 's elements may be incorrect. \\(l\\) is a sum of three vectors (each with elements no larger than a magnitude of 1), thus it may have elements with a magnitude greater than 1. Issue 1 should be handled first as correcting it may \"fix\" issue 2. Handling issue 2 first could result in downscaling speeds more than necessary. Handling issue 1 requires the user to provide a little more information about the vehicle: relative speeds in each DoF. These can be used to calculate downscaling factors to slow down the faster directions (note: speeding up the slower directions could result in impossible speeds, but would be handled by solving issue 2; regardless it is less ideal). These downscaling factors are calculated from \"RELDOF\" information provided by the user (see messages page of user guide). This information is a set of constants, which will be referred to the as the scale factors as \\(m_x\\) , \\(m_y\\) , \\(m_z\\) , \\(m_{rx}\\) , \\(m_{ry}\\) , and \\(m_{rz}\\) for the x, y, z, xrot, yrot, and zrot DoFs respectively (note that these are vehicle DoFs). These scale factors makeup two groups: translational ( \\(m_x\\) , \\(m_y\\) , and \\(m_z\\) ) and rotational ( \\(m_{rx}\\) , \\(m_{ry}\\) , and \\(m_{rz}\\) ). Within each group, the fastest DoF's scale factor is set to 1. The other scale factors are a percentage of the fastest DoF in the group. For example, if the vehicle can move at 2m/s along z, 1m/s along y, and 0.5m/s along x then \\(m_x = 0.25\\) , \\(m_y = 0.5\\) , and \\(m_z = 1.0\\) . Given these scale factors, the simplest option would be to let \\(l = \\begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\\end{pmatrix}\\) . However, this may downscale more than necessary. Consider a scenario where the speed vector \\(l\\) has a \\(0\\) in the position of the slowest DoF of the vehicle (eg if the vehicle is slowest in the x direction, the \\(l\\) vector would have a zero in the x position). In this case, since the slowest direction is unused, we are downscaling too much. Thus, the following algorithm is used to select the ideal downscaling factors by \"ignoring\" the downscaling required for unused DoFs (DoFs with a speed of 0). // Zero downscale factors for unused DoFs if abs(l.x) == 0 m_x = 0 endif if abs(l.y) == 0 m_y = 0 endif if abs(l.z) == 0 m_z = 0 endif // Rebalance scale factors so largest remaining is 1.0 m_max = max(m_x, max(m_y, m_z)); m_x = m_x / m_max; m_y = m_y / m_max; m_z = m_z / m_max; \\(l\\) is then downscaled as \\(l = \\begin{pmatrix}l.x * m_x & l.y * m_y & l.z * m_z\\end{pmatrix}\\) using the scale factors calculated using the above algorithm. Finally, issue 2 must be handled if any element of \\(l\\) still has a magnitude greater than 1.0. Thus, Let \\(m\\) be the magnitude of the element of \\(l\\) with the largest magnitude If \\(m\\) is less than or equal to 1, do not change $l If \\(m\\) is greater than 1 \\(l = l / m\\) This resultant \\(l\\) is a set of speeds that can be passed to LOCAL mode as it's x , y , and z speeds.","title":"Translation DoFs"},{"location":"devs/math/#rotation-dofs","text":"Converting the GLOBAL mode rotations (increase/decrease pitch, roll, yaw) to motions about DoFs is a little harder. It requires decomposing the quaternion into euler angles, then calculating three quaternions describing one euler rotation each. In other words, given the vehicle's current rotation \\(q\\) we need to find \\(q_{pitch}\\) , \\(q_{roll}\\) and \\(q_{yaw}\\) such that (based on the euler angle convention used by the control board) \\(q = q_{yaw} q_{pitch} q_{roll}\\) Note: recall that when composing quaternions to be applied to a body, left-multiplied quaternions are applied in the extrinsic coordinate system (world-basis) whereas right-multiplied quaternions are applied in the intrinsic coordinate system (rotating body's basis). Thus, yaw is about the world-z axis (z), then pitch is about the vehicle's x-axis after yawing (x') and roll is about the vehicle's y-axis after both yawing then pitching (y''). This is consistent with the definitions of the coordinate system and pitch, roll, yaw for the control board. Decomposing the quaternion can be done by converting \\(q\\) to a set of euler angles \\(e = \\begin{pmatrix}pitch & roll & yaw\\end{pmatrix}\\) then constructing the following and converting each to a quaternion \\(e_{pitch} = \\begin{pmatrix}pitch & 0 & 0\\end{pmatrix} \\rightarrow q_{pitch}\\) \\(e_{roll} = \\begin{pmatrix}0 & roll & 0\\end{pmatrix} \\rightarrow q_{roll}\\) \\(e_{yaw} = \\begin{pmatrix}0 & 0 & yaw\\end{pmatrix} \\rightarrow q_{yaw}\\) However, the euler angles obtained from \\(q\\) may not be correct for this use case. An equivalent angle go \\(e\\) (although improper) is \\(e_{alt} = \\begin{pmatrix} \\pi - pitch & roll - \\pi & yaw - \\pi \\end{pmatrix}\\) . We need to compensate for first roll then pitch. Thus, we need the euler angle with minimal roll component. This will be referred to here as \\(e_b\\) , equal to either \\(e\\) or \\(_{alt}\\) , whichever has the smaller magnitude roll component. Then, given \\(s\\) vectors describing motion to change the vehicle's pitch, roll, or yaw \\(s_{pitch} = \\begin{pmatrix}p & 0 & 0\\end{pmatrix}\\) \\(s_{roll} = \\begin{pmatrix}0 & r & 0\\end{pmatrix}\\) \\(s_{yaw} = \\begin{pmatrix}0 & 0 & h\\end{pmatrix}\\) We need to transform these onto the robot's axes as \\(w\\) vectors. For roll this is trivial as roll is about the vehicle's y axis. For pitch, this requires undoing roll first (rotate by \\(q_{roll}^*\\) ) and for yaw this requires undoing roll then pitch. Thus \\(w_{roll} = s_{roll}\\) \\(\\left\\{0, w_{pitch}\\right\\} = q_{roll}^* \\left\\{0, s_{pitch}\\right\\} q_{roll}\\) \\(\\left\\{0, w_{yaw}\\right\\} = q_{pitch}^* q_{roll}^* \\left\\{0, s_{yaw}\\right\\} q_{roll} q_{pitch}\\) These vectors are angular speeds about the vehicle's x, y, and z axes. Thus, just as for translations Upscale each \\(w\\) vector using p , r , and h speeds (can skip for roll as this vector is never rotated) Sum the three \\(w\\) vectors into a net \\(w\\) vector Adjust \\(w\\) for relative DoF speeds (using \\(m_{rx}\\) , \\(m_{ry}\\) , \\(m_{rz}\\) ) Downscale \\(w\\) if needed so all elements are less than 1 Then \\(w\\) is the xrot, yrot, and zrot parts of the LOCAL mode target.","title":"Rotation DoFs:"},{"location":"devs/math/#orientation-hold-ohold-mode-motion","text":"Orientation hold mode uses closed-loop control for vehicle orientation in 3D space. This is achieved using three PID controllers xrot PID: Controls rotation about vehicle x-axis yrot PID: Controls rotation about vehicle y-axis zrot PID: Controls rotation about vehicle z-axis The inputs to OHOLD mode are as follows x : Translation along gx-axis (same as in GLOBAL mode) y : Translation along gy-axis (same as in GLOBAL mode) z : Translation along gz-axis (same as in GLOBAL mode) e_t : Target orientation as euler angles Optionally, a yaw speed ( h ) can be provided. In this case, PIDs will not adjust the vehicle's yaw (heading). Instead the h value works similar to GLOBAL mode (it is a rate of change of vehicle yaw). This effectively abstracts a 2D plane in which the vehicle operates. This is the same gx-gy plane described in the GLOBAL mode section. There are two variants of OHOLD mode Variant 1 (OHOLD1): Speed for yaw ( h ) is used instead of PID control Variant 2 (OHOLD2): PID is used for yaw (yaw speed h is ignored) Variant 2 is mathematically then simpler variant. Variant 1 adds some additional complexity for orientation control.","title":"Orientation Hold (OHOLD) Mode Motion"},{"location":"devs/math/#translation-dofs_1","text":"The user provided x , y , and z translation speeds are handled the same way by OHOLD mode as they are in GLOBAL mode to obtain the final LOCAL mode translation vector \\(l\\) .","title":"Translation DoFs"},{"location":"devs/math/#ohold2-orientation-control","text":"While translation for OHOLD is nearly identical to GLOBAL mode, rotation DoFs are very different. Orientation is controlled with a set of 3 PID controllers that work in LOCAL DoFs. Thus it is necessary to determine the rotations necessary about the vehicle's axes to achieve the desired orientation. We are given a target orientation as euler angles, \\(e_t\\) . This can be converted to a target orientation quaternion, \\(q_t\\) using the formulas derived in later sections. Note: For OHOLD1 obtaining \\(q_t\\) is more complex. See the OHOLD1 section for details. However, what is done with \\(q_t\\) from here on, is the same in OHOLD1 as it is in OHOLD2. The vehicle's current orientation (as a quaternion) is also available from the IMU as \\(q_c\\) . We need to calculate a quaternion \\(q_d\\) that represents the minimal rotation from \\(q_c\\) to \\(q_t\\) . However, importantly we want \\(q_d\\) to be a rotation in the vehicle's basis. Recall that right multiplication of quaternions are applied in the vehicle's basis. Thus, to describe the target orientation as the current orientation plus a rotation in the vehicle's basis \\(q_t = q_c q_d\\) Therefore \\(q_d = q_c^* q_t\\) However, this angle may not be minimal. Recall that \\(q\\) and \\(-q\\) represent the same orientation. Thus another solution to this problem would be \\(q_d = (-q_c)^* q_t\\) The minimal rotation is the one where \\(q_c\\) and \\(q_t\\) are on the same half of the unit quaternion hypersphere (meaning their dot product is not negative). Thus If \\(q_c \\cdot q_t < 0\\) then \\(q_d = (-q_c)^* q_t\\) Otherwise \\(q_d = q_c^* q_t\\) Then convert \\(q_d\\) to axis-angle representation. The following algorithm is used to do so for numeric stability reasons (the common formulas using asin are not numerically stable) let \\(q_d = \\left\\{s, v\\right\\}\\) \\(\\theta = 2 atan2(|v|, s)\\) \\(n = v / |v|\\) if \\(|v| > 0\\) else \\(n = v = (0, 0, 0)\\) This axis ( \\(n\\) ) will be a unit vector. Thus it represents proportions of the rotation about each of the vehicle's axes (this is about vehicle's axes because \\(q_d\\) is in the vehicle basis). The angle ( \\(\\theta\\) ) is the magnitude of the rotation that must be taken. Thus, the product of \\(n\\) and \\(\\theta\\) is proportional to the error in angle about each of the vehicle's axes. \\(e = \\theta n\\) This error vector, \\(e\\) , contains the errors to be provided to each orientation PID (xrot, yrot, zrot). The output of these PIDs are angular velocity percentages (-1.0 to 1.0) about each of the vehicle's axes ( \\(w_i\\) denotes the output of the \\(i\\) rot PID) \\(w = \\begin{pmatrix} w_x & w_y & w_z \\end{pmatrix}\\) Note: for OHOLD 1 there is an extra step here to calculate the correct w (see section below). Finally, it is necessary to downscale the \\(w\\) vectors just as in GLOBAL mode First downscale using \\(m_{rx}\\) , \\(m_{ry}\\) and \\(m_{rz}\\) using the algorithm described in the GLOBAL mode section Then downscale the vector so that all elements are less than 1. While this is never needed for OHOLD2, it is needed for OHOLD1 and it is simpler branching logic (code implementation) to just always check if downscaling is needed. This \\(w\\) can then be passed to LOCAL mode along with the LOCAL translation vector \\(l\\) obtained earlier.","title":"OHOLD2 Orientation Control"},{"location":"devs/math/#ohold1-orientation-control","text":"OHOLD1 uses much of the same process to control the vehicle's orientation as OHOLD2, however it is necessary to decouple yaw from pitch and roll. In other words, we want to construct a target quaternion using the user provided pitch and roll, but matching the vehicle's current yaw. The most intuitive option would be to decompose \\(q_c\\) into euler angles and obtain the yaw from those. However, this does not account for the fact that the yaw can be altered by pitch or roll. For example, (p=115, r=0, h=90) would be decomposed as (p=65, r=180, h=-90). Here the heading is 180 degrees off. This would be a significant issue. Thus instead, the target quaternion can be decomposed using the swing-twist decomposition of quaternion rotations. We twist about the world z axis (since this is the axis the vehicle initially yawed about). The vehicle's yaw can be calculated from this twist quaternion (using euler conversion formula). The target quaternion is constructed from the target pitch and roll provided by the user and the yaw provided by the twist quaternion as described above. This target quaternion is \\(q_t\\) and is used the same way described for OHOLD2. After calculating \\(w\\) as described for OHOLD2, there is one extra step before handling DoF scaling. The yaw speed ( h ) provided by the user must be transformed to the vehicle's axes and added to the \\(w\\) from the PIDs. This can be done similar to GLOBAL mode \\(\\left\\{0, w_{yaw}\\right\\} = q_{rot} \\left\\{0, \\begin{pmatrix}0 & 0 & h\\end{pmatrix}\\right\\} q_{rot}^*\\) \\(w_{yaw}\\) will then be upscaled just as in GLOBAL mode. Then \\(w = w + w_{yaw}\\) . This is then downscaled as described for OHOLD2.","title":"OHOLD1 Orientation Control"},{"location":"devs/math/#stability-assist-sassist-mode","text":"Stability assist mode (SASSIST) is simply OHOLD mode, with the addition of closed-loop control for vehicle depth. Thus, instead of providing a z speed (for motion along the gz-axis), a PID controller is used to achieve the target depth. Inputs to this mode are the same as OHOLD mode, however instead of z , d_t is provided. d_t : Target depth (meters; negative for below the surface) The PID used to maintain vehicle depth is referred to as the Depth PID. This PID uses the current vehicle depth (provided by a depth sensor) to calculate a z speed in the gz-axis, then provides it as an input to OHOLD mode. All other inputs from SASSIST are passed through to OHOLD mode as provided. Thus, there are two variants of SASSIST mode (variant 1 and variant 2) built ontop of the corresponding variants of OHOLD mode.","title":"Stability Assist (SASSIST) Mode"},{"location":"devs/math/#accumulated-euler-angles","text":"WARNING: Make sure you understand what these are and are not before using them. These are not normal euler angles. They are intended to allow high level software using the control board to a measurement of orientation about an individual axis that accumulates similar to a gyroscope. Generally, use of these is discouraged. They are provided as a \"compatibility bridge\" for code developed for a gyroscope system. WARNING: Accumulated euler angles are not the current orientation of the vehicle as an euler angle! The euler and quaternion values provided by the IMU are not directly useful for tracking multiple rotations of the vehicle. Unlike simply integrating gyroscope data, euler angles (pitch, roll, yaw) and quaternions do not track the number of times the vehicle has rotated about a particular axis. They only track the vehicle's orientation in space. While integrating raw gyro data would provide an accumulating measurement, such a solution would be rotations about the robot's axes. Generally, it is more useful to track the number of times the vehicle has \"pitched\", \"rolled\" or \"yawed\". Note that an accumulated gyro z angle of 500 does not necessarily mean the robot has yawed 500 degrees; the robot could have been oriented at a pitch of 90. Instead, we want to measure how many degrees the vehicle has rotated through. This can be done by tracking changes between subsequent quaternions from the IMU. The idea is to compare each quaternion read from the IMU with the previous quaternion received from the IMU (note that quaternions of all zeros are ignored to avoid issues with invalid IMU data samples after first configuring the sensor). For each quaternion read from the IMU: Calculate the shortest set of rotations from the previous to the current quaternion Convert the shortest angle to euler angles Add the pitch, roll, and yaw from the shortest euler angles to accumulated pitch, roll, and yaw variables Note that if the IMU axis config changes, the accumulated data should be zeroed and the previously read quaternion discarded. This makes the assumption that the smallest rotation between two quaternions is the most probable path the robot took to change its orientation. This is an approximation, however it is a fairly good one as long as sample rate of data is sufficiently high. The specifics of the path are lost, however, if the sample rate is high enough, the length of the path is sufficiently small that and this is a good approximation. The second issue with this approximation has to do with rotations exceeding 180 degrees. The method for determining shortest path between two quaternions will be incorrect if the vehicle rotates more than 180 degrees in any axis (because the shortest path would have involved rotating the other direction). To guarantee rotations between two samples never exceed 180 degrees, the max measured rotation rate of the IMU is considered. For the BNO055 this is 2000 degrees per second. Thus, with a sample period of \\(l\\) milliseconds, the largest angle change between samples is \\(\\frac{2000 \\textrm{ deg}}{1 \\textrm{ sec}} \\cdot \\frac{1 \\textrm{ sec}}{1000 \\textrm{ ms}} \\cdot \\frac{l \\textrm{ ms}}{1 \\textrm{ sample}} = 2l \\textrm{ deg / sample}\\) To ensure that changes of more than 180 degrees do not occur, the following must be satisfied \\(2l < 180 \\rightarrow l < 90 \\textrm{ milliseconds}\\) However, it is possible for some samples from the IMU to be delayed (ie I2C bus busy with another sensor) or lost (I2C failure). Thus, it is necessary to choose a value for \\(l\\) that allows for at least one sample to be lost. When a sample is lost, this doubles the effective time between samples. Thus, it is necessary to half \\(l\\) \\(l < 45 \\textrm{ milliseconds}\\) By further reducing \\(l\\) it is possible to allow for larger delays or more lost samples. The current firmware samples IMU data every 15ms (the max rate supported by the BNO055 in fusion mode is 100Hz = 10ms period). Using \\(l=15 \\textrm{ ms}\\) it is possible for 5 consecutive samples to be lost while still guaranteeing that no more than 90ms passes between valid samples (thus still ensuring no more than 180 degree change between samples).","title":"Accumulated Euler Angles"},{"location":"devs/math/#other-derivations","text":"","title":"Other Derivations"},{"location":"devs/math/#euler-to-quaternion-conversion","text":"The euler angle convention used for the control board is an intrinsic set z-x'-y'' (rotate about z, then about new x, then about new y to compose an angle). Using the definitions of front/right/top for this coordinate system, this means that roll is about y, pitch is about x, and yaw is about z. A rotation quaternion, \\(Q\\) , can be composed using three rotations, each being one of the rotations used to construct the euler angle representation (in order). \\(Q = Q_z Q_x Q_y\\) Quaternion multiplication is associative. Recall that right-multiplied quaternions are applied in the vehicle frame during composition. Thus, when viewed left to right: First, apply \\(Q_z\\) in vehicle frame (also equal to world frame since this is the first rotation) Then, apply \\(Q_x\\) in the vehicle frame Finally, apply \\(Q_y\\) in the vehicle frame However, recall that left-multiplied quaternions are applied in the world frame during composition. Thus, when viewed right to left First, apply \\(Q_y\\) in the world frame Then, apply \\(Q_x\\) in the world frame Finally, apply \\(Q_z\\) in the world frame In other words, the following are equivalent Yaw about z , then pitch about x' , then roll about y'' Roll about y , then pitch about x , the yaw about z These use the same pitch, roll, and yaw angles , but the latter applies them about world axes , which have known and trivial direction vectors. Thus, each of pitch, roll, and yaw are a rotation about a known axis (x, y, or z respectively). Converting these from axis-angle from, the quaternions are represented as follows \\(Q_y = \\left\\{ cos(\\frac{roll}{2}), sin(\\frac{roll}{2}) \\begin{pmatrix} 0 \\\\ 1 \\\\ 0 \\end{pmatrix} \\right\\}\\) \\(Q_x = \\left\\{ cos(\\frac{pitch}{2}), sin(\\frac{pitch}{2}) \\begin{pmatrix} 1 \\\\ 0 \\\\ 0 \\end{pmatrix} \\right\\}\\) \\(Q_z = \\left\\{ cos(\\frac{yaw}{2}), sin(\\frac{yaw}{2}) \\begin{pmatrix} 0 \\\\ 0 \\\\ 1 \\end{pmatrix} \\right\\}\\) By multiplying these, Q can be obtained. This is relatively simple to do because each of the vectors defining the quaternion is a trivial vector (this is because of using world axes!) \\(Q = \\left\\{w, \\begin{pmatrix}x \\\\ y \\\\ z\\end{pmatrix}\\right\\} = Q_z Q_x Q_y\\) let \\(cp = cos(\\frac{pitch}{2})\\) , \\(sp = sin(\\frac{pitch}{2})\\) let \\(cr = cos(\\frac{roll}{2})\\) , \\(sr = sin(\\frac{roll}{2})\\) let \\(cy = cos(\\frac{yaw}{2})\\) , \\(sr = sin(\\frac{yaw}{2})\\) \\(w = cy \\cdot cp \\cdot cr - sy \\cdot sp \\cdot sr\\) \\(x = cy \\cdot sp \\cdot cr - sy \\cdot cp \\cdot sr\\) \\(y = sy \\cdot sp \\cdot cr + cy \\cdot cp \\cdot sr\\) \\(z = sy \\cdot cp \\cdot cr + cy \\cdot sp \\cdot sr\\) This provides a set of equations which can be used to convert from an euler angle representation to a quaternion.","title":"Euler to Quaternion Conversion"},{"location":"devs/math/#quaternion-to-euler-conversion","text":"Converting from quaternion to euler uses the rotation matrix representation as a go-between. For the intrinsic set of euler angles z-x'-y'' , a rotation matrix can be composed as follows (recall that right multiply applies about vehicle axis). \\(R = R_z(yaw) R_x(pitch) R_y(roll)\\) Similar to the quaternion case described above, note that matrix multiplication is associative. Thus, this can instead be interpreted as rotations about world axes in the opposite order. Thus, each of \\(R_x\\) , \\(R_y\\) , and \\(R_z\\) are rotations about world x, y, and z axes. Such matrices have known forms. When multiplied out, the following representation of R is obtained. let \\(cp = cos(pitch)\\) , \\(sp = sin(pitch)\\) let \\(cr = cos(roll)\\) , \\(sr = sin(roll)\\) let \\(cy = cos(yaw)\\) , \\(sy = sin(yaw)\\) \\(R = \\begin{pmatrix} cy \\cdot cr - sr \\cdot sy \\cdot sp & -sy \\cdot cp & cy \\cdot sr + sy \\cdot sp \\cdot cr \\\\ sy \\cdot cr + sr \\cdot sp \\cdot cy & cy \\cdot cp & sy \\cdot sr - cr \\cdot sp \\cdot cy \\\\ -cp \\cdot sr & sp & cp \\cdot cr \\end{pmatrix}\\) Using entries of this matrix, the following relations can be constructed \\(R_{32} = sin(pitch) \\rightarrow pitch = sin^{-1}(R_{32})\\) \\(\\frac{R_{31}}{R_{33}} = \\frac{-sin(roll)cos(pitch)}{cos(roll)cos(pitch)} = tan(roll) \\rightarrow roll = tan^{-1}(\\frac{-R_{31}}{R_{33}})\\) \\(\\frac{R_{12}}{R_{22}} = \\frac{sin(yaw)cos(pitch)}{cos(yaw)cos(pitch)} = tan(yaw) \\rightarrow yaw = tan^{-1}(\\frac{R_{12}}{R_{22}})\\) A quaternion can also be converted to a rotation matrix with a known form. Using cells from this quaternion-backed rotation matrix yields the following relations \\(pitch = sin^{-1}(2(yz+wx))\\) \\(roll = tan^{-1}(\\frac{2(wy-xz)}{1-2(x^2+y^2)})\\) \\(yaw = tan^{-1}(\\frac{2(xy-wz)}{1-2(x^2 + z^2)})\\) Note that arctangent should be implemented in code using the quadrant aware atan2 to account for quadrants properly and avoid divide by zero issues.","title":"Quaternion to Euler Conversion"},{"location":"devs/overview/","text":"Overview TODO: Project Structure and Build System TODO: Hardware abstraction (not really proper abstraction, just multiple implementatiuons of the same external API defined in include/hardware headers; each target has implementation in src/hardware files using include guards to choose which implementation) TODO: Generator projects TODO: System architecture","title":"Firmware Overview"},{"location":"devs/overview/#overview","text":"TODO: Project Structure and Build System TODO: Hardware abstraction (not really proper abstraction, just multiple implementatiuons of the same external API defined in include/hardware headers; each target has implementation in src/hardware files using include guards to choose which implementation) TODO: Generator projects TODO: System architecture","title":"Overview"},{"location":"devs/pythoniface/","text":"Interface Scripts TODO: Implementation details / architecture","title":"Interface Scripts"},{"location":"devs/pythoniface/#interface-scripts","text":"TODO: Implementation details / architecture","title":"Interface Scripts"},{"location":"devs/simulation/","text":"Simulation SimCB SimCB is the name used for the control board firmware built as a binary to run on a PC operating system (Windows, macOS, Linux). This is achieved using the FreeRTOS windows or posix ports. Instead of using UART over USB (as real hardware does), SimCB opens TCP sockets (server) for communication. The code using the control board would then connect to this socket (as a client) instead of opening a UART port to talk with a physical control board. The communication protocol (message structure and format) is exactly the same as described for UART. The SimCB allows a control board to run without hardware. The same firmware that would run on the board instead runs on your PC. There are a few limitations with this. SimCB will only operate in simhijack mode (it cannot be released from simhijack) and only the sim sensors will be available. TODO: Details on where this code lives, how TCP is handled and why. Simulation Hijack TODO: Simhijack and support for this in firmware (including multiple sensor stuff) TODO: Why? Allow a simulated environment to use a control board to actually control a simulated vehicle. Allows motion testing / validation without in-water time (with a real control board). Also allows debugging / testing actual firmware without in-water time with a vehicle. TODO: By combining with SimCB (which can also be simhijacked), motion can be tested without in-water time and without a real control board. Simulator Implementation & Development The simulator is implemented using the Godot game engine (3.5). This provides a 3D rendering and physics engine. GodotAUVSim on GitHub This simulator was initially designed to allow control board firmware development without in-water testing time with a physical vehicle. It has expanded into a reference simulator for end user use. It does not model any environment, just the vehicle(s). TODO: Implementation details / tech docs TODO: Docs on adding vehicles, etc Control Board Setups Thus, there are four ways a \"control board\" can be used Real control board over UART SimCB over TCP Simulator models environment and provides inputs to control board and retrieves outputs from control board Can also do this with SimCB. Simulator is a \"man in the middle\"","title":"Simulation Support"},{"location":"devs/simulation/#simulation","text":"","title":"Simulation"},{"location":"devs/simulation/#simcb","text":"SimCB is the name used for the control board firmware built as a binary to run on a PC operating system (Windows, macOS, Linux). This is achieved using the FreeRTOS windows or posix ports. Instead of using UART over USB (as real hardware does), SimCB opens TCP sockets (server) for communication. The code using the control board would then connect to this socket (as a client) instead of opening a UART port to talk with a physical control board. The communication protocol (message structure and format) is exactly the same as described for UART. The SimCB allows a control board to run without hardware. The same firmware that would run on the board instead runs on your PC. There are a few limitations with this. SimCB will only operate in simhijack mode (it cannot be released from simhijack) and only the sim sensors will be available. TODO: Details on where this code lives, how TCP is handled and why.","title":"SimCB"},{"location":"devs/simulation/#simulation-hijack","text":"TODO: Simhijack and support for this in firmware (including multiple sensor stuff) TODO: Why? Allow a simulated environment to use a control board to actually control a simulated vehicle. Allows motion testing / validation without in-water time (with a real control board). Also allows debugging / testing actual firmware without in-water time with a vehicle. TODO: By combining with SimCB (which can also be simhijacked), motion can be tested without in-water time and without a real control board.","title":"Simulation Hijack"},{"location":"devs/simulation/#simulator-implementation-development","text":"The simulator is implemented using the Godot game engine (3.5). This provides a 3D rendering and physics engine. GodotAUVSim on GitHub This simulator was initially designed to allow control board firmware development without in-water testing time with a physical vehicle. It has expanded into a reference simulator for end user use. It does not model any environment, just the vehicle(s). TODO: Implementation details / tech docs TODO: Docs on adding vehicles, etc","title":"Simulator Implementation & Development"},{"location":"devs/simulation/#control-board-setups","text":"Thus, there are four ways a \"control board\" can be used Real control board over UART SimCB over TCP Simulator models environment and provides inputs to control board and retrieves outputs from control board Can also do this with SimCB. Simulator is a \"man in the middle\"","title":"Control Board Setups"},{"location":"hardware/sensors/","text":"Off-Board Sensors The control board has an integrated IMU (gyro + accel) and is not designed to support off-board IMUs. However, for full functionality, vehicle depth data is also required. This data is provided by an off-board depth sensor. Currently, only one such sensor is supported: MS5837-30BA pressure sensor based depth sensors. The sensor used during development and tested with the control board is BlueRobotics' Bar30 Sensor . The depth sensor is to be connected to the control board via the \"Depth I2C\" header. See the pinout images on the v1 or v2 hardware pages for more details. If using the BlueRobotics sensor, you will need to cut off the connector it comes with and install \"DuPont\" connectors.","title":"Off-Board Sensors"},{"location":"hardware/sensors/#off-board-sensors","text":"The control board has an integrated IMU (gyro + accel) and is not designed to support off-board IMUs. However, for full functionality, vehicle depth data is also required. This data is provided by an off-board depth sensor. Currently, only one such sensor is supported: MS5837-30BA pressure sensor based depth sensors. The sensor used during development and tested with the control board is BlueRobotics' Bar30 Sensor . The depth sensor is to be connected to the control board via the \"Depth I2C\" header. See the pinout images on the v1 or v2 hardware pages for more details. If using the BlueRobotics sensor, you will need to cut off the connector it comes with and install \"DuPont\" connectors.","title":"Off-Board Sensors"},{"location":"hardware/v1/","text":"AUV Control Board v1 Note: Control Board v1 Units must update Adafruit's bootloader before first use. This only needs to be done once, but it must be done. Follow Adafruit's instructions here . DO NOT SKIP THIS!!! Note that some pictures show a version with a pressure / temperature sensor. This is no longer used and should not be included when building the board. The default coordinate system as defined by the IMU is shown below. Note that this is a right hand coordinate system. The red arrows define axes. Rotation about these axes is in the right hand direction (indicated by green arrows). Notice that the green arrows are on top of the red axis arrows, thus a \"left to right\" arrow is left to right across the top of the axis. The axis configuration can be changed to match any plane-aligned mounting position of the control board on a robot. See the BNO055 datasheet for more information. Components 1x Adafruit ItsyBitsy M4 1x Adafruit BNO055 Breakout STEMMA QT version can be substituted, however be aware that the pin order is different. Protoboard (2.54mm spacing; 24 by 18 holes; 5cm by 7cm) These can be found from many vendors Solid Core Wire (22AWG) Female Pin Headers (2.54mm pitch) Two 1x14 headers One 1x6 header One 1x4 header Note: These can be cut between pins carefully Male pin headers (2.54mm pitch) Two 1x8 headers One 1x4 header Two 1x2 headers (optional; for debug interface) Note: These can easily be cut / broken between pins. Two 10K resistors (through hole, 1/4 W) Wiring Diagram Assembly Instructions Solder header strips in the positions shown below. The female headers avoid soldering breakouts / dev boards directly to protoboard which allows easily replacing components if needed (or reusing them for other purposes later). After soldering, breakouts can be populated to make identifying pins easier. The crossed out header can be omitted as it was formerly used for the pressure / temp sensor (not used anymore). Wire the protoboard according to the wiring diagram. Note that the first image shows the pressure / temperature sensor (this should be omitted). The first photo also does not show the pullup resistors or debug headers. The second picture is closer to what a fully assembled Control Board v1 should look like. Hot glue can be used to ensure the resistor leads do not short anything. This photo is an older assembly that includes the temp / pressure sensor (should be omitted) and excludes the pullup resistors and debug headers (should be included). This photo shows the pullup resistors and debug headers Back side wiring","title":"Version 1"},{"location":"hardware/v1/#auv-control-board-v1","text":"Note: Control Board v1 Units must update Adafruit's bootloader before first use. This only needs to be done once, but it must be done. Follow Adafruit's instructions here . DO NOT SKIP THIS!!! Note that some pictures show a version with a pressure / temperature sensor. This is no longer used and should not be included when building the board. The default coordinate system as defined by the IMU is shown below. Note that this is a right hand coordinate system. The red arrows define axes. Rotation about these axes is in the right hand direction (indicated by green arrows). Notice that the green arrows are on top of the red axis arrows, thus a \"left to right\" arrow is left to right across the top of the axis. The axis configuration can be changed to match any plane-aligned mounting position of the control board on a robot. See the BNO055 datasheet for more information.","title":"AUV Control Board v1"},{"location":"hardware/v1/#components","text":"1x Adafruit ItsyBitsy M4 1x Adafruit BNO055 Breakout STEMMA QT version can be substituted, however be aware that the pin order is different. Protoboard (2.54mm spacing; 24 by 18 holes; 5cm by 7cm) These can be found from many vendors Solid Core Wire (22AWG) Female Pin Headers (2.54mm pitch) Two 1x14 headers One 1x6 header One 1x4 header Note: These can be cut between pins carefully Male pin headers (2.54mm pitch) Two 1x8 headers One 1x4 header Two 1x2 headers (optional; for debug interface) Note: These can easily be cut / broken between pins. Two 10K resistors (through hole, 1/4 W)","title":"Components"},{"location":"hardware/v1/#wiring-diagram","text":"","title":"Wiring Diagram"},{"location":"hardware/v1/#assembly-instructions","text":"Solder header strips in the positions shown below. The female headers avoid soldering breakouts / dev boards directly to protoboard which allows easily replacing components if needed (or reusing them for other purposes later). After soldering, breakouts can be populated to make identifying pins easier. The crossed out header can be omitted as it was formerly used for the pressure / temp sensor (not used anymore). Wire the protoboard according to the wiring diagram. Note that the first image shows the pressure / temperature sensor (this should be omitted). The first photo also does not show the pullup resistors or debug headers. The second picture is closer to what a fully assembled Control Board v1 should look like. Hot glue can be used to ensure the resistor leads do not short anything. This photo is an older assembly that includes the temp / pressure sensor (should be omitted) and excludes the pullup resistors and debug headers (should be included). This photo shows the pullup resistors and debug headers Back side wiring","title":"Assembly Instructions"},{"location":"hardware/v2/","text":"AUV Control Board v2 The default coordinate system as defined by the IMU is shown below. Note that this is a right hand coordinate system. The red arrows define axes. Rotation about these axes is in the right hand direction (indicated by green arrows). Notice that the green arrows are on top of the red axis arrows, thus a \"left to right\" arrow is left to right across the top of the axis. The axis configuration can be changed to match any plane-aligned mounting position of the control board on a robot. See the BNO055 datasheet for more information. Components 1x WeAct Studio Black Pill (w / STM32F411 notSTM32F401 ) Note: There are many counterfeit boards sold. These should be avoided. Adafruit Store Aliexpress (Offical WeAct Studio Store) 1x Adafruit BNO055 Breakout STEMMA QT version can be substituted, however be aware that the pin order is different. Protoboard (2.54mm spacing; 24 by 18 holes; 5cm by 7cm) These can be found from many vendors Solid Core Wire (22AWG) Female Pin Headers (2.54mm pitch) Two 1x20 headers One 1x6 header One 1x4 header Note: These can be cut between pins carefully Male pin headers (2.54mm pitch) Two 1x8 headers One 1x4 header Note: These can easily be cut / broken between pins. Resistors (through hole, 1/4 W) Two 10K resistors Two 10 Ohm resistors One 100 Ohm resistor One QBL8RGB60D0-2897 RGB LED Wiring Diagram Assembly Instructions Solder header strips in the positions shown below. The female headers avoid soldering breakouts / dev boards directly to protoboard which allows easily replacing components if needed (or reusing them for other purposes later). After soldering, breakouts can be populated to make identifying pins easier. The location of the LED is also indicated here. Wire the protoboard according to the wiring diagram.","title":"Version 2"},{"location":"hardware/v2/#auv-control-board-v2","text":"The default coordinate system as defined by the IMU is shown below. Note that this is a right hand coordinate system. The red arrows define axes. Rotation about these axes is in the right hand direction (indicated by green arrows). Notice that the green arrows are on top of the red axis arrows, thus a \"left to right\" arrow is left to right across the top of the axis. The axis configuration can be changed to match any plane-aligned mounting position of the control board on a robot. See the BNO055 datasheet for more information.","title":"AUV Control Board v2"},{"location":"hardware/v2/#components","text":"1x WeAct Studio Black Pill (w / STM32F411 notSTM32F401 ) Note: There are many counterfeit boards sold. These should be avoided. Adafruit Store Aliexpress (Offical WeAct Studio Store) 1x Adafruit BNO055 Breakout STEMMA QT version can be substituted, however be aware that the pin order is different. Protoboard (2.54mm spacing; 24 by 18 holes; 5cm by 7cm) These can be found from many vendors Solid Core Wire (22AWG) Female Pin Headers (2.54mm pitch) Two 1x20 headers One 1x6 header One 1x4 header Note: These can be cut between pins carefully Male pin headers (2.54mm pitch) Two 1x8 headers One 1x4 header Note: These can easily be cut / broken between pins. Resistors (through hole, 1/4 W) Two 10K resistors Two 10 Ohm resistors One 100 Ohm resistor One QBL8RGB60D0-2897 RGB LED","title":"Components"},{"location":"hardware/v2/#wiring-diagram","text":"","title":"Wiring Diagram"},{"location":"hardware/v2/#assembly-instructions","text":"Solder header strips in the positions shown below. The female headers avoid soldering breakouts / dev boards directly to protoboard which allows easily replacing components if needed (or reusing them for other purposes later). After soldering, breakouts can be populated to make identifying pins easier. The location of the LED is also indicated here. Wire the protoboard according to the wiring diagram.","title":"Assembly Instructions"},{"location":"user_guide/calibration/","text":"Sensor Calibration BNO055 (IMU) Calibration Calibration Script It is highly recommended to use the calibration script to calibrate the BNO055. Even if you are not using the python interface to the control board, the calibration script can be used to manage the calibration state. Launch the script by using the following command (note that python3 along with the pyserial library must be installed). Run this command in the iface folder. Replace [PORT] with the serial port of the control board. python3 launch.py example/bno055_calibrate.py -p [PORT] If a calibration is currently saved to the control board, the script will show that configuration and prompt you to erase it. You must erase any stored configuration before you can recalibrate the BNO055. Refusing to erase the calibration will result in the script exiting with no further action taken (this can be used as a way to view the currently saved calibration). If the above prompt is not shown, there is no calibration currently saved on the control board. Next, the script will prompt you to run either \"guided\" or \"manual\" calibration. Guided calibration provides instructions to use the BNO055's automatic calibration routine. This is recommended. Manual calibration allows a user to manually enter values for each calibration constant. Select guided mode. Next, instructions on the calibration process will be provided. Read and follow the instructions. Then press enter to begin calibration. The script will then periodically show you the calibration status of both sensor. Perform the described motions until the calibration status of both sensors shows as \"3\". When both values show \"3\", the script will automatically retrieve the calibration generated by the BNO055. It will be shown and you will be prompted to save the calibration to the control board. If you choose to save the calibration, it will be applied each time the control board resets or powers on until the calibration is erased or changed. This is generally what you will want to do . Choosing not to save the calibration will not alter the calibration state of the device until reset. The BNO055 will retain it's calibration and re-running the calibration script (guided) will result in the calibration immediately being \"good\" (all sensors show \"3\"). However, resetting the control board (or power cycling it) will cause the calibration to be lost. Details This section provides details on how BNO055 calibration works. If attempting to implement a calibration routine yourself instead of using the calibration script, this information will be useful. Otherwise, probably not. The BNO055 IMU is calibrated using a set of \"calibration constants\". These constants can be stored to the control board so that they are applied to the sensor when the control board powers up. If not calibration constants are stored on the control board, the IMU will instead run an automatic calibration routine in the background. This routine can be used to generate calibration constants for its sensors. When a set of calibration constants is saved to the control board, they will be applied to the BNO055 each time the control board is powered on or is reset. When the control board applies a saved calibration to the BNO055, this disables the BNO055's automatic calibration routine. Thus, it is necessary to erase any calibration stored on the control board before recalibrating the BNO055. Note that the act of saving a calibration to or erasing a calibration from the control board will reconfigure the BNO055. Thus, saving a calibration to the control board will disable the BNO055's calibration routine whereas erasing a calibration from the control board will enable the BNO055's calibration routine. This means that a power cycle of the control board is not required after saving / erasing a calibration. There are two places calibration constants could be located Saved on the control board (referred to as the stored calibration constants) On the BNO055 (referred to as the live calibration constants) The stored calibration constants are written to the BNO055 when the control board starts. Thus, if any constants are saved, the live calibration constants will be the same as the stored calibration constants. However, if there are no stored calibration constants (they are erased or have never been stored), the live calibration constants will be generated by the BNO055's automatic calibration routine. The live calibration constants are only valid when the BNO055's calibration status register indicates a status code of 3 for each sensor in use (accelerometer and gyroscope). The control board provides commands to manage both the stored calibration constants as well as read the live calibration constants and calibration status directly from the BNO055. The python interface scripts provide functions to do so as well. The read_stored_bno055_calibration , erase_stored_bno055_calibration , and store_bno055_calibration functions manage the stored calibration constants. The bno055_read_calibration and bno055_read_calibration_status functions will read the live calibration constants and the calibration status form the BNO055. Erase any stored calibration. This will reset the IMU without applying any calibration. Thus, the live calibration constants will be generated by the BNO055's calibration routine. Reset the BNO055 (this is done via command to the control board). This will ensure that any calibration already generated is erased. Read the BNO055's calibration status register. Bitwise operations can be used to determine the calibration status of individual sensors. Each sensor is 2-bits of data. See the BNO055 datasheet for more information on the CALIB_STAT register. For the control board, only the accelerometer (ACC) and gyroscope (GYR) calibration status is relevant. Each of these will be a value from 0-3. A value of 3 indicates a good calibration. Perform the required operations described in the BNO055 datasheet for gyro and accelerometer calibration. Once the BNO055's calibration status reads 3 for both sensors, the calibration is complete. Read the live calibration constants from the BNO055 Store the calibration constants read in the previous step to the control board. The BNO055 will be reset, but the calibration will be applied. This same calibration that was stored to the control board will be applied to the BNO055 each time the control board is reset (or powered on). When to Re-Calibrate You should always recalibrate if you change the physical sensor in use on the control board (swap a different BNO055) or if you are using a different control board. Calibration constants will vary between sensors, thus constants from one control board cannot be used on another. Likewise, if you change the sensor on your control board the old constants will no longer be valid. Additionally, significant operating environment changes (pressure, temperature, elevation, etc) can cause enough of a change in sensor behavior to require re-calibration. Similarly, a change of the position of the sensor in the vehicle could require re-calibration. It is recommended to recalibrate if any such changes seem to result in degradation of sensor performance. If the BNO055 axis configuration used by your vehicle changes, the BNO055 should be recalibrated. MS5837 (Depth Sensor) Calibration The depth sensor is really just a pressure & temperature sensor. The depth is calculated using two \"constant\" values: the density of the water the vehicle is operating in and the atmospheric pressure at the surface of the water. For the MS5837 atmospheric pressure must be provided in Pascals and the fluid density must be provided in kg / m^3. The default values are Atmospheric Pressure: 101325 Pa Fluid Density: 997 kg / m^3 These default values are applied on control board reset / power on. MS5837 calibration is not persistent (it cannot be stored on the control board like the BNO055 can). These values can be adjusted using a command sent to the control board or by using the python interface script. The fluid density must be provided by the user (there is no good way to experimentally determine this on the vehicle). However, the pressure can be measured while the vehicle (and sensor) are in air at / above the surface. The example interface script ms5837_calibration.py demonstrates calibration of the depth sensor. Again, since the calibration is not persistent it must be applied by command / interface script function each time the device is started. Note that depth sensor calibration is not persistent due to this being of limited use. The atmospheric conditions the vehicle is operating in will change with time. Thus, any persistent configuration would not be trusted and would be frequently overwritten.","title":"Sensor Calibration"},{"location":"user_guide/calibration/#sensor-calibration","text":"","title":"Sensor Calibration"},{"location":"user_guide/calibration/#bno055-imu-calibration","text":"","title":"BNO055 (IMU) Calibration"},{"location":"user_guide/calibration/#calibration-script","text":"It is highly recommended to use the calibration script to calibrate the BNO055. Even if you are not using the python interface to the control board, the calibration script can be used to manage the calibration state. Launch the script by using the following command (note that python3 along with the pyserial library must be installed). Run this command in the iface folder. Replace [PORT] with the serial port of the control board. python3 launch.py example/bno055_calibrate.py -p [PORT] If a calibration is currently saved to the control board, the script will show that configuration and prompt you to erase it. You must erase any stored configuration before you can recalibrate the BNO055. Refusing to erase the calibration will result in the script exiting with no further action taken (this can be used as a way to view the currently saved calibration). If the above prompt is not shown, there is no calibration currently saved on the control board. Next, the script will prompt you to run either \"guided\" or \"manual\" calibration. Guided calibration provides instructions to use the BNO055's automatic calibration routine. This is recommended. Manual calibration allows a user to manually enter values for each calibration constant. Select guided mode. Next, instructions on the calibration process will be provided. Read and follow the instructions. Then press enter to begin calibration. The script will then periodically show you the calibration status of both sensor. Perform the described motions until the calibration status of both sensors shows as \"3\". When both values show \"3\", the script will automatically retrieve the calibration generated by the BNO055. It will be shown and you will be prompted to save the calibration to the control board. If you choose to save the calibration, it will be applied each time the control board resets or powers on until the calibration is erased or changed. This is generally what you will want to do . Choosing not to save the calibration will not alter the calibration state of the device until reset. The BNO055 will retain it's calibration and re-running the calibration script (guided) will result in the calibration immediately being \"good\" (all sensors show \"3\"). However, resetting the control board (or power cycling it) will cause the calibration to be lost.","title":"Calibration Script"},{"location":"user_guide/calibration/#details","text":"This section provides details on how BNO055 calibration works. If attempting to implement a calibration routine yourself instead of using the calibration script, this information will be useful. Otherwise, probably not. The BNO055 IMU is calibrated using a set of \"calibration constants\". These constants can be stored to the control board so that they are applied to the sensor when the control board powers up. If not calibration constants are stored on the control board, the IMU will instead run an automatic calibration routine in the background. This routine can be used to generate calibration constants for its sensors. When a set of calibration constants is saved to the control board, they will be applied to the BNO055 each time the control board is powered on or is reset. When the control board applies a saved calibration to the BNO055, this disables the BNO055's automatic calibration routine. Thus, it is necessary to erase any calibration stored on the control board before recalibrating the BNO055. Note that the act of saving a calibration to or erasing a calibration from the control board will reconfigure the BNO055. Thus, saving a calibration to the control board will disable the BNO055's calibration routine whereas erasing a calibration from the control board will enable the BNO055's calibration routine. This means that a power cycle of the control board is not required after saving / erasing a calibration. There are two places calibration constants could be located Saved on the control board (referred to as the stored calibration constants) On the BNO055 (referred to as the live calibration constants) The stored calibration constants are written to the BNO055 when the control board starts. Thus, if any constants are saved, the live calibration constants will be the same as the stored calibration constants. However, if there are no stored calibration constants (they are erased or have never been stored), the live calibration constants will be generated by the BNO055's automatic calibration routine. The live calibration constants are only valid when the BNO055's calibration status register indicates a status code of 3 for each sensor in use (accelerometer and gyroscope). The control board provides commands to manage both the stored calibration constants as well as read the live calibration constants and calibration status directly from the BNO055. The python interface scripts provide functions to do so as well. The read_stored_bno055_calibration , erase_stored_bno055_calibration , and store_bno055_calibration functions manage the stored calibration constants. The bno055_read_calibration and bno055_read_calibration_status functions will read the live calibration constants and the calibration status form the BNO055. Erase any stored calibration. This will reset the IMU without applying any calibration. Thus, the live calibration constants will be generated by the BNO055's calibration routine. Reset the BNO055 (this is done via command to the control board). This will ensure that any calibration already generated is erased. Read the BNO055's calibration status register. Bitwise operations can be used to determine the calibration status of individual sensors. Each sensor is 2-bits of data. See the BNO055 datasheet for more information on the CALIB_STAT register. For the control board, only the accelerometer (ACC) and gyroscope (GYR) calibration status is relevant. Each of these will be a value from 0-3. A value of 3 indicates a good calibration. Perform the required operations described in the BNO055 datasheet for gyro and accelerometer calibration. Once the BNO055's calibration status reads 3 for both sensors, the calibration is complete. Read the live calibration constants from the BNO055 Store the calibration constants read in the previous step to the control board. The BNO055 will be reset, but the calibration will be applied. This same calibration that was stored to the control board will be applied to the BNO055 each time the control board is reset (or powered on).","title":"Details"},{"location":"user_guide/calibration/#when-to-re-calibrate","text":"You should always recalibrate if you change the physical sensor in use on the control board (swap a different BNO055) or if you are using a different control board. Calibration constants will vary between sensors, thus constants from one control board cannot be used on another. Likewise, if you change the sensor on your control board the old constants will no longer be valid. Additionally, significant operating environment changes (pressure, temperature, elevation, etc) can cause enough of a change in sensor behavior to require re-calibration. Similarly, a change of the position of the sensor in the vehicle could require re-calibration. It is recommended to recalibrate if any such changes seem to result in degradation of sensor performance. If the BNO055 axis configuration used by your vehicle changes, the BNO055 should be recalibrated.","title":"When to Re-Calibrate"},{"location":"user_guide/calibration/#ms5837-depth-sensor-calibration","text":"The depth sensor is really just a pressure & temperature sensor. The depth is calculated using two \"constant\" values: the density of the water the vehicle is operating in and the atmospheric pressure at the surface of the water. For the MS5837 atmospheric pressure must be provided in Pascals and the fluid density must be provided in kg / m^3. The default values are Atmospheric Pressure: 101325 Pa Fluid Density: 997 kg / m^3 These default values are applied on control board reset / power on. MS5837 calibration is not persistent (it cannot be stored on the control board like the BNO055 can). These values can be adjusted using a command sent to the control board or by using the python interface script. The fluid density must be provided by the user (there is no good way to experimentally determine this on the vehicle). However, the pressure can be measured while the vehicle (and sensor) are in air at / above the surface. The example interface script ms5837_calibration.py demonstrates calibration of the depth sensor. Again, since the calibration is not persistent it must be applied by command / interface script function each time the device is started. Note that depth sensor calibration is not persistent due to this being of limited use. The atmospheric conditions the vehicle is operating in will change with time. Thus, any persistent configuration would not be trusted and would be frequently overwritten.","title":"MS5837 (Depth Sensor) Calibration"},{"location":"user_guide/comm_protocol/","text":"Communication Protocol Communication with the control board relies on sending messages between the control board and PC. This section focuses on how messages are sent, not what messages are sent. Hardware Communication Layer Messages are sent to the control board over the MCU's builtin USB port. The control board acts as a USB ACM CDC device. In practice, this means that it shows up as a serial (UART) port on the computer it is connected to. However, baud rate settings are irrelevant (and changing baud rates has no effect). As such, messages are sent to / received from the control board using \"UART\" with an undefined baud rate*. It is still necessary to set a baud rate when opening a UART port (as that information is provided to the device on the other side), but the rate is unused. Additionally, it is expected to operate in 8N1 data mode (8 data bits, no parity, 1 stop bit). A stream of data is sent to the control board over this port. This data is interpreted as described below. * NOTE: The baud rate 1200 is an exception. Openening then closing the port at this baud rate is used to trigger the control board to reboot to its bootloader. Do not use 1200 baud! Message Format and Construction The messages sent to / received from the control board have a specific format. Each message transfers a raw set of bytes (unsigned byte array). This set of bytes is the \"payload data\" of the message. The \"payload data\" is the data that is actually being send via the message. Messages are limited to a maximum payload size of 96 bytes. To be able to identify what data is part of a single message, it is necessary to add some additional information around the payload. The control board uses a special byte to indicate the start of a message ( START_BYTE ) and another one to identify the end of a message ( END_BYTE ). Since the payload could itself contain a start or end byte, there is also an escape byte ( ESCAPE_BYTE ) used to escape a START_BYTE , END_BYTE , or an ESCAPE_BYTE in the payload. START_BYTE becomes ESCAPE_BYTE , START_BYTE END_BYTE becomes ESCAPE_BYTE , END_BYTE ESCAPE_BYTE becomes ESCAPE_BYTE , ESCAPE_BYTE This is similar to escaping a quote in a string using a backslash. For the control board: START_BYTE = 253 (unsigned 8-bit) = -3 (signed 8-bit) END_BYTE = 254 (unsigned 8-bit) = -2 (signed 8-bit) ESCAPE_BYTE = 255 (unsigned 8-bit) = -1 (signed 8-bit) In addition to the control bytes and the payload, each message contains two other pieces of information: First, each message includes a prepended* ID number (16-bit unsigned big-endian integer). These ID numbers are required to be unique only in one direction . This means that two messages sent from the PC to the control board cannot have the same id. Likewise, two messages sent from the control board to the PC cannot have the same id. However, a message sent from PC to control board can have the same id as another message sent from control board to PC. Note that in practice, eventually (after 65535 messages sent one way) ID numbers must eventually repeat. This is acceptable as long as no two messages that are sent \"close together\" have the same id. Effectively, no two \"active\" messages in a single direction may have the same ID (what \"active\" means can vary, but in practice by the time 60,000 messages have been sent, old messages can be assumed inactive). When sending messages from PC to control board, it is required to restrict message ID between 0 and 59999 (inclusive). This is due to how the simulator is implemented. IDs 60000-65535 are reserved for simulator use. Note that IDs generated by the control board will not be restricted to this range. Second, each message has a CRC appended* to it. This is a 16-bit CRC using the CCITT-FALSE algorithm. It is appended* to the message big endian. The CRC is calculated on the concatenation of the message id bytes and the raw (unescaped) payload bytes. Just like the payload data, when prepending or appending message id or crc, it is necessary to escape bytes that are equal to control bytes (start, end, escape). * Note that append and prepend still mean contained between control (start and end) bytes. This results in message construction looking like the following (the \"payload\" is the raw message being sent).","title":"Communication Protocol"},{"location":"user_guide/comm_protocol/#communication-protocol","text":"Communication with the control board relies on sending messages between the control board and PC. This section focuses on how messages are sent, not what messages are sent.","title":"Communication Protocol"},{"location":"user_guide/comm_protocol/#hardware-communication-layer","text":"Messages are sent to the control board over the MCU's builtin USB port. The control board acts as a USB ACM CDC device. In practice, this means that it shows up as a serial (UART) port on the computer it is connected to. However, baud rate settings are irrelevant (and changing baud rates has no effect). As such, messages are sent to / received from the control board using \"UART\" with an undefined baud rate*. It is still necessary to set a baud rate when opening a UART port (as that information is provided to the device on the other side), but the rate is unused. Additionally, it is expected to operate in 8N1 data mode (8 data bits, no parity, 1 stop bit). A stream of data is sent to the control board over this port. This data is interpreted as described below. * NOTE: The baud rate 1200 is an exception. Openening then closing the port at this baud rate is used to trigger the control board to reboot to its bootloader. Do not use 1200 baud!","title":"Hardware Communication Layer"},{"location":"user_guide/comm_protocol/#message-format-and-construction","text":"The messages sent to / received from the control board have a specific format. Each message transfers a raw set of bytes (unsigned byte array). This set of bytes is the \"payload data\" of the message. The \"payload data\" is the data that is actually being send via the message. Messages are limited to a maximum payload size of 96 bytes. To be able to identify what data is part of a single message, it is necessary to add some additional information around the payload. The control board uses a special byte to indicate the start of a message ( START_BYTE ) and another one to identify the end of a message ( END_BYTE ). Since the payload could itself contain a start or end byte, there is also an escape byte ( ESCAPE_BYTE ) used to escape a START_BYTE , END_BYTE , or an ESCAPE_BYTE in the payload. START_BYTE becomes ESCAPE_BYTE , START_BYTE END_BYTE becomes ESCAPE_BYTE , END_BYTE ESCAPE_BYTE becomes ESCAPE_BYTE , ESCAPE_BYTE This is similar to escaping a quote in a string using a backslash. For the control board: START_BYTE = 253 (unsigned 8-bit) = -3 (signed 8-bit) END_BYTE = 254 (unsigned 8-bit) = -2 (signed 8-bit) ESCAPE_BYTE = 255 (unsigned 8-bit) = -1 (signed 8-bit) In addition to the control bytes and the payload, each message contains two other pieces of information: First, each message includes a prepended* ID number (16-bit unsigned big-endian integer). These ID numbers are required to be unique only in one direction . This means that two messages sent from the PC to the control board cannot have the same id. Likewise, two messages sent from the control board to the PC cannot have the same id. However, a message sent from PC to control board can have the same id as another message sent from control board to PC. Note that in practice, eventually (after 65535 messages sent one way) ID numbers must eventually repeat. This is acceptable as long as no two messages that are sent \"close together\" have the same id. Effectively, no two \"active\" messages in a single direction may have the same ID (what \"active\" means can vary, but in practice by the time 60,000 messages have been sent, old messages can be assumed inactive). When sending messages from PC to control board, it is required to restrict message ID between 0 and 59999 (inclusive). This is due to how the simulator is implemented. IDs 60000-65535 are reserved for simulator use. Note that IDs generated by the control board will not be restricted to this range. Second, each message has a CRC appended* to it. This is a 16-bit CRC using the CCITT-FALSE algorithm. It is appended* to the message big endian. The CRC is calculated on the concatenation of the message id bytes and the raw (unescaped) payload bytes. Just like the payload data, when prepending or appending message id or crc, it is necessary to escape bytes that are equal to control bytes (start, end, escape). * Note that append and prepend still mean contained between control (start and end) bytes. This results in message construction looking like the following (the \"payload\" is the raw message being sent).","title":"Message Format and Construction"},{"location":"user_guide/general_use/","text":"Using Control Board TODO: This page is an outline. Update with details / actual contents. General Procedure Connecting to Control Board System configuration (motor matrix, thruster inversions, thruster PWM, reldof parameters) Sensor configuration Validating sensor connectivity Modes of operation & setting speed Motor Watchdog Reading Sensor data Example program using python interface script LED Indicator Info Calibrate Sensors Custom interface code instead of python iface Many users will want to build their own interface. Python iface is a reference / an easy place to start, but not required You just need to implement communication with the control board as described in comm protocol and messages pages Note that users doing so may wish to read the section on SimCB and simulator too as supporting both TCP and UART in implementations can be useful.","title":"Using Control Board"},{"location":"user_guide/general_use/#using-control-board","text":"TODO: This page is an outline. Update with details / actual contents. General Procedure Connecting to Control Board System configuration (motor matrix, thruster inversions, thruster PWM, reldof parameters) Sensor configuration Validating sensor connectivity Modes of operation & setting speed Motor Watchdog Reading Sensor data Example program using python interface script LED Indicator Info Calibrate Sensors Custom interface code instead of python iface Many users will want to build their own interface. Python iface is a reference / an easy place to start, but not required You just need to implement communication with the control board as described in comm protocol and messages pages Note that users doing so may wish to read the section on SimCB and simulator too as supporting both TCP and UART in implementations can be useful.","title":"Using Control Board"},{"location":"user_guide/messages/","text":"Messages This section describes what specific messages are sent to / received from the control board and what they do / mean. This does not address how messages are constructed or sent. For such information, see Communication Protocol . Note: The Communication Protocol page uses the term \"payload\" for the data being transferred and \"message\" for the formatted / fully constructed set of data. Here, what we refer to as \"messages\" are actually the \"payload\" data, not the constructed data. Message Definition Conventions In the following sections, the following standard is used to describe message contents: Each message's contents are shown as a comma separated list of bytes. Each comma separated item is a single byte, with one exception for parameters (as described below) Parameters are shown inside square brackets. Parameters represent a value that will be described in more detail below the message structure information. Parameters can be multiple bytes (even though they take only one entry between commas). ASCII characters are shown in single quotes. These are single byte unsigned ASCII characters. Numbers are not contained within any symbols. Numbers may be in hex (prefix 0x), binary (prefix 0b), or decimal (no prefix). Types of Messages Commands : Messages instructing an action be taken. These messages must be acknowledged upon receipt. The acknowledgement typically contains no data. Sent from PC to control board. Queries : Messages requesting information. These messages must be acknowledged upon receipt. The acknowledgement will contain the requested information. Sent from PC to control board. Acknowledgements : A very specific type of message acknowledging receipt of another message (with an error code and optional data). Sent from control board to PC. Status Messages : Unprompted messages containing information about state / data changes. Sent from control board to PC. Commands and Queries Motor motion commands Raw Speed Set Used to set motor speeds in RAW mode. This command has the following format. 'R', 'A', 'W', [speed_1], [speed_2], [speed_3], [speed_4], [speed_5], [speed_6], [speed_7], [speed_8] [speed_n] : The speed of thruster n from -1.0 to 1.0. A 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Local Speed Set Used to set motor speeds in LOCAL mode. This command has the following format 'L', 'O', 'C', 'A', 'L', [x], [y], [z], [xrot], [yrot], [zrot] [x] , [y] , [z] , [xrot] , [yrot] , [zrot] : Speed for each DoF relative to the robot -1.0 to 1.0. A 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Global Speed Set Used to set motor speeds in GLOBAL mode. This command has the following format 'G', 'L', 'O', 'B', 'A', 'L', [x], [y], [z], [pitch_spd], [roll_spd], [yaw_spd] [x] , [y] , [z] : Speed for each \"world-relative\" (pitch and roll compensated) DoF -1.0 to 1.0. 32-bit float (little endian). [pitch_spd] , [roll_spd] , [yaw_spd] : Rate of change of vehicle pitch, roll, and yaw -1.0 to 1.0. This message will be acknowledged. The acknowledge message will contain no result data. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Orientation Hold Speed Set (Variant 1) Used to set motor speeds in ORIENTATION_HOLD mode using a speed for yaw. This command has the following format 'O', 'H', 'O', 'L', 'D', '1', [x], [y], [z], [yaw_spd], [target_pitch], [target_roll] Each value is a 32-bit float little endian. Pitch and roll are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). x, y, z, and yaw_spd are x, y, z, and yaw_spd just as in global mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Orientation Hold Speed Set (Variant 2) Used to set motor speeds in ORIENTATION_HOLD mode using a PID to maintain a target yaw. This command has the following format 'O', 'H', 'O', 'L', 'D', '2', [x], [y], [z], [target_pitch], [target_roll], [target_yaw] Each value is a 32-bit float little endian. Target Pitch, roll, and yaw are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). x, y, and z are speeds in the x, y, and z DoFs the same as in GLOBAL mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Stability Assist Speed Set (Variant 1) Used to set motor speeds in STABILITY_ASSIST mode using a speed for yaw. This command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', '1', [x], [y], [yaw_spd], [target_pitch], [target_roll], [target_depth] Each value is a 32-bit float little endian. Pitch and roll are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). Depth is in meters where negative numbers are below the surface. x, y, and yaw_spd are x, y, and yaw_spd just as in global mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Stability Assist Speed Set (Variant 2) Used to set motor speeds in STABILITY_ASSIST mode using a PID to maintain a target yaw. This command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', '2', [x], [y], [target_pitch], [target_roll], [target_yaw], [target_depth] Each value is a 32-bit float little endian. Target Pitch, roll, and yaw are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). Depth is in meters where negative numbers are below the surface. x and y are speeds in the x and y DoFs the same as in GLOBAL mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Feed Motor Watchdog Used to feed the motor watchdog so it does not kill the motors. This command has the following format 'W', 'D', 'G', 'F' This message will be acknowledged. The acknowledge message will contain no result data. Vehicle Configuration Commands Motor Matrix Set Motor matrix set command is used to set a single row of the motor matrix. It has the following format 'M', 'M', 'A', 'T', 'S', [thruster_num], [x], [y], [z], [pitch], [roll], [yaw] [thruster_num] : A single byte who'se unsigned value is the thruster number the row data should be set for (1-8). [x] , [y] , [z] , [pitch] , [roll] , [yaw] : Columns of the motor matrix row being set. Each is a 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Motor Matrix Update Motor matrix update command is used to inform the control board the motor matrix has changed. Causes the control board to perform some calculations with the new motor matrix. This should be sent after writing all rows of the motor matrix that should change using the motor matrix set command. The motor matrix update command has the following format 'M', 'M', 'A', 'T', 'U' This message will be acknowledged. The acknowledge message will contain no result data. Thruster PWM Parameter Config Configure PWM settings for thrusters. 'T', 'P', 'W', 'M', [pwm_period], [pwm_zero], [pwm_range] Each value is an unsigned 16-bit integer (little endian). pwm_period is the PWM signal period in microseconds (determines PWM frequency / update rate for ESCs). pwm_zero is the pulse width for zero speed in microseconds (typically 1500). pwm_range is the deviation from zero to achieve max speed (when added) or min speed (when subtracted) such that the pulse width pwm_zero + pwm_range microseconds is full forward speed and the pulse width pwm_zero - pwm_range microseconds is full reverse speed. This message will be acknowledged. The acknowledge message will contain no result data. Thruster Inversion Set Thruster inversion set command is used to invert the positive and negative direction of thrusters. It has the following format 'T', 'I', 'N', 'V', [inv] [inv] : A single byte where each bit represents the inversion status of a thruster. The MSB (bit 7) corresponds to thruster 8 and the LSB corresponds to thruster 1 (bit + 1 = thruster). A bit value of 1 means the thruster is inverted. A bit value of 0 means the thruster is not inverted. This message will be acknowledged. The acknowledge message will contain no result data. Relative DoF Speed Set Used to set relative speeds of motion in each DoF. There are two groups: linear (x, y, z) and angular (xrot, yrot, zrot). Within each group, use 1.0 for the fastest DoF. Other DoFs in the group are percentages of the fastest speed (from 0.0 to 1.0). This message has the following format 'R', 'E', 'L', 'D', 'O', 'F', [x], [y], [z], [xrot], [yrot], [zrot] [x] , [y] , [z] , [xrot] , [yrot] , [zrot] : 32-bit little endian floats. This message will be acknowledged. The acknowledge message will contain no result data. PID Tune Command Used to tune PID controllers. The command has the following format 'P', 'I', 'D', 'T', 'N', [which], [kp], [ki], [kd], [limit], [invert] [which] indicates which PID to tune ('X' = xrot, 'Y' = yrot, 'Z' = zrot, 'D' = depth hold). [kp] , [ki] , [kd] are proportional, integral, derivative, and feed-forward gains (32-bit float little endian). [limit] Is the PID controller's max output (limits max speed in the controlled DoF). Must be between 0.0 and 1.0. 32-bit float little endian. [invert] Set to one to invert PID output. Zero otherwise. Sensor Commands and Queries Sensor Status Query Check which IMU and depth sensor is currently in use 'S', 'S', 'T', 'A', 'T' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format [which_imu], [which_depth] [which_imu] : Single byte indicating which IMU is in use - No IMU: 0 - Simulated IMU (under simulator hijack): 1 - BNO055 IMU: 2 [which_depth] : Single byte indicating which depth sensor is in use - No Depth sensor: 0 - Simulated depth sensor (under simulator hijack): 1 - MS5837 Depth sensor: 2 IMU Read Reads IMU data once. 'I', 'M', 'U', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the BNO055 data status message. [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw] Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles. IMU Raw Read Reads raw IMU data once. 'I', 'M', 'U', 'W' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the BNO055 data status message. [accel_x], [accel_y], [accel_z], [gyro_x], [gyro_y], [gyro_z] Each value is a 32-bit float, little endian. IMU Periodic Read Used to enable / disable periodic reading of IMU data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use IMU data. 'I', 'M', 'U', 'P', [enable] [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled. This message will be acknowledged. The acknowledge message will contain no result data. Depth Read Reads depth sensor data once. 'D', 'E', 'P', 'T', 'H', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the MS5837 data status message. [depth_m], [pressure_pa], [temp_c] depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water). Depth Periodic Read Used to enable / disable periodic reading of MS5837 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use depth sensor data. 'D', 'E', 'P', 'T', 'G', 'P', [enable] [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled. This message will be acknowledged. The acknowledge message will contain no result data. BNO055 IMU Configuration BNO055 IMU Axis Configure Command Used to configure the BNO055 IMU's axis orientation. Note: This will also reset the accumulated euler angles to zero . 'B', 'N', 'O', '0', '5', '5', 'A', [config] [config] : A single byte. The value of this byte is between 0 and 7 (inclusive) representing on of the BNO055 axis configs (P0 to P7) as described in the BNO055 datasheet. Note: Changing the axis config changes IMU mode. Thus, there will be a brief time afterwards where the IMU may report zeros for all data. This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Save BNO055 Stored Calibration Command This command is used to store a set of calibration constants for the BNO055 to the control board. This will write the \"stored calibration constants\". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'S', [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] Each value is a signed 16-bit integer. The meaning of each value is described in the BNO055 datasheet. This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Erase BNO055 Stored Calibration Command This command is used to erase calibration constants for the BNO055 from the control board. This will erase the \"stored calibration constants\". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'E' This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Reset BNO055 Command This command is used to reset / reconfigure the BNO055. This is typically used to clear any auto generated calibration constants. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'R', 'S', 'T' This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Read BNO055 Stored Calibration Query This command is used to read a set of calibration constants for the BNO055 from the control board. This will read the \"stored calibration constants\". The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [valid], [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] valid is an 8-bit integer. A value of 0 indicates that no calibration is stored on the control board (other values have no meaning). A value of 1 indicates that a calibration is stored (other values are that calibration). All other values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Read BNO055 Live Calibration Status Query This command is used to read the status of the BNO055's calibration routine. Note that this reads the value directly from the BNO055. This value is meaningless if a calibration was manually applied to the sensor. Thus, this is only useful if any \"stored calibration constants\" are first erased. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'C', 'S' This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. If acknowledged with no error, the response will contain data in the following format. [status] status is an 8-bit integer. The value of status is the value of the BNO055's CALIB_STAT register. The meaning of this number is described in the BNO055 datasheet. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Read BNO055 Live Calibration Values Query This command is used to read a set of calibration constants from the BNO055. This will read the \"live calibration constants\" directly from the BNO055. Note that the calibration constants are only valid if the calibration status from the BNO055 is 3 for the accelerometer and gyroscope. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'C', 'V' This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. If acknowledged with no error, the response will contain data in the following format. [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] All values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. MS5837 Depth Sensor Configuration Read MS5837 Calibration Query This command is used to read the values of the MS5837 calibration constants. The command has the following format 'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'G' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [atm_pressure], [fluid_density] Both values are 32-bit little endian floats. Write MS5837 Calibration Command This command is used to write the values of the MS5837 calibration constants. The command has the following format 'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'S', [atm_pressure], [fluid_density] Both values are 32-bit little endian floats. This message will be acknowledged. The acknowledge message will contain no result data. Misc Commands and Queries Reset Command This command is used to rest the control board itself. This will reset the microcontroller on the control board, thus the USB device will disconnect and reconnect (note that if your program still holds the port when this happens, the USB device will likely be assigned a different port number). 'R', 'E', 'S', 'E', 'T', 0x0D, 0x1E This message is not acknowledged. Last Reset Cause Query Get error code for last system reset cause of the control board. Generally not useful for end users, except for reporting errors. Mainly a debug / development tool. See error codes in firmware source debug.h . 'R', 'S', 'T', 'W', 'H', 'Y' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [error_code] error_code is a 32-bit integer (signed), little endian. Simulator Hijack Command This command is used by the simulator to hijack a real control board. This allows the simulator to pass certain information to and receive certain information from the control board. This enables testing of the actual firmware and reproducing bugs under simulation. The command has the following format. 'S', 'I', 'M', 'H', 'I', 'J', 'A', 'C', 'K', [hijack] [hijack] is an 8-bit integer (unsigned) with a value of 1 or 0. If 1, the control board is put into simulator hijack mode. If 0, it is removed from simulator hijack mode. This message will be acknowledged. The acknowledge message will contain no result data. Simulator Data Command This command is used by the simulator to send simulated sensor data to a hijacked control board. 'S', 'I', 'M', 'D', 'A', 'T', [w], [x], [y], [z], [depth] All values are little endian floats (32-bit). x , y , z , w are current quaternion (IMU data) depth is current depth (depth sensor data). Version Info Query Get the version info from the control board. 'C', 'B', 'V', 'E', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format [cb_ver],[fw_ver_major],[fw_ver_minor],[fw_ver_revision],[fw_ver_type],[fw_ver_build] Each value is an unsigned 8-bit integer. All are simply interpreted as numbers, except for fw_ver_type , which is an ASCII character. cb_ver : Version of the control board hardware (CBv1 or CBv2 = 1 or 2; 0 = SimCB in simulator) fw_ver_major : Major version of firmware running on the control board fw_ver_minor : Minor version of firmware running on the control board fw_ver_revision : Revision version of firmware running on the control board fw_ver_type : Type of firmware release. 'a' = alpha, 'b' = beta, 'c' = release candidate (rc), ' ' (space) = full release fw_ver_build : Build number for pre-release firmware. Should be ignored for fw_ver_type release (' ') Acknowledgements An acknowledgement message has the following format 'A', 'C', 'K', [ack_id], [error_code], [result] [ack_id] : The ID of the message being acknowledged. Unsigned 16-bit integer (big endian). [error_code] : A single byte error code. 0 = None: No error. 1 = Unknown Message: Control board does not recognize the message. 2 = Invalid arguments: Message is recognized, but arguments are invalid 3 = Invalid Command: Command is known, but is not valid at this time. 255 = Reserved: Control board will not use this code. Typically used as timeout. [result] : Optional data of variable size attached to the acknowledge message. Its size, format, and meaning depends on the message being acknowledged. Status Messages Motor Watchdog Status Motor watch status message is used by the control board to notify the PC about changes to motor (watchdog) state. It has the following format 'W', 'D', 'G', 'S', [status] [status] is a single byte. A value of 1 indicates the motors are enabled. A value of zero indicates the motors are currently killed by the watchdog. IMU Data Status Used by the control board to periodically send IMU data to the PC. Only sent when IMU periodic reads are enabled via the IMU periodic read command. The message has the following format 'I', 'M', 'U', 'D', [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw] Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles. Depth Data Status Used by the control board to periodically send IMU data to the PC. Only sent when BNO055 periodic reads are enabled via the BNO055 periodic read command. The message has the following format 'D', 'E', 'P', 'T', 'H', 'D', [depth_m], [pressure_pa], [temp_c] depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water). Debug Status Messages Used only during development. These will not occur on release builds of the firmware. These are arbitrary messages sent by the control board to the PC for the firmware developer's use during development. They have the following format 'D', 'E', 'B', 'U', 'G', [msg] msg is an ascii string. Debug Data Status Messages Used only during development. These will not occur on release builds of the firmware. These are arbitrary messages sent by the control board to the PC for the firmware developer's use during development. They have the following format 'D', 'B', 'G', 'D', 'A', 'T', [msg] msg is arbitrary data. Heartbeat Status Messages Sent from control board periodically to indicate that it still exists and is operating as expected. This is generally ignored by end users. It is mostly intended to ensure communication occurs periodically in SimCB so connection drops are detectable. 'H', 'E', 'A', 'R', 'T', 'B', 'E', 'A', 'T' Simulator Status Message Sent from a simulator hijacked control board periodically to provide simulator with state and motor speed information. 'S', 'I', 'M', 'S', 'T', 'A', 'T', [t1], [t2], [t3], [t4], [t5], [t6], [t7], [t8], [mode], [wdog_killed] Each value t1 to t8 is a 32-bit little endian float representing thruster speeds 1 - 8 respectively. mode is an unsigned 8-bit integer indicating the control board's current operating mode from one of the following Raw = 0 Local = 1 Global = 2 Sassist = 3 Ohold = 5 wdog_killed is an unsigned 8-bit integer indicating if the control board's motors are killed due to motor watchdog timeout. 1 indicates that motors are killed. 0 indicates not killed.","title":"Messages"},{"location":"user_guide/messages/#messages","text":"This section describes what specific messages are sent to / received from the control board and what they do / mean. This does not address how messages are constructed or sent. For such information, see Communication Protocol . Note: The Communication Protocol page uses the term \"payload\" for the data being transferred and \"message\" for the formatted / fully constructed set of data. Here, what we refer to as \"messages\" are actually the \"payload\" data, not the constructed data.","title":"Messages"},{"location":"user_guide/messages/#message-definition-conventions","text":"In the following sections, the following standard is used to describe message contents: Each message's contents are shown as a comma separated list of bytes. Each comma separated item is a single byte, with one exception for parameters (as described below) Parameters are shown inside square brackets. Parameters represent a value that will be described in more detail below the message structure information. Parameters can be multiple bytes (even though they take only one entry between commas). ASCII characters are shown in single quotes. These are single byte unsigned ASCII characters. Numbers are not contained within any symbols. Numbers may be in hex (prefix 0x), binary (prefix 0b), or decimal (no prefix).","title":"Message Definition Conventions"},{"location":"user_guide/messages/#types-of-messages","text":"Commands : Messages instructing an action be taken. These messages must be acknowledged upon receipt. The acknowledgement typically contains no data. Sent from PC to control board. Queries : Messages requesting information. These messages must be acknowledged upon receipt. The acknowledgement will contain the requested information. Sent from PC to control board. Acknowledgements : A very specific type of message acknowledging receipt of another message (with an error code and optional data). Sent from control board to PC. Status Messages : Unprompted messages containing information about state / data changes. Sent from control board to PC.","title":"Types of Messages"},{"location":"user_guide/messages/#commands-and-queries","text":"","title":"Commands and Queries"},{"location":"user_guide/messages/#motor-motion-commands","text":"Raw Speed Set Used to set motor speeds in RAW mode. This command has the following format. 'R', 'A', 'W', [speed_1], [speed_2], [speed_3], [speed_4], [speed_5], [speed_6], [speed_7], [speed_8] [speed_n] : The speed of thruster n from -1.0 to 1.0. A 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Local Speed Set Used to set motor speeds in LOCAL mode. This command has the following format 'L', 'O', 'C', 'A', 'L', [x], [y], [z], [xrot], [yrot], [zrot] [x] , [y] , [z] , [xrot] , [yrot] , [zrot] : Speed for each DoF relative to the robot -1.0 to 1.0. A 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Global Speed Set Used to set motor speeds in GLOBAL mode. This command has the following format 'G', 'L', 'O', 'B', 'A', 'L', [x], [y], [z], [pitch_spd], [roll_spd], [yaw_spd] [x] , [y] , [z] : Speed for each \"world-relative\" (pitch and roll compensated) DoF -1.0 to 1.0. 32-bit float (little endian). [pitch_spd] , [roll_spd] , [yaw_spd] : Rate of change of vehicle pitch, roll, and yaw -1.0 to 1.0. This message will be acknowledged. The acknowledge message will contain no result data. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Orientation Hold Speed Set (Variant 1) Used to set motor speeds in ORIENTATION_HOLD mode using a speed for yaw. This command has the following format 'O', 'H', 'O', 'L', 'D', '1', [x], [y], [z], [yaw_spd], [target_pitch], [target_roll] Each value is a 32-bit float little endian. Pitch and roll are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). x, y, z, and yaw_spd are x, y, z, and yaw_spd just as in global mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Orientation Hold Speed Set (Variant 2) Used to set motor speeds in ORIENTATION_HOLD mode using a PID to maintain a target yaw. This command has the following format 'O', 'H', 'O', 'L', 'D', '2', [x], [y], [z], [target_pitch], [target_roll], [target_yaw] Each value is a 32-bit float little endian. Target Pitch, roll, and yaw are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). x, y, and z are speeds in the x, y, and z DoFs the same as in GLOBAL mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Stability Assist Speed Set (Variant 1) Used to set motor speeds in STABILITY_ASSIST mode using a speed for yaw. This command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', '1', [x], [y], [yaw_spd], [target_pitch], [target_roll], [target_depth] Each value is a 32-bit float little endian. Pitch and roll are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). Depth is in meters where negative numbers are below the surface. x, y, and yaw_spd are x, y, and yaw_spd just as in global mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Stability Assist Speed Set (Variant 2) Used to set motor speeds in STABILITY_ASSIST mode using a PID to maintain a target yaw. This command has the following format 'S', 'A', 'S', 'S', 'I', 'S', 'T', '2', [x], [y], [target_pitch], [target_roll], [target_yaw], [target_depth] Each value is a 32-bit float little endian. Target Pitch, roll, and yaw are euler angles (in degrees). These are intrinsic euler angles (z-x'-y'' convention per the control board's coordinate system). Depth is in meters where negative numbers are below the surface. x and y are speeds in the x and y DoFs the same as in GLOBAL mode. This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command. Feed Motor Watchdog Used to feed the motor watchdog so it does not kill the motors. This command has the following format 'W', 'D', 'G', 'F' This message will be acknowledged. The acknowledge message will contain no result data.","title":"Motor motion commands"},{"location":"user_guide/messages/#vehicle-configuration-commands","text":"Motor Matrix Set Motor matrix set command is used to set a single row of the motor matrix. It has the following format 'M', 'M', 'A', 'T', 'S', [thruster_num], [x], [y], [z], [pitch], [roll], [yaw] [thruster_num] : A single byte who'se unsigned value is the thruster number the row data should be set for (1-8). [x] , [y] , [z] , [pitch] , [roll] , [yaw] : Columns of the motor matrix row being set. Each is a 32-bit float (little endian). This message will be acknowledged. The acknowledge message will contain no result data. Motor Matrix Update Motor matrix update command is used to inform the control board the motor matrix has changed. Causes the control board to perform some calculations with the new motor matrix. This should be sent after writing all rows of the motor matrix that should change using the motor matrix set command. The motor matrix update command has the following format 'M', 'M', 'A', 'T', 'U' This message will be acknowledged. The acknowledge message will contain no result data. Thruster PWM Parameter Config Configure PWM settings for thrusters. 'T', 'P', 'W', 'M', [pwm_period], [pwm_zero], [pwm_range] Each value is an unsigned 16-bit integer (little endian). pwm_period is the PWM signal period in microseconds (determines PWM frequency / update rate for ESCs). pwm_zero is the pulse width for zero speed in microseconds (typically 1500). pwm_range is the deviation from zero to achieve max speed (when added) or min speed (when subtracted) such that the pulse width pwm_zero + pwm_range microseconds is full forward speed and the pulse width pwm_zero - pwm_range microseconds is full reverse speed. This message will be acknowledged. The acknowledge message will contain no result data. Thruster Inversion Set Thruster inversion set command is used to invert the positive and negative direction of thrusters. It has the following format 'T', 'I', 'N', 'V', [inv] [inv] : A single byte where each bit represents the inversion status of a thruster. The MSB (bit 7) corresponds to thruster 8 and the LSB corresponds to thruster 1 (bit + 1 = thruster). A bit value of 1 means the thruster is inverted. A bit value of 0 means the thruster is not inverted. This message will be acknowledged. The acknowledge message will contain no result data. Relative DoF Speed Set Used to set relative speeds of motion in each DoF. There are two groups: linear (x, y, z) and angular (xrot, yrot, zrot). Within each group, use 1.0 for the fastest DoF. Other DoFs in the group are percentages of the fastest speed (from 0.0 to 1.0). This message has the following format 'R', 'E', 'L', 'D', 'O', 'F', [x], [y], [z], [xrot], [yrot], [zrot] [x] , [y] , [z] , [xrot] , [yrot] , [zrot] : 32-bit little endian floats. This message will be acknowledged. The acknowledge message will contain no result data. PID Tune Command Used to tune PID controllers. The command has the following format 'P', 'I', 'D', 'T', 'N', [which], [kp], [ki], [kd], [limit], [invert] [which] indicates which PID to tune ('X' = xrot, 'Y' = yrot, 'Z' = zrot, 'D' = depth hold). [kp] , [ki] , [kd] are proportional, integral, derivative, and feed-forward gains (32-bit float little endian). [limit] Is the PID controller's max output (limits max speed in the controlled DoF). Must be between 0.0 and 1.0. 32-bit float little endian. [invert] Set to one to invert PID output. Zero otherwise.","title":"Vehicle Configuration Commands"},{"location":"user_guide/messages/#sensor-commands-and-queries","text":"Sensor Status Query Check which IMU and depth sensor is currently in use 'S', 'S', 'T', 'A', 'T' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format [which_imu], [which_depth] [which_imu] : Single byte indicating which IMU is in use - No IMU: 0 - Simulated IMU (under simulator hijack): 1 - BNO055 IMU: 2 [which_depth] : Single byte indicating which depth sensor is in use - No Depth sensor: 0 - Simulated depth sensor (under simulator hijack): 1 - MS5837 Depth sensor: 2 IMU Read Reads IMU data once. 'I', 'M', 'U', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the BNO055 data status message. [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw] Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles. IMU Raw Read Reads raw IMU data once. 'I', 'M', 'U', 'W' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the BNO055 data status message. [accel_x], [accel_y], [accel_z], [gyro_x], [gyro_y], [gyro_z] Each value is a 32-bit float, little endian. IMU Periodic Read Used to enable / disable periodic reading of IMU data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use IMU data. 'I', 'M', 'U', 'P', [enable] [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled. This message will be acknowledged. The acknowledge message will contain no result data. Depth Read Reads depth sensor data once. 'D', 'E', 'P', 'T', 'H', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the MS5837 data status message. [depth_m], [pressure_pa], [temp_c] depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water). Depth Periodic Read Used to enable / disable periodic reading of MS5837 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use depth sensor data. 'D', 'E', 'P', 'T', 'G', 'P', [enable] [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled. This message will be acknowledged. The acknowledge message will contain no result data.","title":"Sensor Commands and Queries"},{"location":"user_guide/messages/#bno055-imu-configuration","text":"BNO055 IMU Axis Configure Command Used to configure the BNO055 IMU's axis orientation. Note: This will also reset the accumulated euler angles to zero . 'B', 'N', 'O', '0', '5', '5', 'A', [config] [config] : A single byte. The value of this byte is between 0 and 7 (inclusive) representing on of the BNO055 axis configs (P0 to P7) as described in the BNO055 datasheet. Note: Changing the axis config changes IMU mode. Thus, there will be a brief time afterwards where the IMU may report zeros for all data. This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Save BNO055 Stored Calibration Command This command is used to store a set of calibration constants for the BNO055 to the control board. This will write the \"stored calibration constants\". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'S', [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] Each value is a signed 16-bit integer. The meaning of each value is described in the BNO055 datasheet. This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Erase BNO055 Stored Calibration Command This command is used to erase calibration constants for the BNO055 from the control board. This will erase the \"stored calibration constants\". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'E' This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Reset BNO055 Command This command is used to reset / reconfigure the BNO055. This is typically used to clear any auto generated calibration constants. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'R', 'S', 'T' This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Read BNO055 Stored Calibration Query This command is used to read a set of calibration constants for the BNO055 from the control board. This will read the \"stored calibration constants\". The command has the following format 'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [valid], [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] valid is an 8-bit integer. A value of 0 indicates that no calibration is stored on the control board (other values have no meaning). A value of 1 indicates that a calibration is stored (other values are that calibration). All other values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Read BNO055 Live Calibration Status Query This command is used to read the status of the BNO055's calibration routine. Note that this reads the value directly from the BNO055. This value is meaningless if a calibration was manually applied to the sensor. Thus, this is only useful if any \"stored calibration constants\" are first erased. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'C', 'S' This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. If acknowledged with no error, the response will contain data in the following format. [status] status is an 8-bit integer. The value of status is the value of the BNO055's CALIB_STAT register. The meaning of this number is described in the BNO055 datasheet. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code. Read BNO055 Live Calibration Values Query This command is used to read a set of calibration constants from the BNO055. This will read the \"live calibration constants\" directly from the BNO055. Note that the calibration constants are only valid if the calibration status from the BNO055 is 3 for the accelerometer and gyroscope. The command has the following format 'B', 'N', 'O', '0', '5', '5', 'C', 'V' This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the \"Invalid Command\" error code. If acknowledged with no error, the response will contain data in the following format. [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z] All values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code.","title":"BNO055 IMU Configuration"},{"location":"user_guide/messages/#ms5837-depth-sensor-configuration","text":"Read MS5837 Calibration Query This command is used to read the values of the MS5837 calibration constants. The command has the following format 'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'G' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [atm_pressure], [fluid_density] Both values are 32-bit little endian floats. Write MS5837 Calibration Command This command is used to write the values of the MS5837 calibration constants. The command has the following format 'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'S', [atm_pressure], [fluid_density] Both values are 32-bit little endian floats. This message will be acknowledged. The acknowledge message will contain no result data.","title":"MS5837 Depth Sensor Configuration"},{"location":"user_guide/messages/#misc-commands-and-queries","text":"Reset Command This command is used to rest the control board itself. This will reset the microcontroller on the control board, thus the USB device will disconnect and reconnect (note that if your program still holds the port when this happens, the USB device will likely be assigned a different port number). 'R', 'E', 'S', 'E', 'T', 0x0D, 0x1E This message is not acknowledged. Last Reset Cause Query Get error code for last system reset cause of the control board. Generally not useful for end users, except for reporting errors. Mainly a debug / development tool. See error codes in firmware source debug.h . 'R', 'S', 'T', 'W', 'H', 'Y' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. [error_code] error_code is a 32-bit integer (signed), little endian. Simulator Hijack Command This command is used by the simulator to hijack a real control board. This allows the simulator to pass certain information to and receive certain information from the control board. This enables testing of the actual firmware and reproducing bugs under simulation. The command has the following format. 'S', 'I', 'M', 'H', 'I', 'J', 'A', 'C', 'K', [hijack] [hijack] is an 8-bit integer (unsigned) with a value of 1 or 0. If 1, the control board is put into simulator hijack mode. If 0, it is removed from simulator hijack mode. This message will be acknowledged. The acknowledge message will contain no result data. Simulator Data Command This command is used by the simulator to send simulated sensor data to a hijacked control board. 'S', 'I', 'M', 'D', 'A', 'T', [w], [x], [y], [z], [depth] All values are little endian floats (32-bit). x , y , z , w are current quaternion (IMU data) depth is current depth (depth sensor data). Version Info Query Get the version info from the control board. 'C', 'B', 'V', 'E', 'R' This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format [cb_ver],[fw_ver_major],[fw_ver_minor],[fw_ver_revision],[fw_ver_type],[fw_ver_build] Each value is an unsigned 8-bit integer. All are simply interpreted as numbers, except for fw_ver_type , which is an ASCII character. cb_ver : Version of the control board hardware (CBv1 or CBv2 = 1 or 2; 0 = SimCB in simulator) fw_ver_major : Major version of firmware running on the control board fw_ver_minor : Minor version of firmware running on the control board fw_ver_revision : Revision version of firmware running on the control board fw_ver_type : Type of firmware release. 'a' = alpha, 'b' = beta, 'c' = release candidate (rc), ' ' (space) = full release fw_ver_build : Build number for pre-release firmware. Should be ignored for fw_ver_type release (' ')","title":"Misc Commands and Queries"},{"location":"user_guide/messages/#acknowledgements","text":"An acknowledgement message has the following format 'A', 'C', 'K', [ack_id], [error_code], [result] [ack_id] : The ID of the message being acknowledged. Unsigned 16-bit integer (big endian). [error_code] : A single byte error code. 0 = None: No error. 1 = Unknown Message: Control board does not recognize the message. 2 = Invalid arguments: Message is recognized, but arguments are invalid 3 = Invalid Command: Command is known, but is not valid at this time. 255 = Reserved: Control board will not use this code. Typically used as timeout. [result] : Optional data of variable size attached to the acknowledge message. Its size, format, and meaning depends on the message being acknowledged.","title":"Acknowledgements"},{"location":"user_guide/messages/#status-messages","text":"Motor Watchdog Status Motor watch status message is used by the control board to notify the PC about changes to motor (watchdog) state. It has the following format 'W', 'D', 'G', 'S', [status] [status] is a single byte. A value of 1 indicates the motors are enabled. A value of zero indicates the motors are currently killed by the watchdog. IMU Data Status Used by the control board to periodically send IMU data to the PC. Only sent when IMU periodic reads are enabled via the IMU periodic read command. The message has the following format 'I', 'M', 'U', 'D', [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw] Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles. Depth Data Status Used by the control board to periodically send IMU data to the PC. Only sent when BNO055 periodic reads are enabled via the BNO055 periodic read command. The message has the following format 'D', 'E', 'P', 'T', 'H', 'D', [depth_m], [pressure_pa], [temp_c] depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water). Debug Status Messages Used only during development. These will not occur on release builds of the firmware. These are arbitrary messages sent by the control board to the PC for the firmware developer's use during development. They have the following format 'D', 'E', 'B', 'U', 'G', [msg] msg is an ascii string. Debug Data Status Messages Used only during development. These will not occur on release builds of the firmware. These are arbitrary messages sent by the control board to the PC for the firmware developer's use during development. They have the following format 'D', 'B', 'G', 'D', 'A', 'T', [msg] msg is arbitrary data. Heartbeat Status Messages Sent from control board periodically to indicate that it still exists and is operating as expected. This is generally ignored by end users. It is mostly intended to ensure communication occurs periodically in SimCB so connection drops are detectable. 'H', 'E', 'A', 'R', 'T', 'B', 'E', 'A', 'T' Simulator Status Message Sent from a simulator hijacked control board periodically to provide simulator with state and motor speed information. 'S', 'I', 'M', 'S', 'T', 'A', 'T', [t1], [t2], [t3], [t4], [t5], [t6], [t7], [t8], [mode], [wdog_killed] Each value t1 to t8 is a 32-bit little endian float representing thruster speeds 1 - 8 respectively. mode is an unsigned 8-bit integer indicating the control board's current operating mode from one of the following Raw = 0 Local = 1 Global = 2 Sassist = 3 Ohold = 5 wdog_killed is an unsigned 8-bit integer indicating if the control board's motors are killed due to motor watchdog timeout. 1 indicates that motors are killed. 0 indicates not killed.","title":"Status Messages"},{"location":"user_guide/preparing_board/","text":"Preparing a Control Board TODO: Improve this with step by step instructions and screenshots Assemble a control board (see v1 hardware or v2 hardware ) Flash the firmware See Building & Flashing for details Install bossa cli or dfu-util and make sure the binaries (bossac or dfu-util) are in your path Install python Download and extract release package from GitHub Put the board into bootloader mode v1: Double press reset button quickly v2: Hold boot button, press and release reset, release boot button In firmware folder from release package run ./flash.py [v1/v2] Release This will run the flash tool Connect external sensors","title":"Preparing a Control Board"},{"location":"user_guide/preparing_board/#preparing-a-control-board","text":"TODO: Improve this with step by step instructions and screenshots Assemble a control board (see v1 hardware or v2 hardware ) Flash the firmware See Building & Flashing for details Install bossa cli or dfu-util and make sure the binaries (bossac or dfu-util) are in your path Install python Download and extract release package from GitHub Put the board into bootloader mode v1: Double press reset button quickly v2: Hold boot button, press and release reset, release boot button In firmware folder from release package run ./flash.py [v1/v2] Release This will run the flash tool Connect external sensors","title":"Preparing a Control Board"},{"location":"user_guide/pythoniface/","text":"Using the Python Interface TODO: About iface (describe the different files) TODO: Launch script and what it does. Include port selection, but NOT use of SimCB, and NOT use of simulator. TODO: Vehicle configs and what they do TODO: Example scripts descriptions TODO: Guide on adding new vehicle configs TODO: Guide on adding custom scripts","title":"Using the Python Interface"},{"location":"user_guide/pythoniface/#using-the-python-interface","text":"TODO: About iface (describe the different files) TODO: Launch script and what it does. Include port selection, but NOT use of SimCB, and NOT use of simulator. TODO: Vehicle configs and what they do TODO: Example scripts descriptions TODO: Guide on adding new vehicle configs TODO: Guide on adding custom scripts","title":"Using the Python Interface"},{"location":"user_guide/simcb_simulator/","text":"Using SimCB and Simulator TODO: Details and improvements in various sections. TODO: Add port number info. SimCB SimCB is a version of the control board firmware which is built as a binary that runs on Windows, macOS, or Linux. By running this binary, you can run the control board firmware on your computer without having a physical control board. SimCB only supports simulator hijack mode (meaning only the sim IMU and depth sensors will work and thruster speeds will be reported back over comms interface). Instead of communicating with a physical control board via UART, you communicate with SimCB via TCP (the exact same messages are sent, just treat what you send over UART and TCP as byte streams). SimCB is a TCP server so code connecting to SimCB must be a TCP client. TODO: How to use SimCB instead of real control board over uart (including instructions to run SimCB) Using SimCB allows testing various aspects of communication with the control board and system behavior without having a physical control board. Simulator The simulator is a program that runs on your computer and models a vehicle in 3D space. The simulator hijacks a control board and provides the control board \"fake\" sensor data from the simulated environment. The control board then provides the simulator motor speeds so the simulator can move the vehicle appropriately in the simulated environment. When in this mode, user code should communicate with the simulator over TCP instead of communicating with the control board directly. See the simulator's README for a description of how to communicate with the simulator. Note that the python iface scripts have builtin support for using the simulator. The provided simulator (linked above) is fairly simple, and is mostly intended for development testing of the control board firmware. However, it can be useful to end users as well to see how the vehicle is expected to behave. Note thought that the environment provided in this simulator is very limited. For more complex use cases, users may wish to fork and adapt the simulator to their vehicles / use cases. TODO: Instructions to run simulator and connect to real control board. Combining SimCB and Simulator By combining the simulator (providing a simulated vehicle and environment) with SimCB (providing a control board without hardware), it is possible to test vehicle motion without any hardware (no vehicle in water and no real control board). TODO: Instructions to run the simulator and connect to SimCB Ways to access Control Board There are four general ways to use the control board Directly using a real control board over UART (via USB) Directly using SimCB over TCP Using the simulator attached to a real control board (talking to the simulator over TCP) Using the simulator attached to SimCB (talking to the simulator over TCP) TODO: Discussion of these methods and when they are useful Advanced use Cases End users can implement functionality in their own simulators to \"hijack\" a control board for simulation (referred to as \"simhijack\") just as the provided simulator does. When a control board is simhijacked, it does not use real sensors or create PWM signals for real thrusters. Instead, it communicates with a simulator. The control board gives the simulator motor speeds and the simulator is expected to provide the control board with sensor data. The protocol for doing so is fairly simple (see messages documentation) allowing end users to incorporate a control board into their own vehicle simulations. End users may want to design unit tests that require communicating with a control board and validating motion. Unit test code can simhijack either a physical board or SimCB (probably more useful to use SimCB) and provide specific sensor inputs and commands and validate the motor outputs are as expected.","title":"Using SimCB and Simulator"},{"location":"user_guide/simcb_simulator/#using-simcb-and-simulator","text":"TODO: Details and improvements in various sections. TODO: Add port number info.","title":"Using SimCB and Simulator"},{"location":"user_guide/simcb_simulator/#simcb","text":"SimCB is a version of the control board firmware which is built as a binary that runs on Windows, macOS, or Linux. By running this binary, you can run the control board firmware on your computer without having a physical control board. SimCB only supports simulator hijack mode (meaning only the sim IMU and depth sensors will work and thruster speeds will be reported back over comms interface). Instead of communicating with a physical control board via UART, you communicate with SimCB via TCP (the exact same messages are sent, just treat what you send over UART and TCP as byte streams). SimCB is a TCP server so code connecting to SimCB must be a TCP client. TODO: How to use SimCB instead of real control board over uart (including instructions to run SimCB) Using SimCB allows testing various aspects of communication with the control board and system behavior without having a physical control board.","title":"SimCB"},{"location":"user_guide/simcb_simulator/#simulator","text":"The simulator is a program that runs on your computer and models a vehicle in 3D space. The simulator hijacks a control board and provides the control board \"fake\" sensor data from the simulated environment. The control board then provides the simulator motor speeds so the simulator can move the vehicle appropriately in the simulated environment. When in this mode, user code should communicate with the simulator over TCP instead of communicating with the control board directly. See the simulator's README for a description of how to communicate with the simulator. Note that the python iface scripts have builtin support for using the simulator. The provided simulator (linked above) is fairly simple, and is mostly intended for development testing of the control board firmware. However, it can be useful to end users as well to see how the vehicle is expected to behave. Note thought that the environment provided in this simulator is very limited. For more complex use cases, users may wish to fork and adapt the simulator to their vehicles / use cases. TODO: Instructions to run simulator and connect to real control board.","title":"Simulator"},{"location":"user_guide/simcb_simulator/#combining-simcb-and-simulator","text":"By combining the simulator (providing a simulated vehicle and environment) with SimCB (providing a control board without hardware), it is possible to test vehicle motion without any hardware (no vehicle in water and no real control board). TODO: Instructions to run the simulator and connect to SimCB","title":"Combining SimCB and Simulator"},{"location":"user_guide/simcb_simulator/#ways-to-access-control-board","text":"There are four general ways to use the control board Directly using a real control board over UART (via USB) Directly using SimCB over TCP Using the simulator attached to a real control board (talking to the simulator over TCP) Using the simulator attached to SimCB (talking to the simulator over TCP) TODO: Discussion of these methods and when they are useful","title":"Ways to access Control Board"},{"location":"user_guide/simcb_simulator/#advanced-use-cases","text":"End users can implement functionality in their own simulators to \"hijack\" a control board for simulation (referred to as \"simhijack\") just as the provided simulator does. When a control board is simhijacked, it does not use real sensors or create PWM signals for real thrusters. Instead, it communicates with a simulator. The control board gives the simulator motor speeds and the simulator is expected to provide the control board with sensor data. The protocol for doing so is fairly simple (see messages documentation) allowing end users to incorporate a control board into their own vehicle simulations. End users may want to design unit tests that require communicating with a control board and validating motion. Unit test code can simhijack either a physical board or SimCB (probably more useful to use SimCB) and provide specific sensor inputs and commands and validate the motor outputs are as expected.","title":"Advanced use Cases"}]} \ No newline at end of file diff --git a/search/worker.js b/search/worker.js old mode 100755 new mode 100644 diff --git a/sitemap.xml b/sitemap.xml old mode 100755 new mode 100644 index 8c1e09fc..8b2bd1e2 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,87 +2,102 @@ https://mb3hel.github.io/AUVControlBoard/ - 2023-10-01 + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/none/ - 2023-10-01 + 2024-05-05 daily - https://mb3hel.github.io/AUVControlBoard/firmware/build/ - 2023-10-01 + https://mb3hel.github.io/AUVControlBoard/devs/build/ + 2024-05-05 daily - https://mb3hel.github.io/AUVControlBoard/firmware/details/ - 2023-10-01 + https://mb3hel.github.io/AUVControlBoard/devs/buildsimcb/ + 2024-05-05 daily - https://mb3hel.github.io/AUVControlBoard/firmware/developing/ - 2023-10-01 + https://mb3hel.github.io/AUVControlBoard/devs/details/ + 2024-05-05 daily - https://mb3hel.github.io/AUVControlBoard/firmware/math/ - 2023-10-01 + https://mb3hel.github.io/AUVControlBoard/devs/developing/ + 2024-05-05 daily - https://mb3hel.github.io/AUVControlBoard/firmware/overview/ - 2023-10-01 + https://mb3hel.github.io/AUVControlBoard/devs/math/ + 2024-05-05 daily - https://mb3hel.github.io/AUVControlBoard/firmware/simhijack/ - 2023-10-01 + https://mb3hel.github.io/AUVControlBoard/devs/overview/ + 2024-05-05 + daily + + + https://mb3hel.github.io/AUVControlBoard/devs/pythoniface/ + 2024-05-05 + daily + + + https://mb3hel.github.io/AUVControlBoard/devs/simulation/ + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/hardware/sensors/ - 2023-10-01 + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/hardware/v1/ - 2023-10-01 + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/hardware/v2/ - 2023-10-01 + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/user_guide/calibration/ - 2023-10-01 + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/user_guide/comm_protocol/ - 2023-10-01 + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/user_guide/general_use/ - 2023-10-01 + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/user_guide/messages/ - 2023-10-01 + 2024-05-05 daily https://mb3hel.github.io/AUVControlBoard/user_guide/preparing_board/ - 2023-10-01 + 2024-05-05 + daily + + + https://mb3hel.github.io/AUVControlBoard/user_guide/pythoniface/ + 2024-05-05 daily - https://mb3hel.github.io/AUVControlBoard/user_guide/simulator/ - 2023-10-01 + https://mb3hel.github.io/AUVControlBoard/user_guide/simcb_simulator/ + 2024-05-05 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz old mode 100755 new mode 100644 index 9e56abe7e68ec1eaa6e9bfe2d7321366679c9ab0..5259c8e0dbd210828d1ad3a8a5ceba8a7cae65d0 GIT binary patch literal 382 zcmV-^0fGJ>iwFon1UF^^|8r?{Wo=<_E_iKh0M*ycZi6rk0N{I{0&y>)-6m}lrP?O# z3D#Xv2rh|~#4HZ5zWvf4P28s)93g%zW4;5#q1t~tn@teNpx@=2qRcb!3OnQbU4Hm@ zUq9yidR2|V62vSed)nnX#^G6rd7cY05u6;Tz|ad)I$e;s1w~mr<@G8{<6Pi+b{UmL zRiCR+D&yKa4Yue_)MHy16fcL*H}o-}eMK2MQHgW<@@`4dVOMXM6N9H>-v+>qX!1e4ki-qXl%y{OU9{|i@1ZBob%7 literal 361 zcmV-v0hazBiwFpW02yTh|8r?{Wo=<_E_iKh0M*ycPJ}QJ0O0#R1>p{b-HRHQjhgrb zu2)iEmR8#i&J@tMxBC;r-FRpZg-)i!cj*t+;mew224anFpO$%%Cg3{kwHx;7@%1IU zOAqy?n#eHtBqTfDr^@^BL5O8pa@HQK963iC1a~S7Sloi5$nVp7lZ19IaDBV9%3xQY zt8ge|+noZF58A7#%{7Y0@D{7w%G2nvdD@uq}*bwv`$Gk zz=`0q?xi;0l~_ftEb;65lhC1q2$&~Lc98_4owVs=0{7s=~YZ# z413qCVTsAi+A94nyN?LaDs)#gI7Z@_$a5K$b{m0qVxVT2bPuAFMzu;(}XXmIH8H>XBfS6i)D_jpQP_!iQ64GSVacKl}0s{|EICaHWat HPYeJ6lLE4- diff --git a/user_guide/calibration/index.html b/user_guide/calibration/index.html old mode 100755 new mode 100644 index 208b2079..952fb281 --- a/user_guide/calibration/index.html +++ b/user_guide/calibration/index.html @@ -8,21 +8,20 @@ Sensor Calibration - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,11 +47,13 @@
    @@ -110,16 +115,15 @@

    -
    @@ -183,7 +187,7 @@

    MS5837 (Depth Sensor) Calibration

    - - - - - - - - + + + + + + + + diff --git a/user_guide/comm_protocol/index.html b/user_guide/comm_protocol/index.html old mode 100755 new mode 100644 index b4dec845..f8acf546 --- a/user_guide/comm_protocol/index.html +++ b/user_guide/comm_protocol/index.html @@ -8,21 +8,20 @@ Communication Protocol - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,13 +47,15 @@
    @@ -102,16 +107,15 @@

    -
    @@ -193,17 +197,18 @@

    Message Format and Construction

    - - - - - - - - + + + + + + + + diff --git a/user_guide/general_use/index.html b/user_guide/general_use/index.html old mode 100755 new mode 100644 index e08be69c..7a21196a --- a/user_guide/general_use/index.html +++ b/user_guide/general_use/index.html @@ -5,24 +5,23 @@ - Using Control board - AUV Control Board + Using Control Board - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,11 +47,13 @@

    Developers

    @@ -98,25 +103,24 @@

    -

    Using Control Board

    -

    TODO

    +

    TODO: This page is an outline. Update with details / actual contents.

    • General Procedure
      • Connecting to Control Board
      • -
      • System configuration (motor matrix, thruster inversions)
      • +
      • System configuration (motor matrix, thruster inversions, thruster PWM, reldof parameters)
      • Sensor configuration
      • Validating sensor connectivity
      • Modes of operation & setting speed
      • @@ -126,13 +130,21 @@

        Using Control Board

    • LED Indicator Info
    • +
    • Calibrate Sensors
    • +
    • Custom interface code instead of python iface
        +
      • Many users will want to build their own interface.
      • +
      • Python iface is a reference / an easy place to start, but not required
      • +
      • You just need to implement communication with the control board as described in comm protocol and messages pages
      • +
      • Note that users doing so may wish to read the section on SimCB and simulator too as supporting both TCP and UART in implementations can be useful.
      • +
      +
    - - - - - - - - + + + + + + + + diff --git a/user_guide/img/bno055_cal_erase.jpg b/user_guide/img/bno055_cal_erase.jpg old mode 100755 new mode 100644 diff --git a/user_guide/img/bno055_cal_instructions.jpg b/user_guide/img/bno055_cal_instructions.jpg old mode 100755 new mode 100644 diff --git a/user_guide/img/bno055_cal_mode.jpg b/user_guide/img/bno055_cal_mode.jpg old mode 100755 new mode 100644 diff --git a/user_guide/img/bno055_cal_save.jpg b/user_guide/img/bno055_cal_save.jpg old mode 100755 new mode 100644 diff --git a/user_guide/img/bno055_cal_status.jpg b/user_guide/img/bno055_cal_status.jpg old mode 100755 new mode 100644 diff --git a/user_guide/img/comm_protocol_construction.dia b/user_guide/img/comm_protocol_construction.dia old mode 100755 new mode 100644 diff --git a/user_guide/img/comm_protocol_construction.png b/user_guide/img/comm_protocol_construction.png old mode 100755 new mode 100644 diff --git a/user_guide/img/control_board_modes.jpg b/user_guide/img/control_board_modes.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ec41c24d38d8a3f3770fa948e1930bc97c628a9 GIT binary patch literal 54171 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 - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,31 +47,37 @@

    Developers

    @@ -118,29 +127,21 @@

    -

    Messages

    This section describes what specific messages are sent to / received from the control board and what they do / mean. This does not address how messages are constructed or sent. For such information, see Communication Protocol.

    Note: The Communication Protocol page uses the term "payload" for the data being transferred and "message" for the formatted / fully constructed set of data. Here, what we refer to as "messages" are actually the "payload" data, not the constructed data.

    -

    Types of Messages

    -
      -
    • Commands: Messages instructing an action be taken. These messages must be acknowledged upon receipt. The acknowledgement typically contains no data. Sent from PC to control board.
    • -
    • Queries: Messages requesting information. These messages must be acknowledged upon receipt. The acknowledgement will contain the requested information. Sent from PC to control board.
    • -
    • Acknowledgements: A very specific type of message acknowledging receipt of another message (with an error code and optional data). Sent from control board to PC.
    • -
    • Status Messages: Unprompted messages containing information about state / data changes. Sent from control board to PC.
    • -

    Message Definition Conventions

    In the following sections, the following standard is used to describe message contents:

      @@ -151,47 +152,15 @@

      Message Definition Conventions


    -

    Commands

    -

    Configuration Commands

    -

    Motor Matrix Set
    -Motor matrix set command is used to set a single row of the motor matrix. It has the following format

    -
    'M', 'M', 'A', 'T', 'S', [thruster_num], [x], [y], [z], [pitch], [roll], [yaw]
    -
    -

    [thruster_num]: A single byte who'se unsigned value is the thruster number the row data should be set for (1-8).
    -[x], [y], [z], [pitch], [roll], [yaw]: Columns of the motor matrix row being set. Each is a 32-bit float (little endian).
    -This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Motor Matrix Update
    -Motor matrix update command is used to inform the control board the motor matrix has changed. Causes the control board to perform some calculations with the new motor matrix. This should be sent after writing all rows of the motor matrix that should change using the motor matrix set command. The motor matrix update command has the following format

    -
    'M', 'M', 'A', 'T', 'U'
    -
    -

    This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Thruster Inversion Set
    -Thruster inversion set command is used to invert the positive and negative direction of thrusters. It has the following format

    -
    'T', 'I', 'N', 'V', [inv]
    -
    -

    [inv]: A single byte where each bit represents the inversion status of a thruster. The MSB (bit 7) corresponds to thruster 8 and the LSB corresponds to thruster 1 (bit + 1 = thruster). A bit value of 1 means the thruster is inverted. A bit value of 0 means the thruster is not inverted.
    -This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Relative DoF Speed Set
    -Used to set relative speeds of motion in each DoF. There are two groups: linear (x, y, z) and angular (xrot, yrot, zrot). Within each group, use 1.0 for the fastest DoF. Other DoFs in the group are percentages of the fastest speed (from -1.0 to 1.0). This message has the following format

    -
    'R', 'E', 'L', 'D', 'O', 'F', [x], [y], [z], [xrot], [yrot], [zrot]
    -
    -

    [x], [y], [z], [xrot], [yrot], [zrot]: 32-bit little endian floats.
    -This message will be acknowledged. The acknowledge message will contain no result data.

    -

    BNO055 IMU Axis Configure Command
    -Used to configure the BNO055 IMU's axis orientation. Note: This will also reset the accumulated euler angles to zero.

    -
    'B', 'N', 'O', '0', '5', '5', 'A', [config]
    -
    -

    [config]: A single byte. The value of this byte is between 0 and 7 (inclusive) representing on of the BNO055 axis configs (P0 to P7) as described in the BNO055 datasheet. Note: Changing the axis config changes IMU mode. Thus, there will be a brief time afterwards where the IMU may report zeros for all data.
    -This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Stability Assist Mode PID Tune Command
    -Used to tune stability assist mode PID controllers. Note that the rotation PIDs (xrot, yrot, and zrot) are also used in OHOLD mode. Likewise, the depth PID is also used in DHOLD mode. The command has the following format

    -
    'S', 'A', 'S', 'S', 'I', 'S', 'T', 'T', 'N', [which], [kp], [ki], [kd], [limit], [invert]
    -
    -

    [which] indicates which PID to tune ('X' = xrot, 'Y' = yrot, 'Z' = zrot, 'D' = depth hold).
    -[kp], [ki], [kd] are proportional, integral, derivative, and feed-forward gains (32-bit float little endian).
    -[limit] Is the PID controller's max output (limits max speed in the controlled DoF). Must be between 0.0 and 1.0. 32-bit float little endian.
    -[invert] Set to one to invert PID output. Zero otherwise.

    -

    Motor Control Commands

    +

    Types of Messages

    +
      +
    • Commands: Messages instructing an action be taken. These messages must be acknowledged upon receipt. The acknowledgement typically contains no data. Sent from PC to control board.
    • +
    • Queries: Messages requesting information. These messages must be acknowledged upon receipt. The acknowledgement will contain the requested information. Sent from PC to control board.
    • +
    • Acknowledgements: A very specific type of message acknowledging receipt of another message (with an error code and optional data). Sent from control board to PC.
    • +
    • Status Messages: Unprompted messages containing information about state / data changes. Sent from control board to PC.
    • +
    +

    Commands and Queries

    +

    Motor motion commands

    Raw Speed Set
    Used to set motor speeds in RAW mode. This command has the following format.

    'R', 'A', 'W', [speed_1], [speed_2], [speed_3], [speed_4], [speed_5], [speed_6], [speed_7], [speed_8]
    @@ -245,111 +214,125 @@ 

    Motor Control Commands

    Depth is in meters where negative numbers are below the surface.

    x and y are speeds in the x and y DoFs the same as in GLOBAL mode.

    This message will be acknowledged with no data. Note that if the IMU or depth sensor is not working properly, this command will be acknowledged with the "Invalid Command" error code. This can occur if the axis config of the IMU is changed immediately before issuing this command.

    -

    Depth Hold Speed Set
    -Used to set motor speeds in DEPTH_HOLD mode. This command has the following format

    -
    'D', 'H', 'O', 'L', 'D', [x], [y], [pitch_spd], [roll_spd], [yaw_spd], [target_depth]
    -
    -

    Each value is a 32-bit float. Everything except [target_depth] is a speed (same as GLOBAL mode speeds). Target depth is in meters (negative for below surface).

    -

    Other Commands

    Feed Motor Watchdog
    Used to feed the motor watchdog so it does not kill the motors. This command has the following format

    'W', 'D', 'G', 'F'
     

    This message will be acknowledged. The acknowledge message will contain no result data.

    -

    BNO055 Periodic Read
    -Used to enable / disable periodic reading of BNO055 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use IMU data.

    -
    'B', 'N', 'O', '0', '5', '5', 'P', [enable]
    -
    -

    [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled.
    -This message will be acknowledged. The acknowledge message will contain no result data.

    -

    MS5837 Periodic Read
    -Used to enable / disable periodic reading of MS5837 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use depth sensor data.

    -
    'M', 'S', '5', '8', '3', '7', 'P', [enable]
    +

    Vehicle Configuration Commands

    +

    Motor Matrix Set
    +Motor matrix set command is used to set a single row of the motor matrix. It has the following format

    +
    'M', 'M', 'A', 'T', 'S', [thruster_num], [x], [y], [z], [pitch], [roll], [yaw]
     
    -

    [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled.
    +

    [thruster_num]: A single byte who'se unsigned value is the thruster number the row data should be set for (1-8).
    +[x], [y], [z], [pitch], [roll], [yaw]: Columns of the motor matrix row being set. Each is a 32-bit float (little endian).
    This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Reset Command
    -This command is used to rest the control board itself. This will reset the microcontroller on the control board, thus the USB device will disconnect and reconnect (note that if your program still holds the port when this happens, the USB device will likely be assigned a different port number).

    -
    'R', 'E', 'S', 'E', 'T', 0x0D, 0x1E
    +

    Motor Matrix Update
    +Motor matrix update command is used to inform the control board the motor matrix has changed. Causes the control board to perform some calculations with the new motor matrix. This should be sent after writing all rows of the motor matrix that should change using the motor matrix set command. The motor matrix update command has the following format

    +
    'M', 'M', 'A', 'T', 'U'
     
    -

    This message is not acknowledged.

    -

    Simulator Hijack Command
    -This command is used by the simulator to hijack a real control board. This allows the simulator to pass certain information to and receive certain information from the control board. This enables testing of the actual firmware and reproducing bugs under simulation. The command has the following format.

    -
    'S', 'I', 'M', 'H', 'I', 'J', 'A', 'C', 'K', [hijack]
    +

    This message will be acknowledged. The acknowledge message will contain no result data.

    +

    Thruster PWM Parameter Config
    +Configure PWM settings for thrusters.

    +
    'T', 'P', 'W', 'M', [pwm_period], [pwm_zero], [pwm_range]
     
    -

    [hijack] is an 8-bit integer (unsigned) with a value of 1 or 0. If 1, the control board is put into simulator hijack mode. If 0, it is removed from simulator hijack mode.
    +

    Each value is an unsigned 16-bit integer (little endian). pwm_period is the PWM signal period in microseconds (determines PWM frequency / update rate for ESCs). pwm_zero is the pulse width for zero speed in microseconds (typically 1500). pwm_range is the deviation from zero to achieve max speed (when added) or min speed (when subtracted) such that the pulse width pwm_zero + pwm_range microseconds is full forward speed and the pulse width pwm_zero - pwm_range microseconds is full reverse speed.
    This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Save BNO055 Stored Calibration Command
    -This command is used to store a set of calibration constants for the BNO055 to the control board. This will write the "stored calibration constants". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format

    -
    'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'S', [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z]
    +

    Thruster Inversion Set
    +Thruster inversion set command is used to invert the positive and negative direction of thrusters. It has the following format

    +
    'T', 'I', 'N', 'V', [inv]
     
    -

    Each value is a signed 16-bit integer. The meaning of each value is described in the BNO055 datasheet.
    +

    [inv]: A single byte where each bit represents the inversion status of a thruster. The MSB (bit 7) corresponds to thruster 8 and the LSB corresponds to thruster 1 (bit + 1 = thruster). A bit value of 1 means the thruster is inverted. A bit value of 0 means the thruster is not inverted.
    This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Erase BNO055 Stored Calibration Command
    -This command is used to erase calibration constants for the BNO055 from the control board. This will erase the "stored calibration constants". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format

    -
    'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'E'
    -
    -

    This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Write MS5837 Calibration Command
    -This command is used to write the values of the MS5837 calibration constants. The command has the following format

    -
    'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'S', [atm_pressure], [fluid_density]
    +

    Relative DoF Speed Set
    +Used to set relative speeds of motion in each DoF. There are two groups: linear (x, y, z) and angular (xrot, yrot, zrot). Within each group, use 1.0 for the fastest DoF. Other DoFs in the group are percentages of the fastest speed (from 0.0 to 1.0). This message has the following format

    +
    'R', 'E', 'L', 'D', 'O', 'F', [x], [y], [z], [xrot], [yrot], [zrot]
     
    -

    Both values are 32-bit little endian floats.
    +

    [x], [y], [z], [xrot], [yrot], [zrot]: 32-bit little endian floats.
    This message will be acknowledged. The acknowledge message will contain no result data.

    -

    Reset BNO055 Command
    -This command is used to reset / reconfigure the BNO055. This is typically used to clear any auto generated calibration constants. The command has the following format

    -
    'B', 'N', 'O', '0', '5', '5', 'R', 'S', 'T'
    +

    PID Tune Command
    +Used to tune PID controllers. The command has the following format

    +
    'P', 'I', 'D', 'T', 'N', [which], [kp], [ki], [kd], [limit], [invert]
     
    -

    This message will be acknowledged. The acknowledge message will contain no result data.

    -
    - -

    Queries

    -

    Version Info Query
    -Get the version info from the control board.

    -
    'C', 'B', 'V', 'E', 'R'
    -
    -

    This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format

    -
    [cb_ver],[fw_ver_major],[fw_ver_minor],[fw_ver_revision],[fw_ver_type],[fw_ver_build]
    -
    -

    Each value is an unsigned 8-bit integer. All are simply interpreted as numbers, except for fw_ver_type, which is an ASCII character.
    -cb_ver: Version of the control board hardware (CBv1 or CBv2 = 1 or 2; 0 = SimCB in simulator)
    -fw_ver_major: Major version of firmware running on the control board
    -fw_ver_minor: Minor version of firmware running on the control board
    -fw_ver_revision: Revision version of firmware running on the control board
    -fw_ver_type: Type of firmware release. 'a' = alpha, 'b' = beta, 'c' = release candidate (rc), ' ' (space) = full release
    -fw_ver_build: Build number for pre-release firmware. Should be ignored for fw_ver_type release (' ')

    +

    [which] indicates which PID to tune ('X' = xrot, 'Y' = yrot, 'Z' = zrot, 'D' = depth hold).
    +[kp], [ki], [kd] are proportional, integral, derivative, and feed-forward gains (32-bit float little endian).
    +[limit] Is the PID controller's max output (limits max speed in the controlled DoF). Must be between 0.0 and 1.0. 32-bit float little endian.
    +[invert] Set to one to invert PID output. Zero otherwise.

    +

    Sensor Commands and Queries

    Sensor Status Query
    -Gets the status of all sensors (BNO055 and MS5837).

    +Check which IMU and depth sensor is currently in use

    'S', 'S', 'T', 'A', 'T'
     

    This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format

    -
    [status_byte]
    -
    -

    [status_byte]: Single byte containing bits for the status of all sensors. Bit 0 (LSB) is BNO055 status. Bit 1 is MS5837 status. A status bit of 1 indicates the sensor is "ready" (connected and can be used). A status bit of 0 indicates the sensor is "not ready".

    -

    BNO055 Read
    -Reads BNO055 IMU data once.

    -
    'B', 'N', 'O', '0', '5', '5', 'R'
    +
    [which_imu], [which_depth]
    +
    +

    [which_imu]: Single byte indicating which IMU is in use
    +- No IMU: 0
    +- Simulated IMU (under simulator hijack): 1
    +- BNO055 IMU: 2

    +

    [which_depth]: Single byte indicating which depth sensor is in use
    +- No Depth sensor: 0
    +- Simulated depth sensor (under simulator hijack): 1
    +- MS5837 Depth sensor: 2

    +

    IMU Read
    +Reads IMU data once.

    +
    'I', 'M', 'U', 'R'
     

    This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the BNO055 data status message.

    [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw]
     

    Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles.

    -

    MS5837 Read
    -Reads MS5837 data once.

    -
    'M', 'S', '5', '8', '3', '7', 'R'
    +

    IMU Raw Read
    +Reads raw IMU data once.

    +
    'I', 'M', 'U', 'W'
    +
    +

    This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the BNO055 data status message.

    +
    [accel_x], [accel_y], [accel_z], [gyro_x], [gyro_y], [gyro_z]
    +
    +

    Each value is a 32-bit float, little endian.

    +

    IMU Periodic Read
    +Used to enable / disable periodic reading of IMU data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use IMU data.

    +
    'I', 'M', 'U', 'P', [enable]
    +
    +

    [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled.
    +This message will be acknowledged. The acknowledge message will contain no result data.

    +

    Depth Read
    +Reads depth sensor data once.

    +
    'D', 'E', 'P', 'T', 'H', 'R'
     

    This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format. Note that this is the same format as the data contained within the MS5837 data status message.

    [depth_m], [pressure_pa], [temp_c]
     

    depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water).

    -

    Last Reset Cause Query
    -Get error code for last system reset cause of the control board. Generally not useful for end users, except for reporting errors. Mainly a debug / development tool. See error codes in firmware source debug.h.

    -
    'R', 'S', 'T', 'W', 'H', 'Y'
    +

    Depth Periodic Read
    +Used to enable / disable periodic reading of MS5837 data. This will only impact data being sent from control board to the pc. The control board itself will continue to read and use depth sensor data.

    +
    'D', 'E', 'P', 'T', 'G', 'P', [enable]
     
    -

    This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format.

    -
    [error_code]
    +

    [enable] is an 8-bit integer with a value of either 1 or 0. If 1, reading data periodically is enabled. If 0, reading data periodically is disabled.
    +This message will be acknowledged. The acknowledge message will contain no result data.

    +

    BNO055 IMU Configuration

    +

    BNO055 IMU Axis Configure Command
    +Used to configure the BNO055 IMU's axis orientation. Note: This will also reset the accumulated euler angles to zero.

    +
    'B', 'N', 'O', '0', '5', '5', 'A', [config]
     
    -

    error_code is a 32-bit integer (signed), little endian.

    +

    [config]: A single byte. The value of this byte is between 0 and 7 (inclusive) representing on of the BNO055 axis configs (P0 to P7) as described in the BNO055 datasheet. Note: Changing the axis config changes IMU mode. Thus, there will be a brief time afterwards where the IMU may report zeros for all data.
    +This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code.

    +

    Save BNO055 Stored Calibration Command
    +This command is used to store a set of calibration constants for the BNO055 to the control board. This will write the "stored calibration constants". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format

    +
    'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'S', [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z]
    +
    +

    Each value is a signed 16-bit integer. The meaning of each value is described in the BNO055 datasheet.
    +This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code.

    +

    Erase BNO055 Stored Calibration Command
    +This command is used to erase calibration constants for the BNO055 from the control board. This will erase the "stored calibration constants". This command will also cause the IMU to be reconfigured (this can take some time, so acknowledgements for this command may take longer than most). The command has the following format

    +
    'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'E'
    +
    +

    This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code.

    +

    Reset BNO055 Command
    +This command is used to reset / reconfigure the BNO055. This is typically used to clear any auto generated calibration constants. The command has the following format

    +
    'B', 'N', 'O', '0', '5', '5', 'R', 'S', 'T'
    +
    +

    This message will be acknowledged. The acknowledge message will contain no result data. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code.

    Read BNO055 Stored Calibration Query
    This command is used to read a set of calibration constants for the BNO055 from the control board. This will read the "stored calibration constants". The command has the following format

    'S', 'C', 'B', 'N', 'O', '0', '5', '5', 'R'
    @@ -366,7 +349,7 @@ 

    Queries

    This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the "Invalid Command" error code. If acknowledged with no error, the response will contain data in the following format.

    [status]
     
    -

    status is an 8-bit integer. The value of status is the value of the BNO055's CALIB_STAT register. The meaning of this number is described in the BNO055 datasheet.

    +

    status is an 8-bit integer. The value of status is the value of the BNO055's CALIB_STAT register. The meaning of this number is described in the BNO055 datasheet. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code.

    Read BNO055 Live Calibration Values Query
    This command is used to read a set of calibration constants from the BNO055. This will read the "live calibration constants" directly from the BNO055. Note that the calibration constants are only valid if the calibration status from the BNO055 is 3 for the accelerometer and gyroscope. The command has the following format

    'B', 'N', 'O', '0', '5', '5', 'C', 'V'
    @@ -374,7 +357,8 @@ 

    Queries

    This message will be acknowledged. Note that if the IMU is not working properly, this command will be acknowledged with the "Invalid Command" error code. If acknowledged with no error, the response will contain data in the following format.

    [accel_offset_x], [accel_offset_y], [accel_offset_z], [accel_radius], [gyro_offset_x], [gyro_offset_y], [gyro_offset_z]
     
    -

    All values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet.

    +

    All values in the acknowledge data are signed 16-bit integers. The meaning of these integers is described in the BNO055 datasheet. Note that if the BNO055 is not the active IMU (see sensor status query), this will be acknowledged using the INVALID_CMD error code.

    +

    MS5837 Depth Sensor Configuration

    Read MS5837 Calibration Query
    This command is used to read the values of the MS5837 calibration constants. The command has the following format

    'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'G'
    @@ -383,8 +367,51 @@ 

    Queries

    [atm_pressure], [fluid_density]
     

    Both values are 32-bit little endian floats.

    -
    - +

    Write MS5837 Calibration Command
    +This command is used to write the values of the MS5837 calibration constants. The command has the following format

    +
    'M', 'S', '5', '8', '3', '7', 'C', 'A', 'L', 'S', [atm_pressure], [fluid_density]
    +
    +

    Both values are 32-bit little endian floats.
    +This message will be acknowledged. The acknowledge message will contain no result data.

    +

    Misc Commands and Queries

    +

    Reset Command
    +This command is used to rest the control board itself. This will reset the microcontroller on the control board, thus the USB device will disconnect and reconnect (note that if your program still holds the port when this happens, the USB device will likely be assigned a different port number).

    +
    'R', 'E', 'S', 'E', 'T', 0x0D, 0x1E
    +
    +

    This message is not acknowledged.

    +

    Last Reset Cause Query
    +Get error code for last system reset cause of the control board. Generally not useful for end users, except for reporting errors. Mainly a debug / development tool. See error codes in firmware source debug.h.

    +
    'R', 'S', 'T', 'W', 'H', 'Y'
    +
    +

    This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format.

    +
    [error_code]
    +
    +

    error_code is a 32-bit integer (signed), little endian.

    +

    Simulator Hijack Command
    +This command is used by the simulator to hijack a real control board. This allows the simulator to pass certain information to and receive certain information from the control board. This enables testing of the actual firmware and reproducing bugs under simulation. The command has the following format.

    +
    'S', 'I', 'M', 'H', 'I', 'J', 'A', 'C', 'K', [hijack]
    +
    +

    [hijack] is an 8-bit integer (unsigned) with a value of 1 or 0. If 1, the control board is put into simulator hijack mode. If 0, it is removed from simulator hijack mode.
    +This message will be acknowledged. The acknowledge message will contain no result data.

    +

    Simulator Data Command
    +This command is used by the simulator to send simulated sensor data to a hijacked control board.

    +
    'S', 'I', 'M', 'D', 'A', 'T', [w], [x], [y], [z], [depth]
    +
    +

    All values are little endian floats (32-bit). x, y, z, w are current quaternion (IMU data) depth is current depth (depth sensor data).

    +

    Version Info Query
    +Get the version info from the control board.

    +
    'C', 'B', 'V', 'E', 'R'
    +
    +

    This message will be acknowledged. If acknowledged with no error, the response will contain data in the following format

    +
    [cb_ver],[fw_ver_major],[fw_ver_minor],[fw_ver_revision],[fw_ver_type],[fw_ver_build]
    +
    +

    Each value is an unsigned 8-bit integer. All are simply interpreted as numbers, except for fw_ver_type, which is an ASCII character.
    +cb_ver: Version of the control board hardware (CBv1 or CBv2 = 1 or 2; 0 = SimCB in simulator)
    +fw_ver_major: Major version of firmware running on the control board
    +fw_ver_minor: Minor version of firmware running on the control board
    +fw_ver_revision: Revision version of firmware running on the control board
    +fw_ver_type: Type of firmware release. 'a' = alpha, 'b' = beta, 'c' = release candidate (rc), ' ' (space) = full release
    +fw_ver_build: Build number for pre-release firmware. Should be ignored for fw_ver_type release (' ')

    Acknowledgements

    An acknowledgement message has the following format

    'A', 'C', 'K', [ack_id], [error_code], [result]
    @@ -397,22 +424,20 @@ 

    Acknowledgements

            3 = Invalid Command: Command is known, but is not valid at this time.
            255 = Reserved: Control board will not use this code. Typically used as timeout.
    [result]: Optional data of variable size attached to the acknowledge message. Its size, format, and meaning depends on the message being acknowledged.

    -
    -

    Status Messages

    Motor Watchdog Status
    Motor watch status message is used by the control board to notify the PC about changes to motor (watchdog) state. It has the following format

    'W', 'D', 'G', 'S', [status]
     

    [status] is a single byte. A value of 1 indicates the motors are enabled. A value of zero indicates the motors are currently killed by the watchdog.

    -

    BNO055 Data Status
    -Used by the control board to periodically send IMU data to the PC. Only sent when BNO055 periodic reads are enabled via the BNO055 periodic read command. The message has the following format

    -
    'B', 'N', 'O', '0', '5', '5', 'D', [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw]
    +

    IMU Data Status
    +Used by the control board to periodically send IMU data to the PC. Only sent when IMU periodic reads are enabled via the IMU periodic read command. The message has the following format

    +
    'I', 'M', 'U', 'D', [quat_w], [quat_x], [quat_y], [quat_z], [accum_pitch], [accum_roll], [accum_yaw]
     

    Each value is a 32-bit float, little endian. quat_ values are components of the orientation quaternion. accum_ values are accumulated euler angles.

    -

    MS5837 Data Status
    +

    Depth Data Status
    Used by the control board to periodically send IMU data to the PC. Only sent when BNO055 periodic reads are enabled via the BNO055 periodic read command. The message has the following format

    -
    'M', 'S', '5', '8', '3', '7', 'D', [depth_m], [pressure_pa], [temp_c]
    +
    'D', 'E', 'P', 'T', 'H', 'D', [depth_m], [pressure_pa], [temp_c]
     

    depth_m is a 32-bit float, little endian (meters below surface). pressure_pa is a 32-bit float, little endian (measured pressure in Pa). temp_c is a 32-bit float, little endian (temperature of air / water).

    Debug Status Messages
    @@ -425,6 +450,24 @@

    Status Messages

    'D', 'B', 'G', 'D', 'A', 'T', [msg]
     

    msg is arbitrary data.

    +

    Heartbeat Status Messages
    +Sent from control board periodically to indicate that it still exists and is operating as expected. This is generally ignored by end users. It is mostly intended to ensure communication occurs periodically in SimCB so connection drops are detectable.

    +
    'H', 'E', 'A', 'R', 'T', 'B', 'E', 'A', 'T'
    +
    +

    Simulator Status Message
    +Sent from a simulator hijacked control board periodically to provide simulator with state and motor speed information.

    +
    'S', 'I', 'M', 'S', 'T', 'A', 'T', [t1], [t2], [t3], [t4], [t5], [t6], [t7], [t8], [mode], [wdog_killed]
    +
    +

    Each value t1 to t8 is a 32-bit little endian float representing thruster speeds 1 - 8 respectively.
    +mode is an unsigned 8-bit integer indicating the control board's current operating mode from one of the following

    +
      +
    • Raw = 0
    • +
    • Local = 1
    • +
    • Global = 2
    • +
    • Sassist = 3
    • +
    • Ohold = 5
    • +
    +

    wdog_killed is an unsigned 8-bit integer indicating if the control board's motors are killed due to motor watchdog timeout. 1 indicates that motors are killed. 0 indicates not killed.

    @@ -464,17 +507,18 @@

    Status Messages

    - - - - - - - - + + + + + + + + diff --git a/user_guide/preparing_board/index.html b/user_guide/preparing_board/index.html old mode 100755 new mode 100644 index 44f3c907..bbd58120 --- a/user_guide/preparing_board/index.html +++ b/user_guide/preparing_board/index.html @@ -8,21 +8,20 @@ Preparing a Control Board - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -46,13 +45,15 @@

    User Guide

    Developers

    @@ -98,32 +103,44 @@

    -

    Preparing a Control Board

    -

    TODO:

    +

    TODO: Improve this with step by step instructions and screenshots

      -
    • Flashing firmware
    • -
    • Wiring
    • -
    • Testing USB
    • +
    • Assemble a control board (see v1 hardware or v2 hardware)
    • +
    • Flash the firmware
        +
      • See Building & Flashing for details
      • +
      • Install bossa cli or dfu-util and make sure the binaries (bossac or dfu-util) are in your path
      • +
      • Install python
      • +
      • Download and extract release package from GitHub
      • +
      • Put the board into bootloader mode
          +
        • v1: Double press reset button quickly
        • +
        • v2: Hold boot button, press and release reset, release boot button
        • +
        +
      • +
      • In firmware folder from release package run ./flash.py [v1/v2] Release
      • +
      • This will run the flash tool
      • +
      +
    • +
    • Connect external sensors
    - - - - - - - - + + + + + + + + diff --git a/user_guide/simulator/index.html b/user_guide/pythoniface/index.html old mode 100755 new mode 100644 similarity index 64% rename from user_guide/simulator/index.html rename to user_guide/pythoniface/index.html index 3d890535..4df793a8 --- a/user_guide/simulator/index.html +++ b/user_guide/pythoniface/index.html @@ -3,26 +3,25 @@ - + - Using the Simulator - AUV Control Board + Using the Python Interface - AUV Control Board - + - - - + + @@ -34,7 +33,7 @@ AUV Control Board
    - +
    @@ -48,11 +47,13 @@

    Developers

    @@ -98,27 +103,31 @@

    -
    -

    Using the Simulator

    -

    TODO

    +

    Using the Python Interface

    +

    TODO: About iface (describe the different files)

    +

    TODO: Launch script and what it does. Include port selection, but NOT use of SimCB, and NOT use of simulator.

    +

    TODO: Vehicle configs and what they do

    +

    TODO: Example scripts descriptions

    +

    TODO: Guide on adding new vehicle configs

    +

    TODO: Guide on adding custom scripts

    - - - - - - - - + + + + + + + + diff --git a/user_guide/simcb_simulator/index.html b/user_guide/simcb_simulator/index.html new file mode 100644 index 00000000..88edfcd2 --- /dev/null +++ b/user_guide/simcb_simulator/index.html @@ -0,0 +1,225 @@ + + + + + + + + Using SimCB and Simulator - AUV Control Board + + + + + + + + + + + + + +
    + + +
    + +
    +
    + +
    +
    +
    +
    + +

    Using SimCB and Simulator

    +

    TODO: Details and improvements in various sections.

    +

    TODO: Add port number info.

    +

    SimCB

    +

    SimCB is a version of the control board firmware which is built as a binary that runs on Windows, macOS, or Linux. By running this binary, you can run the control board firmware on your computer without having a physical control board.

    +

    SimCB only supports simulator hijack mode (meaning only the sim IMU and depth sensors will work and thruster speeds will be reported back over comms interface). Instead of communicating with a physical control board via UART, you communicate with SimCB via TCP (the exact same messages are sent, just treat what you send over UART and TCP as byte streams). SimCB is a TCP server so code connecting to SimCB must be a TCP client.

    +

    TODO: How to use SimCB instead of real control board over uart (including instructions to run SimCB)

    +

    Using SimCB allows testing various aspects of communication with the control board and system behavior without having a physical control board.

    +

    Simulator

    +

    The simulator is a program that runs on your computer and models a vehicle in 3D space. The simulator hijacks a control board and provides the control board "fake" sensor data from the simulated environment. The control board then provides the simulator motor speeds so the simulator can move the vehicle appropriately in the simulated environment. When in this mode, user code should communicate with the simulator over TCP instead of communicating with the control board directly.

    +

    See the simulator's README for a description of how to communicate with the simulator. Note that the python iface scripts have builtin support for using the simulator.

    +

    The provided simulator (linked above) is fairly simple, and is mostly intended for development testing of the control board firmware. However, it can be useful to end users as well to see how the vehicle is expected to behave. Note thought that the environment provided in this simulator is very limited. For more complex use cases, users may wish to fork and adapt the simulator to their vehicles / use cases.

    +

    TODO: Instructions to run simulator and connect to real control board.

    +

    Combining SimCB and Simulator

    +

    By combining the simulator (providing a simulated vehicle and environment) with SimCB (providing a control board without hardware), it is possible to test vehicle motion without any hardware (no vehicle in water and no real control board).

    +

    TODO: Instructions to run the simulator and connect to SimCB

    +

    Ways to access Control Board

    +

    There are four general ways to use the control board

    +
      +
    1. +

      Directly using a real control board over UART (via USB)

      +
    2. +
    3. +

      Directly using SimCB over TCP

      +
    4. +
    5. +

      Using the simulator attached to a real control board (talking to the simulator over TCP)

      +
    6. +
    7. +

      Using the simulator attached to SimCB (talking to the simulator over TCP)

      +
    8. +
    +

    +

    TODO: Discussion of these methods and when they are useful

    +

    Advanced use Cases

    +
      +
    1. +

      End users can implement functionality in their own simulators to "hijack" a control board for simulation (referred to as "simhijack") just as the provided simulator does. When a control board is simhijacked, it does not use real sensors or create PWM signals for real thrusters. Instead, it communicates with a simulator. The control board gives the simulator motor speeds and the simulator is expected to provide the control board with sensor data. The protocol for doing so is fairly simple (see messages documentation) allowing end users to incorporate a control board into their own vehicle simulations.

      +
    2. +
    3. +

      End users may want to design unit tests that require communicating with a control board and validating motion. Unit test code can simhijack either a physical board or SimCB (probably more useful to use SimCB) and provide specific sensor inputs and commands and validate the motor outputs are as expected.

      +
    4. +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + + + GitHub + + + + « Previous + + + Next » + + +
    + + + + + + + + + + + +