├── .DS_Store ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config.inav ├── docs ├── cmd.PNG ├── devManager.PNG ├── logo.png ├── osd.jpg ├── python.png └── video.png ├── flash-spiffs.sh ├── platformio.ini ├── spiffs ├── ace.js.gz ├── app.js ├── ext-searchbox.js.gz ├── favicon.ico ├── index.htm ├── index.htm.old ├── main.css ├── material-design-icons.eot ├── material-design-icons.svg ├── material-design-icons.ttf ├── material-design-icons.woff ├── mode-css.js.gz ├── mode-html.js.gz ├── mode-javascript.js.gz ├── phonon.css ├── phonon.js └── worker-html.js.gz ├── src ├── .DS_Store ├── inavradarlogo.h ├── lib │ ├── LoRa.cpp │ ├── LoRa.h │ ├── MSP.cpp │ └── MSP.h ├── main.cpp └── main.h ├── testing ├── .DS_Store ├── air-to-air-433.zip └── air-to-air-868.zip └── tools └── mkspiffs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .pioenvs 3 | .piolibdeps 4 | .clang_complete 5 | .gcc-flags.json 6 | .DS_Store 7 | src/.DS_Store 8 | platformio.ini 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < https://docs.platformio.org/page/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < https://docs.platformio.org/page/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < https://docs.platformio.org/page/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choose one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # - platformio update 39 | # 40 | # script: 41 | # - platformio run 42 | 43 | 44 | # 45 | # Template #2: The project is intended to be used as a library with examples. 46 | # 47 | 48 | # language: python 49 | # python: 50 | # - "2.7" 51 | # 52 | # sudo: false 53 | # cache: 54 | # directories: 55 | # - "~/.platformio" 56 | # 57 | # env: 58 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 59 | # - PLATFORMIO_CI_SRC=examples/file.ino 60 | # - PLATFORMIO_CI_SRC=path/to/test/directory 61 | # 62 | # install: 63 | # - pip install -U platformio 64 | # - platformio update 65 | # 66 | # script: 67 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 2, June 1991 4 | 5 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 6 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies 9 | of this license document, but changing it is not allowed. 10 | 11 | Preamble 12 | 13 | The licenses for most software are designed to take away your freedom to share and change it. 14 | By contrast, the GNU General Public License is intended to guarantee your freedom to share 15 | and change free software--to make sure the software is free for all its users. 16 | This General Public License applies to most of the Free Software Foundation's software and 17 | to any other program whose authors commit to using it. (Some other Free Software Foundation 18 | software is covered by the GNU Lesser General Public License instead.) You can apply it 19 | to your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not price. Our General Public 22 | Licenses are designed to make sure that you have the freedom to distribute copies of free 23 | software (and charge for this service if you wish), that you receive source code or can get 24 | it if you want it, that you can change the software or use pieces of it in new free programs; 25 | and that you know you can do these things. 26 | 27 | To protect your rights, we need to make restrictions that forbid anyone to deny you these 28 | rights or to ask you to surrender the rights. These restrictions translate to certain 29 | responsibilities for you if you distribute copies of the software, or if you modify it. 30 | 31 | For example, if you distribute copies of such a program, whether gratis or for a fee, you 32 | must give the recipients all the rights that you have. You must make sure that they, too, 33 | receive or can get the source code. And you must show them these terms so they know their rights. 34 | 35 | We protect your rights with two steps: (1) copyright the software, and (2) offer you this 36 | license which gives you legal permission to copy, distribute and/or modify the software. 37 | 38 | Also, for each author's protection and ours, we want to make certain that everyone understands 39 | that there is no warranty for this free software. If the software is modified by someone else 40 | and passed on, we want its recipients to know that what they have is not the original, 41 | so that any problems introduced by others will not reflect on the original authors' reputations. 42 | 43 | Finally, any free program is threatened constantly by software patents. We wish to avoid 44 | the danger that redistributors of a free program will individually obtain patent licenses, 45 | in effect making the program proprietary. To prevent this, we have made it clear that any 46 | patent must be licensed for everyone's free use or not licensed at all. 47 | 48 | The precise terms and conditions for copying, distribution and modification follow. 49 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 50 | 51 | 0. This License applies to any program or other work which contains a notice placed by the 52 | copyright holder saying it may be distributed under the terms of this General Public License. 53 | The "Program", below, refers to any such program or work, and a "work based on the Program" 54 | means either the Program or any derivative work under copyright law: that is to say, a work 55 | containing the Program or a portion of it, either verbatim or with modifications and/or 56 | translated into another language. (Hereinafter, translation is included without limitation 57 | in the term "modification".) Each licensee is addressed as "you". 58 | 59 | Activities other than copying, distribution and modification are not covered by this License; 60 | they are outside its scope. The act of running the Program is not restricted, and the output 61 | from the Program is covered only if its contents constitute a work based on the Program 62 | (independent of having been made by running the Program). Whether that is true depends on what 63 | the Program does. 64 | 65 | 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, 66 | in any medium, provided that you conspicuously and appropriately publish on each copy an 67 | appropriate copyright notice and disclaimer of warranty; keep intact all the notices that 68 | refer to this License and to the absence of any warranty; and give any other recipients of 69 | the Program a copy of this License along with the Program. 70 | 71 | You may charge a fee for the physical act of transferring a copy, and you may at your option 72 | offer warranty protection in exchange for a fee. 73 | 74 | 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work 75 | based on the Program, and copy and distribute such modifications or work under the terms of 76 | Section 1 above, provided that you also meet all of these conditions: 77 | 78 | a) You must cause the modified files to carry prominent notices stating that you changed 79 | the files and the date of any change. 80 | b) You must cause any work that you distribute or publish, that in whole or in part contains 81 | or is derived from the Program or any part thereof, to be licensed as a whole at no charge 82 | to all third parties under the terms of this License. 83 | c) If the modified program normally reads commands interactively when run, you must cause it, 84 | when started running for such interactive use in the most ordinary way, to print or 85 | display an announcement including an appropriate copyright notice and a notice that there 86 | is no warranty (or else, saying that you provide a warranty) and that users may redistribute 87 | the program under these conditions, and telling the user how to view a copy of this License. 88 | (Exception: if the Program itself is interactive but does not normally print such an announcement, 89 | your work based on the Program is not required to print an announcement.) 90 | 91 | These requirements apply to the modified work as a whole. If identifiable sections of that 92 | work are not derived from the Program, and can be reasonably considered independent and 93 | separate works in themselves, then this License, and its terms, do not apply to those 94 | sections when you distribute them as separate works. But when you distribute the same s 95 | ections as part of a whole which is a work based on the Program, the distribution of 96 | the whole must be on the terms of this License, whose permissions for other licensees 97 | extend to the entire whole, and thus to each and every part regardless of who wrote it. 98 | 99 | Thus, it is not the intent of this section to claim rights or contest your rights to work 100 | written entirely by you; rather, the intent is to exercise the right to control the 101 | distribution of derivative or collective works based on the Program. 102 | 103 | In addition, mere aggregation of another work not based on the Program with the Program 104 | (or with a work based on the Program) on a volume of a storage or distribution medium 105 | does not bring the other work under the scope of this License. 106 | 107 | 3. You may copy and distribute the Program (or a work based on it, under Section 2) 108 | in object code or executable form under the terms of Sections 1 and 2 above provided t 109 | hat you also do one of the following: 110 | 111 | a) Accompany it with the complete corresponding machine-readable source code, 112 | which must be distributed under the terms of Sections 1 and 2 above on a medium 113 | customarily used for software interchange; or, 114 | b) Accompany it with a written offer, valid for at least three years, to give any 115 | third party, for a charge no more than your cost of physically performing source 116 | distribution, a complete machine-readable copy of the corresponding source code, 117 | to be distributed under the terms of Sections 1 and 2 above on a medium customarily 118 | used for software interchange; or, 119 | c) Accompany it with the information you received as to the offer to distribute 120 | corresponding source code. (This alternative is allowed only for noncommercial 121 | distribution and only if you received the program in object code or executable 122 | form with such an offer, in accord with Subsection b above.) 123 | 124 | The source code for a work means the preferred form of the work for making modifications 125 | to it. For an executable work, complete source code means all the source code for all 126 | modules it contains, plus any associated interface definition files, plus the scripts 127 | used to control compilation and installation of the executable. However, as a special 128 | exception, the source code distributed need not include anything that is normally 129 | distributed (in either source or binary form) with the major components (compiler, 130 | kernel, and so on) of the operating system on which the executable runs, unless that 131 | component itself accompanies the executable. 132 | 133 | If distribution of executable or object code is made by offering access to copy from a 134 | designated place, then offering equivalent access to copy the source code from the same 135 | place counts as distribution of the source code, even though third parties are not 136 | compelled to copy the source along with the object code. 137 | 138 | 4. You may not copy, modify, sublicense, or distribute the Program except as expressly 139 | provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute 140 | the Program is void, and will automatically terminate your rights under this License. 141 | However, parties who have received copies, or rights, from you under this License will 142 | not have their licenses terminated so long as such parties remain in full compliance. 143 | 144 | 5. You are not required to accept this License, since you have not signed it. However, 145 | nothing else grants you permission to modify or distribute the Program or its derivative 146 | works. These actions are prohibited by law if you do not accept this License. Therefore, 147 | by modifying or distributing the Program (or any work based on the Program), you indicate 148 | your acceptance of this License to do so, and all its terms and conditions for copying, 149 | distributing or modifying the Program or works based on it. 150 | 151 | 6. Each time you redistribute the Program (or any work based on the Program), the recipient 152 | automatically receives a license from the original licensor to copy, distribute or modify 153 | the Program subject to these terms and conditions. You may not impose any further 154 | restrictions on the recipients' exercise of the rights granted herein. You are not 155 | responsible for enforcing compliance by third parties to this License. 156 | 157 | 7. If, as a consequence of a court judgment or allegation of patent infringement or for 158 | any other reason (not limited to patent issues), conditions are imposed on you (whether 159 | by court order, agreement or otherwise) that contradict the conditions of this License, 160 | they do not excuse you from the conditions of this License. If you cannot distribute so 161 | as to satisfy simultaneously your obligations under this License and any other pertinent 162 | obligations, then as a consequence you may not distribute the Program at all. For example, 163 | if a patent license would not permit royalty-free redistribution of the Program by all 164 | those who receive copies directly or indirectly through you, then the only way you could 165 | satisfy both it and this License would be to refrain entirely from distribution of the Program. 166 | 167 | If any portion of this section is held invalid or unenforceable under any particular 168 | circumstance, the balance of the section is intended to apply and the section as a 169 | whole is intended to apply in other circumstances. 170 | 171 | It is not the purpose of this section to induce you to infringe any patents or other 172 | property right claims or to contest validity of any such claims; this section has the 173 | sole purpose of protecting the integrity of the free software distribution system, 174 | which is implemented by public license practices. Many people have made generous 175 | contributions to the wide range of software distributed through that system in reliance 176 | on consistent application of that system; it is up to the author/donor to decide if he or 177 | she is willing to distribute software through any other system and a licensee cannot impose that choice. 178 | 179 | This section is intended to make thoroughly clear what is believed to be a consequence 180 | of the rest of this License. 181 | 182 | 8. If the distribution and/or use of the Program is restricted in certain countries either 183 | by patents or by copyrighted interfaces, the original copyright holder who places the Program 184 | under this License may add an explicit geographical distribution limitation excluding those 185 | countries, so that distribution is permitted only in or among countries not thus excluded. 186 | In such case, this License incorporates the limitation as if written in the body of this License. 187 | 188 | 9. The Free Software Foundation may publish revised and/or new versions of the 189 | General Public License from time to time. Such new versions will be similar in spirit 190 | to the present version, but may differ in detail to address new problems or concerns. 191 | 192 | Each version is given a distinguishing version number. If the Program specifies a 193 | version number of this License which applies to it and "any later version", you have 194 | the option of following the terms and conditions either of that version or of any later 195 | version published by the Free Software Foundation. If the Program does not specify a 196 | version number of this License, you may choose any version ever published by the Free Software Foundation. 197 | 198 | 10. If you wish to incorporate parts of the Program into other free programs whose 199 | distribution conditions are different, write to the author to ask for permission. 200 | For software which is copyrighted by the Free Software Foundation, write to the Free 201 | Software Foundation; we sometimes make exceptions for this. Our decision will be guided 202 | by the two goals of preserving the free status of all derivatives of our free software 203 | and of promoting the sharing and reuse of software generally. 204 | 205 | NO WARRANTY 206 | 207 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, 208 | TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE 209 | COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF 210 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 211 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK 212 | AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 213 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 214 | 215 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 216 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM 217 | AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 218 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 219 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE 220 | OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH 221 | ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY 222 | OF SUCH DAMAGES. 223 | 224 | END OF TERMS AND CONDITIONS 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/mistyk/inavradar-ESP32/raw/master/docs/logo.png) 2 | 3 | # LoRa based inter UAV communication 4 | 5 | INAV-Radar is an addition to the [INAV](https://github.com/iNavFlight/inav) flight control software, it relays information about UAVs in the area to the flight controller for display on the OSD. INAV-Radar does this by using [LoRa](https://en.wikipedia.org/wiki/LoRa) radio to broadcast position, altitude, speed and plane name. It also listens for other UAVs so INAV OSD can display this information as a HUD. 6 | 7 | [![Video](https://github.com/mistyk/inavradar-ESP32/raw/master/docs/video.png)](https://www.youtube.com/watch?v=7ww0YOGN7F0) 8 | 9 | ## News 10 | 11 | ESP32 Firmware Installer: [Download](https://github.com/KingKone/INAV-Radar_Installer/releases) 12 | 13 | RCgroups thread: [INAV-Radar on RCgroups](https://www.rcgroups.com/forums/showthread.php?3304673-iNav-Radar-ESP32-LoRa-modems) 14 | 15 | *** 1.30 (2019/05/18) 16 | 17 | Radar logo at boot 18 | Better timings, greatly reduced display latency 19 | Many cosmetic tweaks and fixes 20 | Newest inav 2.2.dev REQUIRED (built 2019/05/18 or newer) 21 | 22 | 23 | *** 1.20 (2019/05/14) 24 | 25 | Better timing for MSP and air packets 26 | 5 nodes capable, but locked at 4 nodes for now 27 | Faster rate for MSP messages to improve tracking accuracy 28 | in iNav 2.2, faster display to reduce tracking stuttering 29 | Known issue : sometime the debug page with the timings 30 | reboots the module 31 | 32 | 33 | *** 1.01 (2019/05/06) 34 | 35 | - More detailled screens per nodes 36 | - Displays the local vbat and mAh. These datas are not yet 37 | transmitted to the other nodes. 38 | - Pressing the top button during the boot sequence will put 39 | the module in "silent" mode (ground-station), it will only 40 | receive, and won't transmit, thus freeing a slot. Button 41 | must be pressed at least once, between the time the module 42 | is plugged and the end of the SCAN progress bar. 43 | - No need to update iNav since 1.00, no changes. 44 | 45 | *** 1.00 (2019/04/25) 46 | 47 | - Initial release 48 | - Require iNav 2.2-dev, including the latest version for the 49 | Hud branch (build date 2019/04/27 or newer) 50 | - Cycle time 500ms, slotspacing 125ms, LoRA SF9 bw250, 51 | maximum 4 nodes (you + 3 others) 52 | 53 | If you feel brave engough to be a tester, just ask us in the Facebook group. [Contact](#contact) 54 | 55 | ## Index 56 | [Hardware](#hardware) 57 | 58 | [Development](#development) 59 | 60 | [Testing](#testing) 61 | 62 | [Wireing](#Wireing) 63 | 64 | [FC settings](#FC-settings) 65 | 66 | [ESP32 commands](#commands) 67 | 68 | [Manual flashing ESP](#manual) 69 | 70 | [Contact](#contact) 71 | 72 | ## Hardware 73 | 74 | Current development is done using these cheap ESP32 LoRa modules. 75 | 76 | There are different variants for 433MHz and 868/915MHz: 77 | 78 | [Banggood: ESP32 Lora 868/915MHz (2 Pcs)](https://www.banggood.com/de/2Pcs-Wemos-TTGO-LORA32-868915Mhz-ESP32-LoRa-OLED-0_96-Inch-Blue-Display-p-1239769.html?rmmds=search&cur_warehouse=CN) 79 | 80 | [Banggood: ESP32 Lora 433MHz](https://www.banggood.com/de/Wemos-TTGO-LORA-SX1278-ESP32-0_96OLED-16-Mt-Bytes-128-Mt-bit-433Mhz-For-Arduino-p-1205930.html?rmmds=search&cur_warehouse=CN) 81 | 82 | Other variants (e.g. Heltec) or without OLED display and different antenna connectors should also work. 83 | 84 | Also please keep track of your countries regulations regarding radio transmissions. 85 | 86 | ## Development 87 | 88 | Everything here is WORK IN PROGRESS! 89 | 90 | The software is based on two components: 91 | - ESP32 LoRa part is found in this repo. 92 | It's developed using [PlatformIO](https://platformio.org/) plugin for [Atom](https://atom.io/) editor. 93 | - INAV OSD part repo is found [here](https://github.com/OlivierC-FR/inav/tree/oc_hud). 94 | It's a fork from the INAV repo and instructions how to build can be found [here](https://github.com/iNavFlight/inav/blob/master/docs/development/Building%20in%20Docker.md). 95 | 96 | INAV-Radar is a experimental firmware based on INAV and soon will become a part of the INAV flight control software. INAV repo can be found [here](https://github.com/iNavFlight/inav). 97 | 98 | ## ESP32 firmware flashing 99 | 100 | With the installer (Only Windows at the moment, for Linux / Mac os user see "Manual flashing" (bottom of page) 101 | 102 | For testing there is no need to install Atom and PlatformIO, just use the [ESP32 firmware installer](https://github.com/KingKone/INAV-Radar_Installer/releases) for flashing. 103 | 104 | ## Wireing 105 | 106 | To connect the ESP32 to the FC: 107 | - wire up +5V and GND 108 | - TX from FC to ESP RX pin 17 109 | - RX from FC to ESP TX pin 23 110 | 111 | 112 | ## FC settings 113 | 114 | Backup your FC settings, flash the current [testing version of INAV](https://github.com/mistyk/inavradar-ESP32/releases). 115 | 116 | Dump your backup back into the cli. 117 | 118 | Activate MSP on the corresponding UART, the speed is 115200. 119 | Enable the crosshair. 120 | 121 | Please also flash the extra Vision OSD fonts for signal strenth and the homing crosshair. Vision 1 is small/light, Vision 4 is heavy/bold. 122 | 123 | The HUD has an entry in the stick menu (OSD->HUD) where you can change this configuration at runtime. 124 | 125 | Optional OSD and HUD cli settings: 126 | ``` 127 | osd_layout 0 2 0 0 V 128 | osd_layout 0 43 0 0 H 129 | osd_layout 0 44 0 0 H 130 | osd_layout 0 45 0 0 H 131 | set osd_crosshairs_style = TYPE6 132 | set osd_horizon_offset = 0 133 | set osd_camera_uptilt = 0 134 | set osd_camera_fov_h = 135 135 | set osd_camera_fov_v = 85 136 | set osd_hud_margin_h = 1 137 | set osd_hud_margin_v = 3 138 | set osd_hud_homing = ON 139 | set osd_hud_homepoint = ON 140 | set osd_hud_radar_disp = 4 141 | set osd_hud_radar_range_min = 1 142 | set osd_hud_radar_range_max = 4000 143 | ``` 144 | 145 | ## Contact 146 | 147 | [Facebook Group](https://www.facebook.com/groups/360607501179901/) 148 | 149 | [INAV-Radar on RCgroups](https://www.rcgroups.com/forums/showthread.php?3304673-iNav-Radar-ESP32-LoRa-modems) 150 | 151 | [Patreon](https://www.patreon.com/inavradar) 152 | 153 | ## Commands 154 | 155 | !!! COMMANDS ARE DISABLED IN CURRENT VERSION !!! 156 | 157 | ``` 158 | ================= Commands ================= 159 | status - Show whats going on 160 | help - List all commands 161 | config - List all settings 162 | config loraFreq n - Set frequency in Hz (e.g. n = 433000000) 163 | config loraBandwidth n - Set bandwidth in Hz (e.g. n = 250000) 164 | config loraSpread n - Set SF (e.g. n = 7) 165 | config uavtimeout n - Set UAV timeout in sec (e.g. n = 10) 166 | config fctimeout n - Set FC timeout in sec (e.g. n = 5) 167 | config debuglat n - Set debug GPS lat * 10000000 (e.g. n = 501004900) 168 | config debuglon n - Set debug GPS lon * 10000000 (e.g. n = 87632280) 169 | reboot - Reset MCU and radio 170 | gpspos - Show last GPS position 171 | debug - Toggle debug output 172 | localfakeplanes - Send fake plane to FC 173 | lfp - Send fake plane to FC 174 | radiofakeplanes - Send fake plane via radio 175 | rfp - Send fake plane via radio 176 | movefakeplanes - Move fake plane 177 | mfp - Move fake plane 178 | ``` 179 | 180 | 181 | ## Manual Flashing ESP method: 182 | 183 | Your system may needs the driver for the USB UART bridge: 184 | [Windows+MacOS](https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers) 185 | or [Alternative MacOS](https://github.com/adrianmihalko/ch340g-ch34g-ch34x-mac-os-x-driver) 186 | 187 | You will need [Python 3.4 or newer](https://www.python.org/downloads/) installed on your system. 188 | 189 | Be sure to check 'Add Python to PATH': 190 | 191 | The latest stable esptool.py release can be installed via pip in your command prompt: 192 | 193 | Windows: 194 | ``` 195 | c:\> pip install esptool 196 | ``` 197 | 198 | MacOS: 199 | ``` 200 | $ pip3 install esptool 201 | ``` 202 | 203 | Download the air-to-air test firmware from the [releases page](https://github.com/mistyk/inavradar-ESP32/releases) 204 | and extract it. Run this command to flash it onto your ESP32 Lora module (Windows and MacOS): 205 | 206 | You may change the --port to match your operating system. If you are using Windows check the [device manager](https://github.com/mistyk/inavradar-ESP32/raw/master/docs/devManager.PNG). 207 | 208 | Windows: 209 | ``` 210 | c:\> cd (your air-to-air directory here) 211 | c:\> esptool.py --port COM11 write_flash -z --flash_mode dio 0x1000 bootloader_dio_40m.bin 0x8000 default.bin 0xe000 boot_app0.bin 0x10000 firmware.bin 212 | ``` 213 | 214 | MacOS: 215 | ``` 216 | $ cd (your air-to-air directory here) 217 | $ esptool.py --port /dev/tty.SLAB_USBtoUART write_flash -z --flash_mode dio 0x1000 bootloader_dio_40m.bin 0x8000 default.bin 0xe000 boot_app0.bin 0x10000 firmware.bin 218 | 219 | ``` 220 | 221 | The output should look something like this: 222 | ![Windows CMD output](https://github.com/mistyk/inavradar-ESP32/raw/master/docs/cmd.PNG) 223 | -------------------------------------------------------------------------------- /config.inav: -------------------------------------------------------------------------------- 1 | # mixer 2 | mmix 0 1.000 -1.000 1.000 -1.000 3 | mmix 1 1.000 -1.000 -1.000 1.000 4 | mmix 2 1.000 1.000 1.000 1.000 5 | mmix 3 1.000 1.000 -1.000 -1.000 6 | 7 | # servo mix 8 | 9 | # servo 10 | 11 | # feature 12 | feature MOTOR_STOP 13 | feature GPS 14 | feature PWM_OUTPUT_ENABLE 15 | 16 | # beeper 17 | 18 | # map 19 | 20 | # serial 21 | serial 0 576 115200 38400 115200 115200 22 | serial 1 0 115200 38400 115200 115200 23 | serial 3 2 115200 38400 0 115200 24 | serial 4 1 115200 38400 0 115200 25 | 26 | # led 27 | 28 | # color 29 | 30 | # mode_color 31 | 32 | # aux 33 | aux 0 0 0 1975 2025 34 | 35 | # adjrange 36 | 37 | # rxrange 38 | 39 | # osd_layout 40 | osd_layout 0 0 23 0 H 41 | osd_layout 0 1 25 11 V 42 | osd_layout 0 3 8 6 H 43 | osd_layout 0 4 8 6 H 44 | osd_layout 0 7 12 12 H 45 | osd_layout 0 9 1 2 H 46 | osd_layout 0 11 1 3 H 47 | osd_layout 0 12 1 4 H 48 | osd_layout 0 14 25 10 V 49 | osd_layout 0 15 25 9 V 50 | osd_layout 0 20 15 0 V 51 | osd_layout 0 21 4 0 V 52 | osd_layout 0 28 23 11 H 53 | osd_layout 0 30 1 1 V 54 | osd_layout 0 45 0 0 V 55 | 56 | # master 57 | set acc_hardware = MPU6500 58 | set acczero_x = -13 59 | set acczero_y = -10 60 | set acczero_z = -32 61 | set accgain_x = 4085 62 | set accgain_y = 4085 63 | set accgain_z = 4104 64 | set mag_hardware = NONE 65 | set magzero_x = 93 66 | set magzero_y = -88 67 | set magzero_z = -19 68 | set baro_hardware = BMP280 69 | set pitot_hardware = NONE 70 | set serialrx_provider = IBUS 71 | set min_throttle = 1065 72 | set motor_pwm_rate = 2000 73 | set motor_pwm_protocol = MULTISHOT 74 | set model_preview_type = 3 75 | set gps_sbas_mode = EGNOS 76 | set name = Daniel 77 | 78 | # profile 79 | profile 1 80 | 81 | 82 | # battery_profile 83 | battery_profile 1 84 | save 85 | -------------------------------------------------------------------------------- /docs/cmd.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/docs/cmd.PNG -------------------------------------------------------------------------------- /docs/devManager.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/docs/devManager.PNG -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/docs/logo.png -------------------------------------------------------------------------------- /docs/osd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/docs/osd.jpg -------------------------------------------------------------------------------- /docs/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/docs/python.png -------------------------------------------------------------------------------- /docs/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/docs/video.png -------------------------------------------------------------------------------- /flash-spiffs.sh: -------------------------------------------------------------------------------- 1 | tools/mkspiffs -c spiffs/ -b 4096 -p 256 -s 0x16F000 testing/fs.bin 2 | esptool.py --port /dev/tty.SLAB_USBtoUART write_flash -z 0x291000 testing/fs.bin 3 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32dev] 12 | platform = espressif32 13 | board = esp32dev 14 | framework = arduino 15 | -------------------------------------------------------------------------------- /spiffs/ace.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/ace.js.gz -------------------------------------------------------------------------------- /spiffs/app.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- Shortcuts 2 | var $ = function(id) { return document.getElementById(id); }; 3 | var C = function(tag) { return document.createElement(tag); }; 4 | // ----------------------------------------------------------------------------- Settings 5 | var currSet; 6 | var currCat; 7 | var currOpt; 8 | var rStatus = { 9 | "FC":"", 10 | "Name":"", 11 | "Arm state":"", 12 | "GPS":"", 13 | "Battery":"" 14 | } 15 | var rSettings = [{ 16 | "cat": "General", 17 | "sub": [{ 18 | "name": "UAV timeout", 19 | "cmd": "config uavtimeout ", 20 | "clitext": "UAV timeout", 21 | "value": "", 22 | "options": [ 23 | { "name": "10 seconds", "value": 10 }, 24 | { "name": "5 minutes", "value": 300 }, 25 | { "name": "1 hour", "value": 3600 }] 26 | }] 27 | },{ 28 | "cat": "Lora", 29 | "sub": [{ 30 | "name": "Frequency", 31 | "cmd": "config loraFreq ", 32 | "clitext": "Lora frequency", 33 | "value": "", 34 | "options": [ 35 | { "name": "433 MHz", "value": 433000000 }, 36 | { "name": "868 MHz", "value": 868000000 }, 37 | { "name": "915 MHz", "value": 915000000 }] 38 | }, { 39 | "name": "Bandwidth", 40 | "cmd": "config loraBandwidth ", 41 | "clitext": "Lora bandwidth", 42 | "value": "", 43 | "options": [ 44 | { "name": "250 kHz", "value": 250000 }, 45 | { "name": "62.8 kHz", "value": 62800 }, 46 | { "name": "7.8 kHz", "value": 7800 }] 47 | }, { 48 | "name": "Spreading factor", 49 | "cmd": "config loraSpread ", 50 | "clitext": "Lora spreading factor", 51 | "value": "", 52 | "options": [ 53 | { "name": "7", "value": 7 }, 54 | { "name": "8", "value": 8 }, 55 | { "name": "9", "value": 9 }, 56 | { "name": "10", "value": 10 }, 57 | { "name": "11", "value": 11 }, 58 | { "name": "12", "value": 12 }] 59 | }] 60 | },{ 61 | "cat": "Debugging", 62 | "sub": [{ 63 | "name": "Debug output", 64 | "cmd": "debug", 65 | "clitext": "Debug output", 66 | "value": "", 67 | "options": [{ "name": "onoff" }] 68 | }, { 69 | "name": "Local fake UAVs", 70 | "cmd": "localfakeplanes", 71 | "clitext": "Local fake planes", 72 | "value": "", 73 | "options": [{ "name": "onoff" }] 74 | },{ 75 | "name": "Radio fake UAVs", 76 | "cmd": "radiofakeplanes", 77 | "clitext": "Radio fake planes", 78 | "value": "", 79 | "options": [{ "name": "onoff" }] 80 | },{ 81 | "name": "Move fake UAVs", 82 | "cmd": "movefakeplanes", 83 | "clitext": "Move fake planes", 84 | "value": "", 85 | "options": [{ "name": "onoff" }] 86 | }] 87 | },{ 88 | "cat": "App", 89 | "sub": [{ 90 | "name": "About this app", 91 | "value": "Version 0.1", 92 | "options": [ 93 | { "name": "Developed by" }, 94 | { "name": "Daniel Heymann" }, 95 | { "name": "dh@iumt.de" }] 96 | },{ 97 | "name": "About INAV-Radar", 98 | "value": "Version 0.1", 99 | "options": [ 100 | { "name": "Developed by" }, 101 | { "name": "Camille Maria and Daniel Heymann" }, 102 | { "name": "dh@iumt.de" }] 103 | }] 104 | }]; 105 | // ----------------------------------------------------------------------------- Phonon UI 106 | phonon.options({ 107 | navigator: { 108 | defaultPage: 'home', 109 | animatePages: true 110 | }, 111 | i18n: null 112 | }); 113 | 114 | var navi = phonon.navigator(); 115 | navi.on({ 116 | page: 'home', 117 | preventClose: true, 118 | content: null 119 | }); 120 | 121 | navi.on({ 122 | page: 'suboptions', 123 | preventClose: false, 124 | content: null 125 | }, function(activity) { 126 | activity.onReady(function() { 127 | $("subtitle").textContent = currSet.name; 128 | var list = C('ul'); 129 | list.className = 'list'; 130 | currSet.options.forEach(function(option,i) { 131 | var opli = C("li"); 132 | var a = C("a"); 133 | a.className = "padded-list"; 134 | a.textContent = option.name; 135 | opli.on('click', function () { 136 | var o = option; 137 | var out = currSet.cmd + "\n"; 138 | if (rSettings[currCat].sub[currOpt].cmd.slice(-1) == ' ') out = rSettings[currCat].sub[currOpt].cmd + o.value + "\n"; 139 | ws.send(out); 140 | rSettings[currCat].sub[currOpt].value = o.value; 141 | navi.changePage('radiosettings'); 142 | }) 143 | opli.appendChild(a); 144 | list.appendChild(opli); 145 | }) 146 | $('subcontent').innerHTML = '' 147 | $('subcontent').appendChild(list); 148 | }); 149 | }); 150 | 151 | navi.on({ 152 | page: 'radiosettings', 153 | preventClose: false, 154 | content: null 155 | }, function(activity) { 156 | activity.onReady(function() { 157 | $('slist').innerHTML = ''; 158 | rSettings.forEach(function (cat,ci) { 159 | var li = C("li"); 160 | li.className = "divider"; 161 | li.textContent = cat.cat; 162 | slist.appendChild(li); 163 | cat.sub.forEach(function (sub,i) { 164 | if (sub.options[0].name =='onoff') { 165 | var input = C("input"); 166 | input.type = "checkbox"; 167 | if (sub.value == 1) input.checked = true; 168 | else input.checked = false; 169 | var span = C("span"); 170 | span.className = "text"; 171 | span.textContent = sub.name; 172 | var span2 = C("span"); 173 | span2.style.marginRight = "16px"; 174 | var opli = C("li"); 175 | opli.on('click', function (ev) { 176 | var cset = sub; 177 | var ccat = ci; 178 | var copt = i; 179 | var inp = input; 180 | currSet = cset; 181 | currCat = ccat; 182 | currOpt = copt; 183 | var out = currSet.cmd + "\n"; 184 | ws.send(out); 185 | rSettings[currCat].sub[currOpt].value = !inp.checked; 186 | inp.checked = !inp.checked 187 | }); 188 | opli.className = "checkbox"; 189 | opli.style.paddingLeft = "16px"; 190 | opli.appendChild(input); 191 | opli.appendChild(span2); 192 | opli.appendChild(span); 193 | $('slist').appendChild(opli); 194 | } else { 195 | var span = C("span"); 196 | span.className = "pull-right"; 197 | span.style.width = "100px"; 198 | span.textContent = sub.value; 199 | var a = C("a"); 200 | a.className = "padded-list"; 201 | a.textContent = sub.name; 202 | a.on('click', function (ev) { 203 | var cset = sub; 204 | var ccat = ci; 205 | var copt = i; 206 | currSet = cset; 207 | currCat = ccat; 208 | currOpt = copt; 209 | navi.changePage('suboptions'); 210 | 211 | }); 212 | var opli = C("li"); 213 | opli.appendChild(span); 214 | opli.appendChild(a); 215 | $('slist').appendChild(opli); 216 | } 217 | }); 218 | }); 219 | }); 220 | activity.onClose(function(self) { 221 | 222 | }); 223 | }); 224 | var setStatus = function (items) { 225 | $('connectedto').textContent = 'Connected to ' + items[1] + '(' + items[0] + ')'; 226 | if (items[3] == 1) $('armed').textContent = 'Armed'; 227 | else $('armed').textContent = 'Disarmed'; 228 | $('gpsstatus').textContent = items[4] + ' Sats'; 229 | $('battery').textContent = items[2] + ' V'; 230 | } 231 | var setConfig = function (items) { 232 | rSettings.forEach(function (cat,ci) { 233 | cat.sub.forEach(function (sub,si) { 234 | cfgs = items.split('<'); 235 | cfgs.forEach(function (cfg,i) { 236 | keyval = cfg.split('>'); 237 | if (keyval[0] == rSettings[ci].sub[si].clitext) rSettings[ci].sub[si].value = keyval[1]; 238 | }) 239 | }) 240 | }) 241 | } 242 | // ----------------------------------------------------------------------------- ws com 243 | var ws = null; 244 | function sendBlob(str){ 245 | var buf = new Uint8Array(str.length); 246 | for (var i = 0; i < str.length; ++i) buf[i] = str.charCodeAt(i); 247 | ws.send(buf); 248 | } 249 | function addMessage(m){ 250 | console.log(m); 251 | } 252 | function startSocket(){ 253 | ws = new WebSocket('ws://'+document.location.host+'/ws',['arduino']); 254 | ws.binaryType = "arraybuffer"; 255 | ws.onopen = function(e){ 256 | addMessage("Connected"); 257 | }; 258 | ws.onclose = function(e){ 259 | addMessage("Disconnected"); 260 | }; 261 | ws.onerror = function(e){ 262 | console.log("ws error", e); 263 | addMessage("Error"); 264 | }; 265 | ws.onmessage = function(e){ 266 | addMessage(e.data); 267 | }; 268 | //ws.send(ge("input_el").value); 269 | 270 | } 271 | function startEvents(){ 272 | var es = new EventSource('/events'); 273 | es.onopen = function(e) { 274 | addMessage("Events Opened"); 275 | }; 276 | es.onerror = function(e) { 277 | if (e.target.readyState != EventSource.OPEN) { 278 | addMessage("Events Closed"); 279 | } 280 | }; 281 | es.onmessage = function(e) { 282 | addMessage("Event: " + e.data); 283 | msg = e.data.split(": "); 284 | if (msg[0] == "Status") setStatus(msg[1].split(", ")); 285 | if (msg[0] == "Config") setConfig(msg[1]); 286 | }; 287 | es.addEventListener('ota', function(e) { 288 | addMessage("Event[ota]: " + e.data); 289 | }, false); 290 | } 291 | 292 | 293 | // ----------------------------------------------------------------------------- starting point 294 | (function() { 295 | navi.start() 296 | startSocket(); 297 | startEvents(); 298 | 299 | })(); 300 | -------------------------------------------------------------------------------- /spiffs/ext-searchbox.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/ext-searchbox.js.gz -------------------------------------------------------------------------------- /spiffs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/favicon.ico -------------------------------------------------------------------------------- /spiffs/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | App 10 | 11 | 12 | 13 |
14 |
15 | 16 |

Settings

17 |
18 |
19 |
20 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 |

30 |
31 |
32 |
33 |
34 |
35 | 36 | 37 |
38 | 39 |
40 |

INAV-Radar

41 |
42 | 43 |
44 |
45 |
46 |

Connected to

47 | DISARMED
48 | 0.00 V
49 | GPS
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
UAV NameStatePosition
63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /spiffs/index.htm.old: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | WebSocketTester 23 | 52 | 124 | 125 | 126 |

127 |     
128 | $ 129 |
130 | 131 | 132 | -------------------------------------------------------------------------------- /spiffs/main.css: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none; 3 | } 4 | .padded { 5 | padding: 0 16px; 6 | } 7 | .checkmargin { 8 | margin-right: 16px; 9 | } 10 | .logo { 11 | width: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /spiffs/material-design-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/material-design-icons.eot -------------------------------------------------------------------------------- /spiffs/material-design-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /spiffs/material-design-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/material-design-icons.ttf -------------------------------------------------------------------------------- /spiffs/material-design-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/material-design-icons.woff -------------------------------------------------------------------------------- /spiffs/mode-css.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/mode-css.js.gz -------------------------------------------------------------------------------- /spiffs/mode-html.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/mode-html.js.gz -------------------------------------------------------------------------------- /spiffs/mode-javascript.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/mode-javascript.js.gz -------------------------------------------------------------------------------- /spiffs/phonon.css: -------------------------------------------------------------------------------- 1 | /* normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | html { 3 | font-family: sans-serif; /* 1 */ 4 | -ms-text-size-adjust: 100%; /* 2 */ 5 | -webkit-text-size-adjust: 100%; /* 2 */ 6 | } 7 | article, 8 | aside, 9 | details, 10 | figcaption, 11 | figure, 12 | footer, 13 | header, 14 | hgroup, 15 | main, 16 | menu, 17 | nav, 18 | section, 19 | summary { 20 | display: block; 21 | } 22 | audio, 23 | canvas, 24 | progress, 25 | video { 26 | display: inline-block; /* 1 */ 27 | vertical-align: baseline; /* 2 */ 28 | } 29 | audio:not([controls]) { 30 | display: none; 31 | height: 0; 32 | } 33 | [hidden], 34 | template { 35 | display: none; 36 | } 37 | a { 38 | background-color: transparent; 39 | } 40 | a:active, 41 | a:hover { 42 | outline: 0; 43 | } 44 | b, 45 | strong { 46 | font-weight: bold; 47 | } 48 | h1 { 49 | font-size: 2em; 50 | margin: 0.67em 0; 51 | } 52 | small { 53 | font-size: 80%; 54 | } 55 | sub, 56 | sup { 57 | font-size: 75%; 58 | line-height: 0; 59 | position: relative; 60 | vertical-align: baseline; 61 | } 62 | sup { 63 | top: -0.5em; 64 | } 65 | sub { 66 | bottom: -0.25em; 67 | } 68 | img { 69 | border: 0; 70 | } 71 | svg:not(:root) { 72 | overflow: hidden; 73 | } 74 | figure { 75 | margin: 1em 40px; 76 | } 77 | hr { 78 | -webkit-box-sizing: content-box; 79 | box-sizing: content-box; 80 | height: 0; 81 | } 82 | pre { 83 | overflow: auto; 84 | } 85 | button, 86 | input, 87 | optgroup, 88 | select, 89 | textarea { 90 | color: inherit; /* 1 */ 91 | font: inherit; /* 2 */ 92 | margin: 0; /* 3 */ 93 | } 94 | button { 95 | overflow: visible; 96 | } 97 | button, 98 | select { 99 | text-transform: none; 100 | } 101 | button, 102 | html input[type="button"], 103 | input[type="reset"], 104 | input[type="submit"] { 105 | -webkit-appearance: button; /* 2 */ 106 | cursor: pointer; /* 3 */ 107 | } 108 | button[disabled], 109 | html input[disabled] { 110 | cursor: default; 111 | } 112 | button::-moz-focus-inner, 113 | input::-moz-focus-inner { 114 | border: 0; 115 | padding: 0; 116 | } 117 | input { 118 | line-height: normal; 119 | } 120 | input[type="checkbox"], 121 | input[type="radio"] { 122 | -webkit-box-sizing: border-box; 123 | box-sizing: border-box; /* 1 */ 124 | padding: 0; /* 2 */ 125 | } 126 | input[type="number"]::-webkit-inner-spin-button, 127 | input[type="number"]::-webkit-outer-spin-button { 128 | height: auto; 129 | } 130 | input[type="search"] { 131 | -webkit-appearance: textfield; /* 1 */ 132 | -webkit-box-sizing: content-box; 133 | box-sizing: content-box; /* 2 */ 134 | } 135 | input[type="search"]::-webkit-search-cancel-button, 136 | input[type="search"]::-webkit-search-decoration { 137 | -webkit-appearance: none; 138 | } 139 | fieldset { 140 | border: 1px solid #c0c0c0; 141 | margin: 0 2px; 142 | padding: 0.35em 0.625em 0.75em; 143 | } 144 | legend { 145 | border: 0; /* 1 */ 146 | padding: 0; /* 2 */ 147 | } 148 | textarea { 149 | overflow: auto; 150 | } 151 | optgroup { 152 | font-weight: bold; 153 | } 154 | table { 155 | border-collapse: collapse; 156 | border-spacing: 0; 157 | } 158 | td, 159 | th { 160 | padding: 0; 161 | } 162 | *, 163 | *:before, 164 | *:after { 165 | -webkit-box-sizing: border-box; 166 | box-sizing: border-box; 167 | } 168 | html, 169 | body { 170 | position: relative; 171 | height: 100%; 172 | width: 100%; 173 | overflow-x: hidden; 174 | } 175 | html { 176 | -ms-touch-action: pan-y; 177 | touch-action: pan-y; 178 | } 179 | body { 180 | top: 0; 181 | right: 0; 182 | bottom: 0; 183 | left: 0; 184 | margin: 0; 185 | padding: 0; 186 | word-wrap: break-word; 187 | font-size: 14px; 188 | font-family: "Helvetica Neue", "Roboto", "Segoe UI", sans-serif; 189 | text-rendering: optimizeLegibility; 190 | -webkit-font-smoothing: antialiased; 191 | -webkit-backface-visibility: hidden; 192 | backface-visibility: hidden; 193 | -webkit-user-drag: none; 194 | -ms-content-zooming: none; 195 | background-color: #fff; 196 | -webkit-user-select: none; 197 | -moz-user-select: none; 198 | -ms-user-select: none; 199 | user-select: none; 200 | -webkit-touch-callout: none; 201 | -webkit-tap-highlight-color: transparent; 202 | } 203 | body, 204 | input, 205 | textarea, 206 | button, 207 | select, 208 | label, 209 | p { 210 | font-family: "Helvetica Neue", "Roboto", "Segoe UI", sans-serif; 211 | font-size: 14px; 212 | font-weight: normal; 213 | color: #333; 214 | } 215 | h1, 216 | h2, 217 | h3, 218 | h4, 219 | h5, 220 | h6 { 221 | margin-top: 0; 222 | margin-bottom: 10px; 223 | line-height: 1; 224 | font-weight: normal; 225 | } 226 | h1 { 227 | font-size: 36px; 228 | } 229 | h2 { 230 | font-size: 30px; 231 | } 232 | h3 { 233 | font-size: 24px; 234 | } 235 | h4 { 236 | font-size: 18px; 237 | } 238 | h5 { 239 | margin-top: 20px; 240 | font-size: 14px; 241 | } 242 | h6 { 243 | margin-top: 20px; 244 | font-size: 12px; 245 | } 246 | p { 247 | margin-top: 0; 248 | margin-bottom: 10px; 249 | } 250 | .content { 251 | position: absolute; 252 | top: 0; 253 | right: 0; 254 | bottom: 0; 255 | left: 0; 256 | overflow: auto; 257 | -webkit-overflow-scrolling: touch; 258 | } 259 | input, 260 | textarea, 261 | select { 262 | -webkit-touch-callout: default; 263 | -moz-user-select: all; 264 | -ms-user-select: all; 265 | user-select: all; 266 | -webkit-user-select: auto; 267 | } 268 | a { 269 | color: #0084e7; 270 | text-decoration: none; 271 | } 272 | button { 273 | border: none; 274 | outline: none; 275 | } 276 | .app-page { 277 | display: block; 278 | visibility: hidden; 279 | position: absolute; 280 | width: 100%; 281 | height: 100%; 282 | -webkit-backface-visibility: hidden; 283 | backface-visibility: hidden; 284 | -webkit-transform: translate3d(0, 0, 0); 285 | transform: translate3d(0, 0, 0); 286 | overflow: hidden; 287 | z-index: 1; 288 | background-color: #fff; 289 | /* 290 | * 291 | * Page transition animation 292 | * 293 | */ 294 | } 295 | .app-page.app-active { 296 | visibility: visible; 297 | z-index: 2; 298 | } 299 | .app-page.page-sliding.left { 300 | z-index: 3; 301 | -webkit-animation: leftTransition 300ms ease-out; 302 | animation: leftTransition 300ms ease-out; 303 | } 304 | .app-page.page-sliding.right { 305 | z-index: 4; 306 | -webkit-animation: rightTransition 300ms ease-out; 307 | animation: rightTransition 300ms ease-out; 308 | } 309 | @-webkit-keyframes leftTransition { 310 | 0% { 311 | opacity: 1; 312 | -webkit-transform: translate3d(0, 0, 0); 313 | transform: translate3d(0, 0, 0); 314 | } 315 | 100% { 316 | opacity: 0; 317 | -webkit-transform: translate3d(-50%, 0, 0); 318 | transform: translate3d(-50%, 0, 0); 319 | } 320 | } 321 | @keyframes leftTransition { 322 | 0% { 323 | opacity: 1; 324 | -webkit-transform: translate3d(0, 0, 0); 325 | transform: translate3d(0, 0, 0); 326 | } 327 | 100% { 328 | opacity: 0; 329 | -webkit-transform: translate3d(-50%, 0, 0); 330 | transform: translate3d(-50%, 0, 0); 331 | } 332 | } 333 | @-webkit-keyframes rightTransition { 334 | 0% { 335 | opacity: 1; 336 | -webkit-transform: translate3d(0, 0, 0); 337 | transform: translate3d(0, 0, 0); 338 | } 339 | 100% { 340 | opacity: 0; 341 | -webkit-transform: translate3d(50%, 0, 0); 342 | transform: translate3d(50%, 0, 0); 343 | } 344 | } 345 | @keyframes rightTransition { 346 | 0% { 347 | opacity: 1; 348 | -webkit-transform: translate3d(0, 0, 0); 349 | transform: translate3d(0, 0, 0); 350 | } 351 | 100% { 352 | opacity: 0; 353 | -webkit-transform: translate3d(50%, 0, 0); 354 | transform: translate3d(50%, 0, 0); 355 | } 356 | } 357 | .btn.primary, 358 | .btn.positive, 359 | .btn.negative { 360 | color: #fff !important; 361 | } 362 | .primary { 363 | background-color: #0084e7 !important; 364 | } 365 | .primary.btn-flat { 366 | color: #0084e7 !important; 367 | background-color: transparent !important; 368 | } 369 | .positive { 370 | background-color: #0ae700 !important; 371 | } 372 | .positive.btn-flat { 373 | color: #0ae700 !important; 374 | background-color: transparent !important; 375 | } 376 | .negative { 377 | background-color: #f40b00 !important; 378 | } 379 | .negative.btn-flat { 380 | color: #f40b00 !important; 381 | background-color: transparent !important; 382 | } 383 | .light-primary { 384 | background-color: #b9d3e7 !important; 385 | } 386 | .light-positive { 387 | background-color: #bbe7b9 !important; 388 | } 389 | .light-negative { 390 | background-color: #f4c5c3 !important; 391 | } 392 | .dark-primary { 393 | background-color: #0067b5 !important; 394 | } 395 | .dark-positive { 396 | background-color: #08b500 !important; 397 | } 398 | .dark-negative { 399 | background-color: #c20900 !important; 400 | } 401 | .btn { 402 | position: relative; 403 | padding: 0 12px; 404 | display: inline-block; 405 | min-height: 42px; 406 | line-height: 1; 407 | color: #333; 408 | text-align: center; 409 | vertical-align: top; 410 | cursor: pointer; 411 | outline: none; 412 | text-transform: uppercase; 413 | border: none; 414 | border-radius: 3px; 415 | background-color: #f1f1f1; 416 | -webkit-transition: background-color 250ms ease-in-out; 417 | -o-transition: background-color 250ms ease-in-out; 418 | transition: background-color 250ms ease-in-out; 419 | } 420 | .btn .icon { 421 | line-height: 1; 422 | display: inline-block; 423 | font-size: inherit; 424 | color: inherit; 425 | } 426 | .btn:not(.btn-progress):disabled { 427 | opacity: 0.75; 428 | color: #d3d3d3; 429 | } 430 | .btn-flat { 431 | background-color: transparent !important; 432 | } 433 | .btn:active { 434 | opacity: 0.85; 435 | } 436 | .floating-action { 437 | position: fixed; 438 | padding: 16px; 439 | border-radius: 50%; 440 | -webkit-box-shadow: 0 2px 3px #8a8a8a; 441 | box-shadow: 0 2px 3px #8a8a8a; 442 | z-index: 30; 443 | color: #fff; 444 | opacity: 0.5; 445 | -webkit-transform: 300ms, opacity 0.25s linear; 446 | -ms-transform: 300ms, opacity 0.25s linear; 447 | transform: 300ms, opacity 0.25s linear; 448 | } 449 | .floating-action.active, 450 | .floating-action:active { 451 | opacity: 1; 452 | } 453 | .floating-action.top { 454 | top: 68px; 455 | } 456 | .floating-action.bottom { 457 | bottom: 16px; 458 | } 459 | .floating-action.left { 460 | left: 16px; 461 | } 462 | .floating-action.right { 463 | right: 16px; 464 | } 465 | .buttons { 466 | list-style: none; 467 | text-align: right; 468 | margin: 0; 469 | padding: 0; 470 | } 471 | .buttons li { 472 | display: inline-block; 473 | } 474 | .buttons li a { 475 | height: 48px; 476 | line-height: 48px; 477 | vertical-align: middle; 478 | font-weight: 700; 479 | min-width: 60px; 480 | padding: 0 8px; 481 | } 482 | .header-bar { 483 | position: fixed; 484 | right: 0; 485 | left: 0; 486 | z-index: 20; 487 | width: 100%; 488 | height: 52px; 489 | overflow: hidden; 490 | background-color: #0084e7; 491 | border-bottom: 0px solid #2e2bc9; 492 | -webkit-backface-visibility: hidden; 493 | backface-visibility: hidden; 494 | } 495 | .header-bar .center { 496 | position: absolute; 497 | text-align: center; 498 | top: 0; 499 | right: 0; 500 | left: 0; 501 | } 502 | .header-bar .pull-left { 503 | padding-left: 16px; 504 | } 505 | .header-bar .pull-right { 506 | padding-right: 16px; 507 | } 508 | .header-bar .title { 509 | z-index: 21; 510 | font-size: 20px; 511 | color: #fff; 512 | height: 52px; 513 | line-height: 52px; 514 | vertical-align: middle; 515 | } 516 | .header-bar .center .title { 517 | margin: 0 auto; 518 | } 519 | .header-bar .arrow { 520 | cursor: pointer; 521 | } 522 | .header-bar .arrow:after { 523 | position: relative; 524 | width: 0; 525 | height: 0; 526 | top: 16px; 527 | left: 6px; 528 | content: ''; 529 | border-right: 4px solid transparent; 530 | border-top: 5px solid #fff; 531 | border-left: 4px solid transparent; 532 | } 533 | .header-bar .btn { 534 | position: relative; 535 | z-index: 22; 536 | padding: 0 12px; 537 | height: 52px; 538 | line-height: 52px; 539 | vertical-align: middle; 540 | color: #fff; 541 | background-color: transparent; 542 | border: none; 543 | border-radius: 0; 544 | } 545 | .header-bar .btn:active { 546 | opacity: 1; 547 | background-color: #0084e7; 548 | background-color: rgba(0,0,0,0.1); 549 | } 550 | .header-bar .search-input { 551 | width: 75%; 552 | position: absolute; 553 | height: 52px; 554 | color: #fff; 555 | background-color: transparent; 556 | border: none; 557 | } 558 | .header-bar .search-input::-webkit-input-placeholder { 559 | color: #fff; 560 | } 561 | .header-bar .search-input:-moz-placeholder { 562 | color: #fff; 563 | } 564 | .header-bar .search-input::-moz-placeholder { 565 | color: #fff; 566 | } 567 | .header-bar .search-input:-ms-input-placeholder { 568 | color: #fff; 569 | } 570 | .header-bar ~ .content { 571 | margin-top: 52px; 572 | } 573 | .header-bar ~ .header-tabs { 574 | top: 52px; 575 | } 576 | .header-bar ~ .header-tabs ~ .content { 577 | margin-top: 102px; 578 | } 579 | .text-left { 580 | text-align: left; 581 | } 582 | .text-center { 583 | text-align: center; 584 | } 585 | .text-right { 586 | text-align: right; 587 | } 588 | .text-justify { 589 | text-align: justify; 590 | } 591 | .pull-left { 592 | float: left !important; 593 | } 594 | .pull-right { 595 | float: right !important; 596 | } 597 | .fit-parent { 598 | width: 100%; 599 | } 600 | .show-for-phone-only, 601 | .show-for-tablet-only, 602 | .show-for-tablet-up, 603 | .show-for-large-only { 604 | display: none !important; 605 | } 606 | .show-for-android-only, 607 | .show-for-ios-only, 608 | .show-for-web-only { 609 | display: none !important; 610 | } 611 | .android .show-for-android-only { 612 | display: inherit !important; 613 | } 614 | .ios .show-for-ios-only { 615 | display: inherit !important; 616 | } 617 | .web .show-for-web-only { 618 | display: inherit !important; 619 | } 620 | @media only screen and (max-width: 640px) { 621 | .show-for-phone-only { 622 | display: inherit !important; 623 | } 624 | .padded-full { 625 | margin: 16px; 626 | } 627 | .padded-top { 628 | margin-top: 16px; 629 | } 630 | .padded-left { 631 | margin-left: 16px; 632 | } 633 | .padded-right { 634 | margin-right: 16px; 635 | } 636 | .padded-bottom { 637 | margin-bottom: 16px; 638 | } 639 | } 640 | @media only screen and (min-width: 641px) and (max-width: 1024px) { 641 | .show-for-tablet-only { 642 | display: inherit !important; 643 | } 644 | } 645 | @media only screen and (min-width: 641px) { 646 | .show-for-tablet-only, 647 | .show-for-tablet-up { 648 | display: inherit !important; 649 | } 650 | .padded-full { 651 | margin: 32px; 652 | } 653 | .padded-top { 654 | margin-top: 32px; 655 | } 656 | .padded-left { 657 | margin-left: 32px; 658 | } 659 | .padded-right { 660 | margin-right: 32px; 661 | } 662 | .padded-bottom { 663 | margin-bottom: 32px; 664 | } 665 | .notification { 666 | width: 450px !important; 667 | } 668 | .dialog { 669 | width: 380px !important; 670 | } 671 | .side-panel { 672 | width: 28% !important; 673 | } 674 | .header-bar .pull-left { 675 | padding-left: 32px; 676 | } 677 | .header-bar .pull-right { 678 | padding-right: 32px; 679 | } 680 | .expose-aside-left { 681 | width: 72%; 682 | margin-left: 28%; 683 | } 684 | .expose-aside-left .panel-full { 685 | width: 72%; 686 | margin-left: 28%; 687 | } 688 | .expose-aside-left .notification { 689 | left: 28%; 690 | } 691 | .expose-aside-right { 692 | width: 72%; 693 | } 694 | .expose-aside-right .panel-full { 695 | width: 72%; 696 | margin-right: 28%; 697 | } 698 | } 699 | @media only screen and (min-width: 1025px) { 700 | .show-for-tablet-only { 701 | display: none !important; 702 | } 703 | .show-for-tablet-up, 704 | .show-for-large-only { 705 | display: inherit !important; 706 | } 707 | } 708 | .tabs { 709 | position: fixed; 710 | top: auto; 711 | right: 0; 712 | left: 0; 713 | bottom: 0; 714 | z-index: 10; 715 | display: block; 716 | width: 100%; 717 | height: 50px; 718 | background-color: #fff; 719 | -webkit-backface-visibility: hidden; 720 | backface-visibility: hidden; 721 | padding: 0; 722 | } 723 | .tabs .tab-items { 724 | display: -webkit-box; 725 | display: -webkit-flex; 726 | display: -ms-flexbox; 727 | display: flex; 728 | box-orient: horizontal; 729 | } 730 | .tabs .tab-items .tab-item { 731 | display: -webkit-box; 732 | display: -webkit-flex; 733 | display: -ms-flexbox; 734 | display: flex; 735 | -webkit-flex-basis: 100%; 736 | -ms-flex-preferred-size: 100%; 737 | flex-basis: 100%; 738 | -webkit-box-flex: 1; 739 | -webkit-flex-grow: 1; 740 | -ms-flex-positive: 1; 741 | flex-grow: 1; 742 | -webkit-box-align: center; 743 | -webkit-align-items: center; 744 | -ms-flex-align: center; 745 | align-items: center; 746 | -webkit-box-pack: center; 747 | -webkit-justify-content: center; 748 | -ms-flex-pack: center; 749 | justify-content: center; 750 | height: 50px; 751 | line-height: 50px; 752 | white-space: nowrap; 753 | -o-text-overflow: ellipsis; 754 | text-overflow: ellipsis; 755 | overflow: hidden; 756 | color: #0084e7; 757 | text-align: center; 758 | text-transform: uppercase; 759 | vertical-align: middle; 760 | padding: 0; 761 | margin: 0; 762 | } 763 | .tabs .tab-items .tab-item .icon { 764 | position: relative; 765 | top: 3px; 766 | width: 32px; 767 | height: 32px; 768 | vertical-align: middle; 769 | text-align: center; 770 | padding: 0; 771 | margin: 0; 772 | } 773 | .tabs .tab-items .icon-text { 774 | line-height: 75px; 775 | white-space: normal; 776 | display: inline-grid; 777 | } 778 | .tabs .tab-items .icon-text .icon { 779 | width: auto; 780 | height: 0; 781 | } 782 | .tabs .tab-indicator { 783 | width: 50%; 784 | position: absolute; 785 | z-index: 11; 786 | top: auto; 787 | left: 0; 788 | right: 0; 789 | margin-top: auto; 790 | margin-bottom: -3px; 791 | height: 3px; 792 | background-color: #0084e7; 793 | } 794 | .header-tabs { 795 | top: auto; 796 | bottom: 0; 797 | } 798 | .header-tabs .tab-indicator { 799 | margin-top: -3px; 800 | margin-bottom: auto; 801 | } 802 | @font-face { 803 | font-family: 'MaterialDesignIcons'; 804 | src: url("material-design-icons.eot?-613f9d"); 805 | src: url("material-design-icons.eot?#iefix-613f9d") format('embedded-opentype'), url("material-design-icons.woff?-613f9d") format('woff'), url("material-design-icons.ttf?-613f9d") format('truetype'), url("material-design-icons.svg?-613f9d#material-design-icons") format('svg'); 806 | font-weight: normal; 807 | font-style: normal; 808 | } 809 | .icon { 810 | display: inline-block; 811 | font-family: 'MaterialDesignIcons'; 812 | font-size: 24px; 813 | line-height: 1; 814 | text-decoration: none; 815 | text-rendering: auto; 816 | -webkit-font-smoothing: antialiased; 817 | -moz-osx-font-smoothing: grayscale; 818 | } 819 | .icon-home:before { 820 | content: "\e600"; 821 | } 822 | .icon-info-outline:before { 823 | content: "\e601"; 824 | } 825 | .icon-settings:before { 826 | content: "\e606"; 827 | } 828 | .icon-add:before { 829 | content: "\e602"; 830 | } 831 | .icon-edit:before { 832 | content: "\e603"; 833 | } 834 | .icon-arrow-back:before { 835 | content: "\e604"; 836 | } 837 | .icon-arrow-forward:before { 838 | content: "\e60a"; 839 | } 840 | .icon-check:before { 841 | content: "\e605"; 842 | } 843 | .icon-chevron-left:before { 844 | content: "\e609"; 845 | } 846 | .icon-chevron-right:before { 847 | content: "\e60f"; 848 | } 849 | .icon-close:before { 850 | content: "\e608"; 851 | } 852 | .icon-expand-less:before { 853 | content: "\e610"; 854 | } 855 | .icon-expand-more:before { 856 | content: "\e611"; 857 | } 858 | .icon-menu:before { 859 | content: "\e60b"; 860 | } 861 | .icon-more-horiz:before { 862 | content: "\e60c"; 863 | } 864 | .icon-more-vert:before { 865 | content: "\e60d"; 866 | } 867 | .icon-sync:before { 868 | content: "\e60e"; 869 | } 870 | .icon-sync-problem:before { 871 | content: "\e607"; 872 | } 873 | .icon-star:before { 874 | content: "\e612"; 875 | } 876 | .icon-star-outline:before { 877 | content: "\e613"; 878 | } 879 | i.icon { 880 | font-style: normal; 881 | } 882 | .with-circle { 883 | padding: 10px; 884 | border-radius: 50%; 885 | border: 1px solid #eee; 886 | } 887 | .row { 888 | width: 100%; 889 | } 890 | .row:before, 891 | .row:after { 892 | content: ' '; 893 | display: table; 894 | } 895 | .row:after { 896 | clear: both; 897 | } 898 | .row .row { 899 | width: auto; 900 | } 901 | .row .row:before, 902 | .row .row:after { 903 | content: ' '; 904 | display: table; 905 | } 906 | .row .row:after { 907 | clear: both; 908 | } 909 | .column { 910 | width: 100%; 911 | position: relative; 912 | float: left; 913 | } 914 | [class*="column"] + [class*="column"]:last-child { 915 | float: right; 916 | } 917 | @media only screen and (max-width: 640px) { 918 | .phone-1 { 919 | width: 8.333333333333332%; 920 | } 921 | .phone-2 { 922 | width: 16.666666666666664%; 923 | } 924 | .phone-3 { 925 | width: 25%; 926 | } 927 | .phone-4 { 928 | width: 33.33333333333333%; 929 | } 930 | .phone-5 { 931 | width: 41.66666666666667%; 932 | } 933 | .phone-6 { 934 | width: 50%; 935 | } 936 | .phone-7 { 937 | width: 58.333333333333336%; 938 | } 939 | .phone-8 { 940 | width: 66.66666666666666%; 941 | } 942 | .phone-9 { 943 | width: 75%; 944 | } 945 | .phone-10 { 946 | width: 83.33333333333334%; 947 | } 948 | .phone-11 { 949 | width: 91.66666666666666%; 950 | } 951 | .phone-12 { 952 | width: 100%; 953 | } 954 | } 955 | @media only screen and (min-width: 641px) { 956 | .tablet-1 { 957 | width: 8.333333333333332%; 958 | } 959 | .tablet-2 { 960 | width: 16.666666666666664%; 961 | } 962 | .tablet-3 { 963 | width: 25%; 964 | } 965 | .tablet-4 { 966 | width: 33.33333333333333%; 967 | } 968 | .tablet-5 { 969 | width: 41.66666666666667%; 970 | } 971 | .tablet-6 { 972 | width: 50%; 973 | } 974 | .tablet-7 { 975 | width: 58.333333333333336%; 976 | } 977 | .tablet-8 { 978 | width: 66.66666666666666%; 979 | } 980 | .tablet-9 { 981 | width: 75%; 982 | } 983 | .tablet-10 { 984 | width: 83.33333333333334%; 985 | } 986 | .tablet-11 { 987 | width: 91.66666666666666%; 988 | } 989 | .tablet-12 { 990 | width: 100%; 991 | } 992 | } 993 | @media only screen and (min-width: 1025px) { 994 | .large-1 { 995 | width: 8.333333333333332%; 996 | } 997 | .large-2 { 998 | width: 16.666666666666664%; 999 | } 1000 | .large-3 { 1001 | width: 25%; 1002 | } 1003 | .large-4 { 1004 | width: 33.33333333333333%; 1005 | } 1006 | .large-5 { 1007 | width: 41.66666666666667%; 1008 | } 1009 | .large-6 { 1010 | width: 50%; 1011 | } 1012 | .large-7 { 1013 | width: 58.333333333333336%; 1014 | } 1015 | .large-8 { 1016 | width: 66.66666666666666%; 1017 | } 1018 | .large-9 { 1019 | width: 75%; 1020 | } 1021 | .large-10 { 1022 | width: 83.33333333333334%; 1023 | } 1024 | .large-11 { 1025 | width: 91.66666666666666%; 1026 | } 1027 | .large-12 { 1028 | width: 100%; 1029 | } 1030 | } 1031 | .list { 1032 | position: relative; 1033 | padding: 0; 1034 | margin: 0; 1035 | list-style: none; 1036 | } 1037 | .list li { 1038 | height: auto; 1039 | min-height: 51px; 1040 | line-height: 52px; 1041 | overflow: hidden /* for accordion animation */; 1042 | display: block; 1043 | border-bottom: 1px solid #eee; 1044 | } 1045 | .list li a { 1046 | width: 100%; 1047 | height: 100%; 1048 | display: block; 1049 | color: #000; 1050 | -webkit-transition: background-color 250ms ease-in-out; 1051 | -o-transition: background-color 250ms ease-in-out; 1052 | transition: background-color 250ms ease-in-out; 1053 | } 1054 | .list li a:active { 1055 | background-color: #eee; 1056 | } 1057 | .list li .pull-left, 1058 | .list li .pull-right { 1059 | width: 52px; 1060 | height: 52px; 1061 | line-height: 52px; 1062 | vertical-align: middle; 1063 | text-align: center; 1064 | z-index: 1; 1065 | } 1066 | .list li .item-content { 1067 | display: inline-block; 1068 | padding: 8px 15px; 1069 | line-height: 78px; 1070 | } 1071 | .list li .accordion-content { 1072 | position: absolute; 1073 | height: auto; 1074 | display: none; 1075 | -webkit-transition: max-height 300ms ease-in-out; 1076 | -o-transition: max-height 300ms ease-in-out; 1077 | transition: max-height 300ms ease-in-out; 1078 | padding: 0 16px; 1079 | background-color: #f9f9f9; 1080 | } 1081 | .list li .accordion-active { 1082 | position: static; 1083 | display: block; 1084 | -webkit-transition: max-height 300ms ease-in-out; 1085 | -o-transition: max-height 300ms ease-in-out; 1086 | transition: max-height 300ms ease-in-out; 1087 | } 1088 | .list li .title, 1089 | .list li .body { 1090 | line-height: 31px; 1091 | display: block; 1092 | } 1093 | .list li .body { 1094 | font-size: 13px; 1095 | color: #777; 1096 | } 1097 | .list .item-expanded { 1098 | height: 78px; 1099 | line-height: 78px; 1100 | } 1101 | .list .item-expanded .pull-left, 1102 | .list .item-expanded .pull-right { 1103 | width: 78px; 1104 | height: 78px; 1105 | line-height: 78px; 1106 | } 1107 | .list .divider { 1108 | height: 48px; 1109 | line-height: 48px; 1110 | font-size: 16px; 1111 | background-color: #f5f5f5; 1112 | } 1113 | .list .divider, 1114 | .list .padded-list { 1115 | padding: 0 16px; 1116 | } 1117 | input[type="submit"], 1118 | input[type="reset"], 1119 | input[type="button"] { 1120 | width: 100%; 1121 | } 1122 | select, 1123 | textarea, 1124 | input[type="text"], 1125 | input[type="search"], 1126 | input[type="password"], 1127 | input[type="datetime"], 1128 | input[type="datetime-local"], 1129 | input[type="date"], 1130 | input[type="month"], 1131 | input[type="time"], 1132 | input[type="week"], 1133 | input[type="number"], 1134 | input[type="email"], 1135 | input[type="url"], 1136 | input[type="tel"], 1137 | input[type="color"] { 1138 | width: 100%; 1139 | height: 48px; 1140 | line-height: 16px; 1141 | -webkit-appearance: none; 1142 | -moz-appearance: none; 1143 | appearance: none; 1144 | color: #636363; 1145 | background-color: #fff; 1146 | border: none; 1147 | border-bottom: 1px solid #ddd; 1148 | outline: none; 1149 | } 1150 | input:invalid, 1151 | .input-invalid { 1152 | border-bottom-width: 2px !important; 1153 | border-color: #e53935 !important; 1154 | } 1155 | input::-webkit-input-placeholder { 1156 | padding: 0; 1157 | font-size: 14px; 1158 | color: #999; 1159 | } 1160 | select:focus, 1161 | textarea:focus, 1162 | input[type="text"]:focus, 1163 | input[type="search"]:focus, 1164 | input[type="password"]:focus, 1165 | input[type="datetime"]:focus, 1166 | input[type="datetime-local"]:focus, 1167 | input[type="date"]:focus, 1168 | input[type="month"]:focus, 1169 | input[type="time"]:focus, 1170 | input[type="week"]:focus, 1171 | input[type="number"]:focus, 1172 | input[type="email"]:focus, 1173 | input[type="url"]:focus, 1174 | input[type="tel"]:focus, 1175 | input[type="color"]:focus { 1176 | border-bottom-width: 2px; 1177 | border-color: #0084e7; 1178 | } 1179 | label { 1180 | width: 100%; 1181 | display: inline-block; 1182 | height: 48px; 1183 | line-height: 48px; 1184 | } 1185 | .list label { 1186 | height: 52px; 1187 | line-height: 52px; 1188 | } 1189 | .checkbox input[type="checkbox"], 1190 | .radio input[type="radio"] { 1191 | display: none; 1192 | } 1193 | .checkbox, 1194 | .radio { 1195 | position: relative; 1196 | } 1197 | .checkbox .text, 1198 | .radio .text { 1199 | border: none; 1200 | } 1201 | .checkbox span:not(.text), 1202 | .radio span:not(.text) { 1203 | -webkit-transition: all 0.3s ease-in-out; 1204 | -o-transition: all 0.3s ease-in-out; 1205 | transition: all 0.3s ease-in-out; 1206 | content: ""; 1207 | position: absolute; 1208 | float: right; 1209 | top: 16px; 1210 | right: 0; 1211 | width: 20px; 1212 | height: 20px; 1213 | border: 2px solid #ddd; 1214 | } 1215 | .checkbox :checked + span:not(.text), 1216 | .radio :checked + span:not(.text) { 1217 | -webkit-transform: rotate(-45deg); 1218 | -ms-transform: rotate(-45deg); 1219 | transform: rotate(-45deg); 1220 | border-radius: 0; 1221 | height: 0.65rem; 1222 | border-color: #0084e7; 1223 | border-top-style: none; 1224 | border-right-style: none; 1225 | } 1226 | .radio span { 1227 | border-radius: 50%; 1228 | } 1229 | .input-wrapper { 1230 | width: 100%; 1231 | margin-top: 24px; 1232 | position: relative; 1233 | display: inline-block; 1234 | vertical-align: top; 1235 | } 1236 | .floating-label { 1237 | position: absolute; 1238 | top: 0; 1239 | left: 0; 1240 | width: 100%; 1241 | text-align: left; 1242 | line-height: 48px; 1243 | height: auto; 1244 | display: inline-block; 1245 | font-size: 14px; 1246 | color: #999; 1247 | -webkit-font-smoothing: antialiased; 1248 | -moz-osx-font-smoothing: grayscale; 1249 | -webkit-transform: translate3d(0, 0, 0); 1250 | transform: translate3d(0, 0, 0); 1251 | -webkit-transition: all 200ms; 1252 | -o-transition: all 200ms; 1253 | transition: all 200ms; 1254 | } 1255 | .with-label:focus + .floating-label, 1256 | .input-filled .floating-label { 1257 | -webkit-transform: translate3d(0, -60%, 0); 1258 | transform: translate3d(0, -60%, 0); 1259 | font-size: 12px; 1260 | } 1261 | table, 1262 | .table { 1263 | width: auto; 1264 | border: 1px solid #ddd; 1265 | } 1266 | table thead tr th, 1267 | .table thead tr th { 1268 | font-size: 15px; 1269 | } 1270 | table tbody tr td, 1271 | .table tbody tr td { 1272 | font-size: 13.5px; 1273 | } 1274 | table thead tr th, 1275 | .table thead tr th, 1276 | table tbody tr td, 1277 | .table tbody tr td, 1278 | table tfoot tr th, 1279 | .table tfoot tr th { 1280 | padding: 8px; 1281 | line-height: 1.5; 1282 | text-align: center; 1283 | border-top: 1px solid #ddd; 1284 | font-weight: normal; 1285 | } 1286 | table thead tr th, 1287 | .table thead tr th { 1288 | vertical-align: bottom; 1289 | border-bottom: 1px solid #ddd; 1290 | background: #f6f6f6; 1291 | } 1292 | table, 1293 | .table > caption + thead > tr:first-child > th, 1294 | .table > colgroup + thead > tr:first-child > th, 1295 | .table > thead:first-child > tr:first-child > th, 1296 | .table > caption + thead > tr:first-child > td, 1297 | .table > colgroup + thead > tr:first-child > td, 1298 | .table > thead:first-child > tr:first-child > td { 1299 | border-top: 0; 1300 | } 1301 | table, 1302 | .table > tbody + tbody { 1303 | border-top: 2px solid #ddd; 1304 | } 1305 | .backdrop-dialog { 1306 | position: fixed; 1307 | top: 0; 1308 | left: 0; 1309 | right: 0; 1310 | bottom: 0; 1311 | z-index: 26; 1312 | background: transparent; 1313 | background-color: rgba(0,0,0,0.35); 1314 | } 1315 | .backdrop-dialog.fadeout { 1316 | opacity: 0; 1317 | -webkit-transition: opacity 0.45s linear; 1318 | -o-transition: opacity 0.45s linear; 1319 | transition: opacity 0.45s linear; 1320 | } 1321 | .dialog { 1322 | width: 280px; 1323 | height: auto; 1324 | position: absolute; 1325 | opacity: 0; 1326 | visibility: hidden; 1327 | top: 0; 1328 | left: 0; 1329 | right: 0; 1330 | margin: 0 auto; 1331 | z-index: 28; 1332 | border-radius: 2px; 1333 | -webkit-box-shadow: 0 2px 4px #8a8a8a; 1334 | box-shadow: 0 2px 4px #8a8a8a; 1335 | background-color: #fff; 1336 | -webkit-transform: translate3d(0, 0, 0) scale(1.185); 1337 | transform: translate3d(0, 0, 0) scale(1.185); 1338 | -webkit-transition: opacity 0.3s, -webkit-transform 0.3s; 1339 | transition: opacity 0.3s, -webkit-transform 0.3s; 1340 | -o-transition: transform 0.3s, opacity 0.3s; 1341 | transition: transform 0.3s, opacity 0.3s; 1342 | transition: transform 0.3s, opacity 0.3s, -webkit-transform 0.3s; 1343 | } 1344 | .dialog.active { 1345 | opacity: 1; 1346 | -webkit-transform: translate3d(0, 0, 0) scale(1); 1347 | transform: translate3d(0, 0, 0) scale(1); 1348 | } 1349 | .dialog.close { 1350 | -webkit-animation: closeDialog 0.3s ease-out; 1351 | animation: closeDialog 0.3s ease-out; 1352 | } 1353 | .dialog .content { 1354 | position: relative; 1355 | max-height: 350px; 1356 | } 1357 | .dialog .content h1, 1358 | .dialog .content h2, 1359 | .dialog .content h3 { 1360 | margin-bottom: 22px; 1361 | } 1362 | .dialog .content .circle-progress { 1363 | position: relative; 1364 | margin-left: auto; 1365 | margin-right: auto; 1366 | left: 0; 1367 | } 1368 | .dialog .padded-full { 1369 | margin-bottom: 0; 1370 | } 1371 | @-webkit-keyframes closeDialog { 1372 | 0% { 1373 | opacity: 1; 1374 | -webkit-transform: translate3d(0, 0, 0) scale(1); 1375 | transform: translate3d(0, 0, 0) scale(1); 1376 | } 1377 | 100% { 1378 | opacity: 0; 1379 | -webkit-transform: translate3d(0, 0, 0) scale(0.815); 1380 | transform: translate3d(0, 0, 0) scale(0.815); 1381 | } 1382 | } 1383 | @keyframes closeDialog { 1384 | 0% { 1385 | opacity: 1; 1386 | -webkit-transform: translate3d(0, 0, 0) scale(1); 1387 | transform: translate3d(0, 0, 0) scale(1); 1388 | } 1389 | 100% { 1390 | opacity: 0; 1391 | -webkit-transform: translate3d(0, 0, 0) scale(0.815); 1392 | transform: translate3d(0, 0, 0) scale(0.815); 1393 | } 1394 | } 1395 | .notification { 1396 | width: notification-width; 1397 | height: auto; 1398 | min-height: 48px; 1399 | line-height: 54px; 1400 | vertical-align: bottom; 1401 | padding: 0 24px; 1402 | position: fixed; 1403 | z-index: 28; 1404 | left: 0; 1405 | right: 0; 1406 | bottom: 0; 1407 | border-top-radius: 2px; 1408 | margin-left: auto; 1409 | margin-right: auto; 1410 | opacity: 0; 1411 | color: #fff; 1412 | background-color: #333; 1413 | -webkit-transform: translate3d(0, 72px, 0); 1414 | transform: translate3d(0, 72px, 0); 1415 | -webkit-transition: opacity 200ms, -webkit-transform 0.3s; 1416 | transition: opacity 200ms, -webkit-transform 0.3s; 1417 | -o-transition: transform 0.3s, opacity 200ms; 1418 | transition: transform 0.3s, opacity 200ms; 1419 | transition: transform 0.3s, opacity 200ms, -webkit-transform 0.3s; 1420 | } 1421 | .notification.show { 1422 | -webkit-transform: translate3d(0, 0, 0); 1423 | transform: translate3d(0, 0, 0); 1424 | opacity: 1; 1425 | } 1426 | .notification .btn { 1427 | position: relative; 1428 | float: right; 1429 | top: 6px; 1430 | right: 0; 1431 | height: 42px; 1432 | line-height: 42px; 1433 | background-color: transparent; 1434 | font-weight: 700; 1435 | color: #fff; 1436 | } 1437 | .notification .progress { 1438 | position: absolute; 1439 | top: 0; 1440 | left: 0; 1441 | right: 0; 1442 | } 1443 | .backdrop-panel { 1444 | position: fixed; 1445 | top: 0; 1446 | left: 0; 1447 | right: 0; 1448 | bottom: 0; 1449 | z-index: 21; 1450 | background-color: rgba(0,0,0,0.1); 1451 | } 1452 | .backdrop-panel.fadeout { 1453 | background-color: transparent; 1454 | -webkit-transition: background-color 450ms ease-in-out; 1455 | -o-transition: background-color 450ms ease-in-out; 1456 | transition: background-color 450ms ease-in-out; 1457 | } 1458 | .panel, 1459 | .panel-full { 1460 | position: fixed; 1461 | top: 0; 1462 | left: 0; 1463 | right: 0; 1464 | bottom: 0; 1465 | z-index: 25; 1466 | display: none; 1467 | background-color: #fff; 1468 | } 1469 | .panel { 1470 | width: 100%; 1471 | height: auto; 1472 | max-height: 80%; 1473 | top: auto; 1474 | -webkit-transform: translate3d(0, 100%, 0); 1475 | transform: translate3d(0, 100%, 0); 1476 | -webkit-transition: -webkit-transform 300ms; 1477 | transition: -webkit-transform 300ms; 1478 | -o-transition: transform 300ms; 1479 | transition: transform 300ms; 1480 | transition: transform 300ms, -webkit-transform 300ms; 1481 | } 1482 | .panel .header-bar { 1483 | position: absolute !important; 1484 | } 1485 | .panel.active { 1486 | -webkit-transform: translate3d(0, 0, 0); 1487 | transform: translate3d(0, 0, 0); 1488 | } 1489 | .panel .content { 1490 | position: relative; 1491 | overflow: hidden; 1492 | } 1493 | .panel .content .list { 1494 | margin-bottom: 0; 1495 | } 1496 | .panel-full { 1497 | width: 100%; 1498 | height: 100%; 1499 | -webkit-transform: translate3d(0, 100%, 0); 1500 | transform: translate3d(0, 100%, 0); 1501 | } 1502 | .panel-full.active { 1503 | -webkit-transform: translate3d(0, 0, 0); 1504 | transform: translate3d(0, 0, 0); 1505 | opacity: 1; 1506 | } 1507 | .panel-full.active .content { 1508 | -webkit-transform: translate3d(0, 0, 0); 1509 | transform: translate3d(0, 0, 0); 1510 | -webkit-transition: -webkit-transform 300ms; 1511 | transition: -webkit-transform 300ms; 1512 | -o-transition: transform 300ms; 1513 | transition: transform 300ms; 1514 | transition: transform 300ms, -webkit-transform 300ms; 1515 | } 1516 | .panel-full .content { 1517 | -webkit-transform: translate3d(0, 50%, 0); 1518 | transform: translate3d(0, 50%, 0); 1519 | -webkit-transition: -webkit-transform 300ms; 1520 | transition: -webkit-transform 300ms; 1521 | -o-transition: transform 300ms; 1522 | transition: transform 300ms; 1523 | transition: transform 300ms, -webkit-transform 300ms; 1524 | } 1525 | .panel-closing { 1526 | -webkit-transition: -webkit-transform 300ms !important; 1527 | transition: -webkit-transform 300ms !important; 1528 | -o-transition: transform 300ms !important; 1529 | transition: transform 300ms !important; 1530 | transition: transform 300ms, -webkit-transform 300ms !important; 1531 | } 1532 | .backdrop-popover { 1533 | position: fixed; 1534 | top: 0; 1535 | right: 0; 1536 | bottom: 0; 1537 | left: 0; 1538 | z-index: 30; 1539 | background: transparent; 1540 | } 1541 | .popover { 1542 | z-index: 31; 1543 | position: fixed; 1544 | top: 12px; 1545 | left: 16px; 1546 | opacity: 0; 1547 | width: 160px; 1548 | height: auto; 1549 | background-color: #fff; 1550 | border-radius: 2px; 1551 | visibility: hidden; 1552 | -webkit-box-shadow: 0 0 4px #8a8a8a; 1553 | box-shadow: 0 0 4px #8a8a8a; 1554 | -webkit-transition: opacity 0.2s linear; 1555 | -o-transition: opacity 0.2s linear; 1556 | transition: opacity 0.2s linear; 1557 | } 1558 | .popover.active { 1559 | visibility: visible; 1560 | opacity: 1; 1561 | -webkit-transition: opacity 0.2s linear; 1562 | -o-transition: opacity 0.2s linear; 1563 | transition: opacity 0.2s linear; 1564 | } 1565 | .popover .list { 1566 | max-height: 188px; 1567 | overflow-x: hidden; 1568 | overflow-y: auto; 1569 | } 1570 | .popover .list li { 1571 | height: 47px; 1572 | line-height: 48px; 1573 | } 1574 | .circle-progress { 1575 | width: 36px; 1576 | height: 36px; 1577 | position: relative; 1578 | text-align: center; 1579 | display: block; 1580 | left: 50%; 1581 | right: 0; 1582 | margin-left: -18px; 1583 | z-index: 20; 1584 | opacity: 0; 1585 | -webkit-transition: opacity 0.65s ease; 1586 | -o-transition: opacity 0.65s ease; 1587 | transition: opacity 0.65s ease; 1588 | background-color: #fff; 1589 | border-radius: 50%; 1590 | } 1591 | .circle-progress.center { 1592 | top: 50%; 1593 | left: 50%; 1594 | margin-top: -18px; 1595 | position: absolute; 1596 | } 1597 | .circle-progress.active { 1598 | opacity: 1; 1599 | } 1600 | .circle-progress.active .spinner { 1601 | position: relative; 1602 | display: block; 1603 | width: 100%; 1604 | height: 100%; 1605 | border-radius: 50%; 1606 | border-top: 3px solid #0084e7; 1607 | border-right: 3px solid #0084e7; 1608 | border-bottom: 3px solid #0084e7; 1609 | border-left: 3px solid #eee; 1610 | -webkit-animation: circleLoading 900ms infinite linear; 1611 | animation: circleLoading 900ms infinite linear; 1612 | } 1613 | .circle-progress.primary, 1614 | .circle-progress.negative, 1615 | .circle-progress.positive { 1616 | background-color: #fff !important; 1617 | } 1618 | .circle-progress.primary .spinner { 1619 | border-top: 3px solid #0084e7; 1620 | border-right: 3px solid #0084e7; 1621 | border-bottom: 3px solid #0084e7; 1622 | } 1623 | .circle-progress.negative .spinner { 1624 | border-top: 3px solid #f40b00; 1625 | border-right: 3px solid #f40b00; 1626 | border-bottom: 3px solid #f40b00; 1627 | } 1628 | .circle-progress.positive .spinner { 1629 | border-top: 3px solid #0ae700; 1630 | border-right: 3px solid #0ae700; 1631 | border-bottom: 3px solid #0ae700; 1632 | } 1633 | .progress { 1634 | position: relative; 1635 | left: 0; 1636 | right: 0; 1637 | z-index: 20; 1638 | height: 6px; 1639 | display: none; 1640 | width: 100%; 1641 | background-color: rgba(255,255,255,0.4); 1642 | border-radius: 2px; 1643 | background-clip: padding-box; 1644 | overflow: hidden; 1645 | } 1646 | .progress.active { 1647 | display: block; 1648 | } 1649 | .progress .determinate { 1650 | position: absolute; 1651 | top: 0; 1652 | bottom: 0; 1653 | background-color: #0084e7; 1654 | -webkit-transition: width 0.2s linear; 1655 | -o-transition: width 0.2s linear; 1656 | transition: width 0.2s linear; 1657 | } 1658 | .progress.primary .determinate { 1659 | background-color: #0067b5 !important; 1660 | } 1661 | .progress.negative .determinate { 1662 | background-color: #c20900 !important; 1663 | } 1664 | .progress.positive .determinate { 1665 | background-color: #08b500 !important; 1666 | } 1667 | @-webkit-keyframes circleLoading { 1668 | 0% { 1669 | -webkit-transform: rotate(0deg) translateZ(0); 1670 | transform: rotate(0deg) translateZ(0); 1671 | } 1672 | 100% { 1673 | -webkit-transform: rotate(360deg) translateZ(0); 1674 | transform: rotate(360deg) translateZ(0); 1675 | } 1676 | } 1677 | @keyframes circleLoading { 1678 | 0% { 1679 | -webkit-transform: rotate(0deg) translateZ(0); 1680 | transform: rotate(0deg) translateZ(0); 1681 | } 1682 | 100% { 1683 | -webkit-transform: rotate(360deg) translateZ(0); 1684 | transform: rotate(360deg) translateZ(0); 1685 | } 1686 | } 1687 | .side-panel { 1688 | position: absolute; 1689 | top: 0; 1690 | right: auto; 1691 | left: auto; 1692 | bottom: 0; 1693 | width: 80%; 1694 | height: 100%; 1695 | visibility: hidden; 1696 | overflow: auto; 1697 | -webkit-overflow-scrolling: touch; 1698 | background-color: #333; 1699 | -webkit-transition: width 0.3s ease; 1700 | -o-transition: width 0.3s ease; 1701 | transition: width 0.3s ease; 1702 | } 1703 | .side-panel .list { 1704 | overflow-y: auto; 1705 | } 1706 | .side-panel .list li { 1707 | border-color: #444; 1708 | } 1709 | .side-panel .list a, 1710 | .side-panel .list li, 1711 | .side-panel .list .title, 1712 | .side-panel .list .body { 1713 | color: #fff; 1714 | } 1715 | .side-panel .list a:active, 1716 | .side-panel .list li:active, 1717 | .side-panel .list .title:active, 1718 | .side-panel .list .body:active { 1719 | background-color: #444; 1720 | } 1721 | .side-panel .list .accordion-content { 1722 | background-color: #333; 1723 | } 1724 | .side-panel .header-bar { 1725 | background-color: transparent; 1726 | } 1727 | .side-panel .header-bar .title { 1728 | color: side-panel-title-color; 1729 | font-weight: 400; 1730 | } 1731 | .side-panel .header-bar .btn { 1732 | color: #fff; 1733 | } 1734 | .side-panel-left { 1735 | left: 0; 1736 | z-index: 1; 1737 | } 1738 | .side-panel-right { 1739 | right: 0; 1740 | z-index: 1; 1741 | } 1742 | .snapjs-left .side-panel-right { 1743 | display: none !important; 1744 | } 1745 | .snapjs-right .side-panel-left { 1746 | display: none !important; 1747 | } 1748 | .awesomplete [hidden] { 1749 | display: none; 1750 | } 1751 | .awesomplete .visually-hidden { 1752 | position: absolute; 1753 | clip: rect(0, 0, 0, 0); 1754 | } 1755 | .awesomplete > ul { 1756 | position: absolute; 1757 | z-index: 1; 1758 | -webkit-box-sizing: border-box; 1759 | box-sizing: border-box; 1760 | list-style: none; 1761 | padding: 0; 1762 | margin: 0 auto; 1763 | background: #fff; 1764 | border-radius: 2px; 1765 | margin: 0.2em 0 0; 1766 | border: 1px solid rgba(0,0,0,0.15); 1767 | } 1768 | .awesomplete > ul:empty { 1769 | display: none; 1770 | } 1771 | .awesomplete > ul > li { 1772 | position: relative; 1773 | height: 52px; 1774 | line-height: 52px; 1775 | padding: 0 8px; 1776 | vertical-align: middle; 1777 | cursor: pointer; 1778 | } 1779 | .awesomplete mark { 1780 | background-color: #fff9c4; 1781 | } 1782 | .awesomplete > ul[hidden], 1783 | .awesomplete > ul:empty { 1784 | opacity: 0; 1785 | -webkit-transform: scale(0); 1786 | -ms-transform: scale(0); 1787 | transform: scale(0); 1788 | display: block; 1789 | -webkit-transition-timing-function: ease; 1790 | -o-transition-timing-function: ease; 1791 | transition-timing-function: ease; 1792 | } 1793 | -------------------------------------------------------------------------------- /spiffs/worker-html.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/spiffs/worker-html.js.gz -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/src/.DS_Store -------------------------------------------------------------------------------- /src/inavradarlogo.h: -------------------------------------------------------------------------------- 1 | // 'InavRadarLogo', 128x64px 2 | #define logo_width_s 128 3 | #define logo_height_s 64 4 | const uint8_t logo_bits_s [] PROGMEM = { 5 | 0xC0, 0x01, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 6 | 0x00, 0x00, 0x00, 0x00, 0x78, 0x0E, 0xC0, 0x34, 0x00, 0x04, 0x08, 0x00, 7 | 0x80, 0x00, 0x00, 0x0C, 0x40, 0x00, 0x00, 0x40, 0x0C, 0x18, 0x60, 0x60, 8 | 0x00, 0x0C, 0x1C, 0x00, 0x80, 0x01, 0x00, 0x0E, 0xC0, 0x00, 0x00, 0x60, 9 | 0x04, 0x10, 0x20, 0xC0, 0x00, 0x04, 0x18, 0x00, 0x80, 0x01, 0x00, 0x0A, 10 | 0x80, 0x00, 0x00, 0x30, 0x02, 0x30, 0x10, 0x80, 0x00, 0x0C, 0x38, 0x00, 11 | 0x80, 0x01, 0x00, 0x1A, 0x80, 0x01, 0x00, 0x30, 0x82, 0x20, 0x10, 0x04, 12 | 0x01, 0x0C, 0x6C, 0x00, 0x80, 0x01, 0x00, 0x13, 0x80, 0x01, 0x00, 0x10, 13 | 0xC2, 0x21, 0x10, 0x0E, 0x01, 0x04, 0x4C, 0x00, 0x80, 0x00, 0x00, 0x33, 14 | 0x00, 0x01, 0x00, 0x18, 0xC3, 0x21, 0x18, 0x0E, 0x01, 0x0C, 0xCC, 0x00, 15 | 0x80, 0x01, 0x80, 0x31, 0x00, 0x03, 0x00, 0x18, 0x82, 0x23, 0x18, 0x07, 16 | 0x01, 0x0C, 0x8C, 0x01, 0x80, 0x01, 0x80, 0x21, 0x00, 0x03, 0x00, 0x08, 17 | 0x06, 0x26, 0x90, 0x81, 0x00, 0x0C, 0x88, 0x01, 0x80, 0x01, 0xC0, 0x60, 18 | 0x00, 0x06, 0x00, 0x0C, 0x04, 0x8C, 0x42, 0xC0, 0x00, 0x04, 0x0C, 0x03, 19 | 0x80, 0x00, 0xC0, 0x60, 0x00, 0x06, 0x00, 0x0C, 0x0C, 0x48, 0x6A, 0x60, 20 | 0x00, 0x0C, 0x0C, 0x06, 0x80, 0x01, 0x40, 0xC0, 0x00, 0x06, 0x00, 0x06, 21 | 0xB0, 0x62, 0x18, 0x34, 0x00, 0x0C, 0x0C, 0x0C, 0x80, 0x01, 0x60, 0xC0, 22 | 0x00, 0x0C, 0x00, 0x06, 0xE0, 0x11, 0x10, 0x0F, 0x00, 0x04, 0x0C, 0x0C, 23 | 0x80, 0x00, 0x60, 0x80, 0x00, 0x0C, 0x00, 0x02, 0x00, 0x20, 0x10, 0x00, 24 | 0x00, 0x0C, 0x0C, 0x18, 0x80, 0x01, 0x20, 0x80, 0x01, 0x18, 0x00, 0x03, 25 | 0x00, 0x10, 0x10, 0x00, 0x00, 0x0C, 0x08, 0x30, 0x80, 0x01, 0x30, 0x80, 26 | 0x01, 0x18, 0x00, 0x03, 0x00, 0x20, 0x20, 0x00, 0x00, 0x0C, 0x0C, 0x30, 27 | 0x80, 0x01, 0x30, 0x00, 0x03, 0x18, 0x80, 0x01, 0x00, 0x20, 0x18, 0x00, 28 | 0x00, 0x04, 0x0C, 0x60, 0x80, 0x01, 0x18, 0x00, 0x03, 0x30, 0x80, 0x01, 29 | 0x00, 0xA0, 0x18, 0x00, 0x00, 0x0C, 0x08, 0xC0, 0x80, 0x00, 0x18, 0x00, 30 | 0x02, 0x30, 0x80, 0x00, 0x00, 0xE0, 0x0B, 0x00, 0x00, 0x0C, 0x0C, 0xC0, 31 | 0x80, 0x01, 0x08, 0x00, 0x06, 0x20, 0xC0, 0x00, 0xF0, 0x43, 0x0A, 0x3F, 32 | 0x00, 0x0C, 0x0C, 0x80, 0x81, 0x01, 0x2C, 0x24, 0x06, 0x60, 0xC0, 0x00, 33 | 0x18, 0x80, 0x06, 0x60, 0x00, 0x0C, 0x08, 0x00, 0x83, 0x01, 0xFC, 0xFF, 34 | 0x0F, 0x60, 0x60, 0x00, 0x0C, 0x8C, 0x63, 0xC0, 0x00, 0x04, 0x0C, 0x00, 35 | 0x87, 0x00, 0x06, 0x00, 0x0C, 0x40, 0x60, 0x00, 0x04, 0x04, 0xC2, 0x80, 36 | 0x00, 0x0C, 0x08, 0x00, 0x86, 0x01, 0x06, 0x00, 0x08, 0xC0, 0x60, 0x00, 37 | 0x06, 0x26, 0x98, 0x81, 0x01, 0x0C, 0x0C, 0x00, 0x8C, 0x01, 0x02, 0x00, 38 | 0x18, 0xC0, 0x30, 0x00, 0xC2, 0x21, 0x10, 0x07, 0x01, 0x0C, 0x0C, 0x00, 39 | 0x98, 0x00, 0x03, 0x00, 0x18, 0x80, 0x31, 0x00, 0xC2, 0x21, 0x08, 0x0E, 40 | 0x01, 0x04, 0x0C, 0x00, 0x98, 0x01, 0x03, 0x00, 0x10, 0x80, 0x11, 0x00, 41 | 0xC2, 0x21, 0x10, 0x0E, 0x01, 0x0C, 0x08, 0x00, 0xB0, 0x01, 0x01, 0x00, 42 | 0x30, 0x00, 0x19, 0x00, 0x02, 0x30, 0x10, 0x80, 0x01, 0x0C, 0x08, 0x00, 43 | 0xA0, 0x80, 0x01, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x06, 0x10, 0x30, 0x80, 44 | 0x00, 0x04, 0x0C, 0x00, 0xE0, 0x81, 0x01, 0x00, 0x60, 0x00, 0x0B, 0x00, 45 | 0x0C, 0x18, 0x20, 0xC0, 0x00, 0x0C, 0x0C, 0x00, 0xC0, 0xC1, 0x00, 0x00, 46 | 0x60, 0x00, 0x0E, 0x00, 0x18, 0x0C, 0xC0, 0x60, 0x00, 0x0C, 0x0C, 0x00, 47 | 0x80, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x0E, 0x00, 0xF0, 0x07, 0x80, 0x1F, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 52 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 53 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 54 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 56 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 57 | 0xA8, 0x00, 0x00, 0x04, 0x00, 0x50, 0x00, 0x00, 0x04, 0x00, 0xA4, 0x00, 58 | 0x00, 0x00, 0x00, 0x10, 0xBE, 0x07, 0x00, 0x04, 0x00, 0xBE, 0x03, 0x00, 59 | 0x04, 0x00, 0xAC, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x04, 0x0C, 0x00, 0x0C, 60 | 0x00, 0x02, 0x0C, 0x00, 0x04, 0x00, 0x06, 0x08, 0x00, 0x00, 0x80, 0x3F, 61 | 0x04, 0x18, 0x00, 0x0E, 0x00, 0x03, 0x08, 0x00, 0x0E, 0x00, 0x06, 0x18, 62 | 0x00, 0x00, 0xF0, 0x3F, 0x06, 0x10, 0x00, 0x0A, 0x00, 0x02, 0x18, 0x00, 63 | 0x0A, 0x00, 0x04, 0x10, 0x00, 0x00, 0xF8, 0x7F, 0x06, 0x10, 0x00, 0x1A, 64 | 0x00, 0x02, 0x10, 0x00, 0x1A, 0x00, 0x06, 0x10, 0x00, 0x00, 0xFF, 0x7F, 65 | 0x04, 0x30, 0x00, 0x11, 0x00, 0x03, 0x10, 0x00, 0x13, 0x00, 0x04, 0x30, 66 | 0x00, 0xC0, 0xFF, 0x7F, 0x06, 0x30, 0x00, 0x11, 0x00, 0x02, 0x30, 0x00, 67 | 0x11, 0x00, 0x04, 0x30, 0x00, 0xF0, 0xFF, 0x3F, 0x04, 0x10, 0x00, 0x21, 68 | 0x00, 0x02, 0x30, 0x00, 0x31, 0x00, 0x06, 0x10, 0x00, 0xFC, 0xFF, 0x7F, 69 | 0x06, 0x18, 0x80, 0x21, 0x00, 0x02, 0x30, 0x80, 0x20, 0x00, 0x04, 0x18, 70 | 0x80, 0xFF, 0xFF, 0xFF, 0x04, 0x0C, 0x80, 0x20, 0x00, 0x03, 0x20, 0x80, 71 | 0x20, 0x00, 0x06, 0x08, 0xE0, 0xFF, 0xFF, 0x7F, 0x06, 0x07, 0x80, 0x60, 72 | 0x00, 0x02, 0x30, 0x80, 0x60, 0x00, 0x04, 0x06, 0x78, 0xFF, 0xFF, 0x7F, 73 | 0xFC, 0x01, 0xC0, 0x40, 0x00, 0x02, 0x20, 0xC0, 0x40, 0x00, 0xFE, 0x03, 74 | 0xF6, 0xFE, 0xFF, 0x7F, 0x06, 0x01, 0x40, 0x40, 0x00, 0x03, 0x30, 0x40, 75 | 0x40, 0x00, 0x04, 0x01, 0xF8, 0xFD, 0xFF, 0xFF, 0x04, 0x03, 0xC0, 0xCA, 76 | 0x00, 0x02, 0x30, 0x40, 0xE1, 0x00, 0x04, 0x03, 0xC0, 0xFF, 0xFF, 0x7F, 77 | 0x04, 0x06, 0x60, 0xF9, 0x00, 0x02, 0x30, 0xE0, 0xBB, 0x00, 0x06, 0x06, 78 | 0x00, 0xFF, 0xFF, 0x7F, 0x06, 0x04, 0x20, 0x80, 0x00, 0x02, 0x10, 0x20, 79 | 0x80, 0x00, 0x04, 0x04, 0x00, 0xF8, 0xFF, 0x7F, 0x04, 0x0C, 0x20, 0x80, 80 | 0x01, 0x02, 0x10, 0x20, 0x80, 0x01, 0x04, 0x0C, 0x00, 0xE0, 0xFF, 0x7F, 81 | 0x06, 0x18, 0x30, 0x00, 0x01, 0x02, 0x18, 0x30, 0x00, 0x01, 0x04, 0x18, 82 | 0x00, 0x00, 0xFF, 0x7F, 0x06, 0x10, 0x10, 0x00, 0x01, 0x03, 0x08, 0x10, 83 | 0x00, 0x03, 0x06, 0x10, 0x00, 0x00, 0xFE, 0x7F, 0x04, 0x30, 0x18, 0x00, 84 | 0x03, 0x02, 0x0C, 0x18, 0x00, 0x03, 0x04, 0x30, 0x00, 0x00, 0xF0, 0x3F, 85 | 0x06, 0x60, 0x18, 0x00, 0x02, 0xEE, 0x03, 0x18, 0x00, 0x02, 0x04, 0x60, 86 | 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 87 | 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 89 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 90 | 0x00, 0x00, 0x00, 0x00, }; 91 | -------------------------------------------------------------------------------- /src/lib/LoRa.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Sandeep Mistry. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #include 5 | 6 | // registers 7 | #define REG_FIFO 0x00 8 | #define REG_OP_MODE 0x01 9 | #define REG_FRF_MSB 0x06 10 | #define REG_FRF_MID 0x07 11 | #define REG_FRF_LSB 0x08 12 | #define REG_PA_CONFIG 0x09 13 | #define REG_OCP 0x0b 14 | #define REG_LNA 0x0c 15 | #define REG_FIFO_ADDR_PTR 0x0d 16 | #define REG_FIFO_TX_BASE_ADDR 0x0e 17 | #define REG_FIFO_RX_BASE_ADDR 0x0f 18 | #define REG_FIFO_RX_CURRENT_ADDR 0x10 19 | #define REG_IRQ_FLAGS 0x12 20 | #define REG_RX_NB_BYTES 0x13 21 | #define REG_PKT_SNR_VALUE 0x19 22 | #define REG_PKT_RSSI_VALUE 0x1a 23 | #define REG_MODEM_CONFIG_1 0x1d 24 | #define REG_MODEM_CONFIG_2 0x1e 25 | #define REG_PREAMBLE_MSB 0x20 26 | #define REG_PREAMBLE_LSB 0x21 27 | #define REG_PAYLOAD_LENGTH 0x22 28 | #define REG_MODEM_CONFIG_3 0x26 29 | #define REG_FREQ_ERROR_MSB 0x28 30 | #define REG_FREQ_ERROR_MID 0x29 31 | #define REG_FREQ_ERROR_LSB 0x2a 32 | #define REG_RSSI_WIDEBAND 0x2c 33 | #define REG_DETECTION_OPTIMIZE 0x31 34 | #define REG_INVERTIQ 0x33 35 | #define REG_DETECTION_THRESHOLD 0x37 36 | #define REG_SYNC_WORD 0x39 37 | #define REG_INVERTIQ2 0x3b 38 | #define REG_DIO_MAPPING_1 0x40 39 | #define REG_VERSION 0x42 40 | #define REG_PA_DAC 0x4d 41 | 42 | // modes 43 | #define MODE_LONG_RANGE_MODE 0x80 44 | #define MODE_SLEEP 0x00 45 | #define MODE_STDBY 0x01 46 | #define MODE_TX 0x03 47 | #define MODE_RX_CONTINUOUS 0x05 48 | #define MODE_RX_SINGLE 0x06 49 | 50 | // PA config 51 | #define PA_BOOST 0x80 52 | 53 | // IRQ masks 54 | #define IRQ_TX_DONE_MASK 0x08 55 | #define IRQ_PAYLOAD_CRC_ERROR_MASK 0x20 56 | #define IRQ_RX_DONE_MASK 0x40 57 | 58 | #define MAX_PKT_LENGTH 255 59 | 60 | LoRaClass::LoRaClass() : 61 | _spiSettings(LORA_DEFAULT_SPI_FREQUENCY, MSBFIRST, SPI_MODE0), 62 | _spi(&LORA_DEFAULT_SPI), 63 | _ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), 64 | _frequency(0), 65 | _packetIndex(0), 66 | _implicitHeaderMode(0), 67 | _onReceive(NULL) 68 | { 69 | // overide Stream timeout value 70 | setTimeout(0); 71 | } 72 | 73 | int LoRaClass::begin(long frequency) 74 | { 75 | #ifdef ARDUINO_SAMD_MKRWAN1300 76 | pinMode(LORA_IRQ_DUMB, OUTPUT); 77 | digitalWrite(LORA_IRQ_DUMB, LOW); 78 | 79 | // Hardware reset 80 | pinMode(LORA_BOOT0, OUTPUT); 81 | digitalWrite(LORA_BOOT0, LOW); 82 | 83 | pinMode(LORA_RESET, OUTPUT); 84 | digitalWrite(LORA_RESET, HIGH); 85 | delay(200); 86 | digitalWrite(LORA_RESET, LOW); 87 | delay(200); 88 | digitalWrite(LORA_RESET, HIGH); 89 | delay(50); 90 | #endif 91 | 92 | // setup pins 93 | pinMode(_ss, OUTPUT); 94 | // set SS high 95 | digitalWrite(_ss, HIGH); 96 | 97 | if (_reset != -1) { 98 | pinMode(_reset, OUTPUT); 99 | 100 | // perform reset 101 | digitalWrite(_reset, LOW); 102 | delay(10); 103 | digitalWrite(_reset, HIGH); 104 | delay(10); 105 | } 106 | 107 | // start SPI 108 | _spi->begin(); 109 | 110 | // check version 111 | uint8_t version = readRegister(REG_VERSION); 112 | if (version != 0x12) { 113 | return 0; 114 | } 115 | 116 | // put in sleep mode 117 | sleep(); 118 | 119 | // set frequency 120 | setFrequency(frequency); 121 | 122 | // set base addresses 123 | writeRegister(REG_FIFO_TX_BASE_ADDR, 0); 124 | writeRegister(REG_FIFO_RX_BASE_ADDR, 0); 125 | 126 | // set LNA boost 127 | writeRegister(REG_LNA, readRegister(REG_LNA) | 0x03); 128 | 129 | // set auto AGC 130 | writeRegister(REG_MODEM_CONFIG_3, 0x04); 131 | 132 | // set output power to 17 dBm 133 | setTxPower(17); 134 | 135 | // put in standby mode 136 | idle(); 137 | 138 | return 1; 139 | } 140 | 141 | void LoRaClass::end() 142 | { 143 | // put in sleep mode 144 | sleep(); 145 | 146 | // stop SPI 147 | _spi->end(); 148 | } 149 | 150 | int LoRaClass::beginPacket(int implicitHeader) 151 | { 152 | if (isTransmitting()) { 153 | return 0; 154 | } 155 | 156 | // put in standby mode 157 | idle(); 158 | 159 | if (implicitHeader) { 160 | implicitHeaderMode(); 161 | } else { 162 | explicitHeaderMode(); 163 | } 164 | 165 | // reset FIFO address and paload length 166 | writeRegister(REG_FIFO_ADDR_PTR, 0); 167 | writeRegister(REG_PAYLOAD_LENGTH, 0); 168 | 169 | return 1; 170 | } 171 | 172 | int LoRaClass::endPacket(bool async) 173 | { 174 | // put in TX mode 175 | writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_TX); 176 | 177 | if (async) { 178 | // grace time is required for the radio 179 | delayMicroseconds(150); 180 | } else { 181 | // wait for TX done 182 | while ((readRegister(REG_IRQ_FLAGS) & IRQ_TX_DONE_MASK) == 0) { 183 | yield(); 184 | } 185 | // clear IRQ's 186 | writeRegister(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK); 187 | } 188 | 189 | return 1; 190 | } 191 | 192 | bool LoRaClass::isTransmitting() 193 | { 194 | if ((readRegister(REG_OP_MODE) & MODE_TX) == MODE_TX) { 195 | return true; 196 | } 197 | 198 | if (readRegister(REG_IRQ_FLAGS) & IRQ_TX_DONE_MASK) { 199 | // clear IRQ's 200 | writeRegister(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK); 201 | } 202 | 203 | return false; 204 | } 205 | 206 | int LoRaClass::parsePacket(int size) 207 | { 208 | int packetLength = 0; 209 | int irqFlags = readRegister(REG_IRQ_FLAGS); 210 | 211 | if (size > 0) { 212 | implicitHeaderMode(); 213 | 214 | writeRegister(REG_PAYLOAD_LENGTH, size & 0xff); 215 | } else { 216 | explicitHeaderMode(); 217 | } 218 | 219 | // clear IRQ's 220 | writeRegister(REG_IRQ_FLAGS, irqFlags); 221 | 222 | if ((irqFlags & IRQ_RX_DONE_MASK) && (irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK) == 0) { 223 | // received a packet 224 | _packetIndex = 0; 225 | 226 | // read packet length 227 | if (_implicitHeaderMode) { 228 | packetLength = readRegister(REG_PAYLOAD_LENGTH); 229 | } else { 230 | packetLength = readRegister(REG_RX_NB_BYTES); 231 | } 232 | 233 | // set FIFO address to current RX address 234 | writeRegister(REG_FIFO_ADDR_PTR, readRegister(REG_FIFO_RX_CURRENT_ADDR)); 235 | 236 | // put in standby mode 237 | idle(); 238 | } else if (readRegister(REG_OP_MODE) != (MODE_LONG_RANGE_MODE | MODE_RX_SINGLE)) { 239 | // not currently in RX mode 240 | 241 | // reset FIFO address 242 | writeRegister(REG_FIFO_ADDR_PTR, 0); 243 | 244 | // put in single RX mode 245 | writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_RX_SINGLE); 246 | } 247 | 248 | return packetLength; 249 | } 250 | 251 | int LoRaClass::packetRssi() 252 | { 253 | return (readRegister(REG_PKT_RSSI_VALUE) - (_frequency < 868E6 ? 164 : 157)); 254 | } 255 | 256 | float LoRaClass::packetSnr() 257 | { 258 | return ((int8_t)readRegister(REG_PKT_SNR_VALUE)) * 0.25; 259 | } 260 | 261 | long LoRaClass::packetFrequencyError() 262 | { 263 | int32_t freqError = 0; 264 | freqError = static_cast(readRegister(REG_FREQ_ERROR_MSB) & B111); 265 | freqError <<= 8L; 266 | freqError += static_cast(readRegister(REG_FREQ_ERROR_MID)); 267 | freqError <<= 8L; 268 | freqError += static_cast(readRegister(REG_FREQ_ERROR_LSB)); 269 | 270 | if (readRegister(REG_FREQ_ERROR_MSB) & B1000) { // Sign bit is on 271 | freqError -= 524288; // B1000'0000'0000'0000'0000 272 | } 273 | 274 | const float fXtal = 32E6; // FXOSC: crystal oscillator (XTAL) frequency (2.5. Chip Specification, p. 14) 275 | const float fError = ((static_cast(freqError) * (1L << 24)) / fXtal) * (getSignalBandwidth() / 500000.0f); // p. 37 276 | 277 | return static_cast(fError); 278 | } 279 | 280 | size_t LoRaClass::write(uint8_t byte) 281 | { 282 | return write(&byte, sizeof(byte)); 283 | } 284 | 285 | size_t LoRaClass::write(const uint8_t *buffer, size_t size) 286 | { 287 | int currentLength = readRegister(REG_PAYLOAD_LENGTH); 288 | 289 | // check size 290 | if ((currentLength + size) > MAX_PKT_LENGTH) { 291 | size = MAX_PKT_LENGTH - currentLength; 292 | } 293 | 294 | // write data 295 | for (size_t i = 0; i < size; i++) { 296 | writeRegister(REG_FIFO, buffer[i]); 297 | } 298 | 299 | // update length 300 | writeRegister(REG_PAYLOAD_LENGTH, currentLength + size); 301 | 302 | return size; 303 | } 304 | 305 | int LoRaClass::available() 306 | { 307 | return (readRegister(REG_RX_NB_BYTES) - _packetIndex); 308 | } 309 | 310 | int LoRaClass::read() 311 | { 312 | if (!available()) { 313 | return -1; 314 | } 315 | 316 | _packetIndex++; 317 | 318 | return readRegister(REG_FIFO); 319 | } 320 | 321 | int LoRaClass::peek() 322 | { 323 | if (!available()) { 324 | return -1; 325 | } 326 | 327 | // store current FIFO address 328 | int currentAddress = readRegister(REG_FIFO_ADDR_PTR); 329 | 330 | // read 331 | uint8_t b = readRegister(REG_FIFO); 332 | 333 | // restore FIFO address 334 | writeRegister(REG_FIFO_ADDR_PTR, currentAddress); 335 | 336 | return b; 337 | } 338 | 339 | void LoRaClass::flush() 340 | { 341 | } 342 | 343 | #ifndef ARDUINO_SAMD_MKRWAN1300 344 | void LoRaClass::onReceive(void(*callback)(int)) 345 | { 346 | _onReceive = callback; 347 | 348 | if (callback) { 349 | pinMode(_dio0, INPUT); 350 | 351 | writeRegister(REG_DIO_MAPPING_1, 0x00); 352 | #ifdef SPI_HAS_NOTUSINGINTERRUPT 353 | SPI.usingInterrupt(digitalPinToInterrupt(_dio0)); 354 | #endif 355 | attachInterrupt(digitalPinToInterrupt(_dio0), LoRaClass::onDio0Rise, RISING); 356 | } else { 357 | detachInterrupt(digitalPinToInterrupt(_dio0)); 358 | #ifdef SPI_HAS_NOTUSINGINTERRUPT 359 | SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0)); 360 | #endif 361 | } 362 | } 363 | 364 | void LoRaClass::receive(int size) 365 | { 366 | if (size > 0) { 367 | implicitHeaderMode(); 368 | 369 | writeRegister(REG_PAYLOAD_LENGTH, size & 0xff); 370 | } else { 371 | explicitHeaderMode(); 372 | } 373 | 374 | writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_RX_CONTINUOUS); 375 | } 376 | #endif 377 | 378 | void LoRaClass::idle() 379 | { 380 | writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_STDBY); 381 | } 382 | 383 | void LoRaClass::sleep() 384 | { 385 | writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_SLEEP); 386 | } 387 | 388 | void LoRaClass::setTxPower(int level, int outputPin) 389 | { 390 | if (PA_OUTPUT_RFO_PIN == outputPin) { 391 | // RFO 392 | if (level < 0) { 393 | level = 0; 394 | } else if (level > 14) { 395 | level = 14; 396 | } 397 | 398 | writeRegister(REG_PA_CONFIG, 0x70 | level); 399 | } else { 400 | // PA BOOST 401 | if (level > 17) { 402 | if (level > 20) { 403 | level = 20; 404 | } 405 | 406 | // subtract 3 from level, so 18 - 20 maps to 15 - 17 407 | level -= 3; 408 | 409 | // High Power +20 dBm Operation (Semtech SX1276/77/78/79 5.4.3.) 410 | writeRegister(REG_PA_DAC, 0x87); 411 | setOCP(140); 412 | } else { 413 | if (level < 2) { 414 | level = 2; 415 | } 416 | //Default value PA_HF/LF or +17dBm 417 | writeRegister(REG_PA_DAC, 0x84); 418 | setOCP(100); 419 | } 420 | 421 | writeRegister(REG_PA_CONFIG, PA_BOOST | (level - 2)); 422 | } 423 | } 424 | 425 | void LoRaClass::setFrequency(long frequency) 426 | { 427 | _frequency = frequency; 428 | 429 | uint64_t frf = ((uint64_t)frequency << 19) / 32000000; 430 | 431 | writeRegister(REG_FRF_MSB, (uint8_t)(frf >> 16)); 432 | writeRegister(REG_FRF_MID, (uint8_t)(frf >> 8)); 433 | writeRegister(REG_FRF_LSB, (uint8_t)(frf >> 0)); 434 | } 435 | 436 | int LoRaClass::getSpreadingFactor() 437 | { 438 | return readRegister(REG_MODEM_CONFIG_2) >> 4; 439 | } 440 | 441 | void LoRaClass::setSpreadingFactor(int sf) 442 | { 443 | if (sf < 6) { 444 | sf = 6; 445 | } else if (sf > 12) { 446 | sf = 12; 447 | } 448 | 449 | if (sf == 6) { 450 | writeRegister(REG_DETECTION_OPTIMIZE, 0xc5); 451 | writeRegister(REG_DETECTION_THRESHOLD, 0x0c); 452 | } else { 453 | writeRegister(REG_DETECTION_OPTIMIZE, 0xc3); 454 | writeRegister(REG_DETECTION_THRESHOLD, 0x0a); 455 | } 456 | 457 | writeRegister(REG_MODEM_CONFIG_2, (readRegister(REG_MODEM_CONFIG_2) & 0x0f) | ((sf << 4) & 0xf0)); 458 | setLdoFlag(); 459 | } 460 | 461 | long LoRaClass::getSignalBandwidth() 462 | { 463 | byte bw = (readRegister(REG_MODEM_CONFIG_1) >> 4); 464 | 465 | switch (bw) { 466 | case 0: return 7.8E3; 467 | case 1: return 10.4E3; 468 | case 2: return 15.6E3; 469 | case 3: return 20.8E3; 470 | case 4: return 31.25E3; 471 | case 5: return 41.7E3; 472 | case 6: return 62.5E3; 473 | case 7: return 125E3; 474 | case 8: return 250E3; 475 | case 9: return 500E3; 476 | } 477 | 478 | return -1; 479 | } 480 | 481 | // void LoRaClass::setSignalBandwidth(long sbw, long frequency) 482 | void LoRaClass::setSignalBandwidth(long sbw) 483 | { 484 | int bw; 485 | 486 | if (sbw <= 7.8E3) { 487 | bw = 0; 488 | } else if (sbw <= 10.4E3) { 489 | bw = 1; 490 | } else if (sbw <= 15.6E3) { 491 | bw = 2; 492 | } else if (sbw <= 20.8E3) { 493 | bw = 3; 494 | } else if (sbw <= 31.25E3) { 495 | bw = 4; 496 | } else if (sbw <= 41.7E3) { 497 | bw = 5; 498 | } else if (sbw <= 62.5E3) { 499 | bw = 6; 500 | } else if (sbw <= 125E3) { 501 | bw = 7; 502 | } else if (sbw <= 250E3) { 503 | bw = 8; 504 | } else /*if (sbw == 500E3)*/ { 505 | bw = 9; 506 | /* 507 | if (frequency == 443E6) { 508 | writeRegister(0x36, 0x02); 509 | writeRegister(0x3a, 0x64); 510 | } 511 | else { // 868E6 915E6 512 | writeRegister(0x36, 0x02); 513 | writeRegister(0x3a, 0x7F); 514 | } 515 | */ 516 | } 517 | 518 | writeRegister(REG_MODEM_CONFIG_1, (readRegister(REG_MODEM_CONFIG_1) & 0x0f) | (bw << 4)); 519 | setLdoFlag(); 520 | } 521 | 522 | void LoRaClass::setLdoFlag() 523 | { 524 | // Section 4.1.1.5 525 | long symbolDuration = 1000 / ( getSignalBandwidth() / (1L << getSpreadingFactor()) ) ; 526 | 527 | // Section 4.1.1.6 528 | boolean ldoOn = symbolDuration > 16; 529 | 530 | uint8_t config3 = readRegister(REG_MODEM_CONFIG_3); 531 | bitWrite(config3, 3, ldoOn); 532 | writeRegister(REG_MODEM_CONFIG_3, config3); 533 | } 534 | 535 | void LoRaClass::setCodingRate4(int denominator) 536 | { 537 | if (denominator < 5) { 538 | denominator = 5; 539 | } else if (denominator > 8) { 540 | denominator = 8; 541 | } 542 | 543 | int cr = denominator - 4; 544 | 545 | writeRegister(REG_MODEM_CONFIG_1, (readRegister(REG_MODEM_CONFIG_1) & 0xf1) | (cr << 1)); 546 | } 547 | 548 | void LoRaClass::setPreambleLength(long length) 549 | { 550 | writeRegister(REG_PREAMBLE_MSB, (uint8_t)(length >> 8)); 551 | writeRegister(REG_PREAMBLE_LSB, (uint8_t)(length >> 0)); 552 | } 553 | 554 | void LoRaClass::setSyncWord(int sw) 555 | { 556 | writeRegister(REG_SYNC_WORD, sw); 557 | } 558 | 559 | void LoRaClass::enableCrc() 560 | { 561 | writeRegister(REG_MODEM_CONFIG_2, readRegister(REG_MODEM_CONFIG_2) | 0x04); 562 | } 563 | 564 | void LoRaClass::disableCrc() 565 | { 566 | writeRegister(REG_MODEM_CONFIG_2, readRegister(REG_MODEM_CONFIG_2) & 0xfb); 567 | } 568 | 569 | void LoRaClass::enableInvertIQ() 570 | { 571 | writeRegister(REG_INVERTIQ, 0x66); 572 | writeRegister(REG_INVERTIQ2, 0x19); 573 | } 574 | 575 | void LoRaClass::disableInvertIQ() 576 | { 577 | writeRegister(REG_INVERTIQ, 0x27); 578 | writeRegister(REG_INVERTIQ2, 0x1d); 579 | } 580 | 581 | void LoRaClass::setOCP(uint8_t mA) 582 | { 583 | uint8_t ocpTrim = 27; 584 | 585 | if (mA <= 120) { 586 | ocpTrim = (mA - 45) / 5; 587 | } else if (mA <=240) { 588 | ocpTrim = (mA + 30) / 10; 589 | } 590 | 591 | writeRegister(REG_OCP, 0x20 | (0x1F & ocpTrim)); 592 | } 593 | 594 | byte LoRaClass::random() 595 | { 596 | return readRegister(REG_RSSI_WIDEBAND); 597 | } 598 | 599 | void LoRaClass::setPins(int ss, int reset, int dio0) 600 | { 601 | _ss = ss; 602 | _reset = reset; 603 | _dio0 = dio0; 604 | } 605 | 606 | void LoRaClass::setSPI(SPIClass& spi) 607 | { 608 | _spi = &spi; 609 | } 610 | 611 | void LoRaClass::setSPIFrequency(uint32_t frequency) 612 | { 613 | _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); 614 | } 615 | 616 | void LoRaClass::dumpRegisters(Stream& out) 617 | { 618 | for (int i = 0; i < 128; i++) { 619 | out.print("0x"); 620 | out.print(i, HEX); 621 | out.print(": 0x"); 622 | out.println(readRegister(i), HEX); 623 | } 624 | } 625 | 626 | void LoRaClass::explicitHeaderMode() 627 | { 628 | _implicitHeaderMode = 0; 629 | 630 | writeRegister(REG_MODEM_CONFIG_1, readRegister(REG_MODEM_CONFIG_1) & 0xfe); 631 | } 632 | 633 | void LoRaClass::implicitHeaderMode() 634 | { 635 | _implicitHeaderMode = 1; 636 | 637 | writeRegister(REG_MODEM_CONFIG_1, readRegister(REG_MODEM_CONFIG_1) | 0x01); 638 | } 639 | 640 | void LoRaClass::handleDio0Rise() 641 | { 642 | int irqFlags = readRegister(REG_IRQ_FLAGS); 643 | 644 | // clear IRQ's 645 | writeRegister(REG_IRQ_FLAGS, irqFlags); 646 | 647 | if ((irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK) == 0) { 648 | // received a packet 649 | _packetIndex = 0; 650 | 651 | // read packet length 652 | int packetLength = _implicitHeaderMode ? readRegister(REG_PAYLOAD_LENGTH) : readRegister(REG_RX_NB_BYTES); 653 | 654 | // set FIFO address to current RX address 655 | writeRegister(REG_FIFO_ADDR_PTR, readRegister(REG_FIFO_RX_CURRENT_ADDR)); 656 | 657 | if (_onReceive) { 658 | _onReceive(packetLength); 659 | } 660 | 661 | // reset FIFO address 662 | writeRegister(REG_FIFO_ADDR_PTR, 0); 663 | } 664 | } 665 | 666 | uint8_t LoRaClass::readRegister(uint8_t address) 667 | { 668 | return singleTransfer(address & 0x7f, 0x00); 669 | } 670 | 671 | void LoRaClass::writeRegister(uint8_t address, uint8_t value) 672 | { 673 | singleTransfer(address | 0x80, value); 674 | } 675 | 676 | uint8_t LoRaClass::singleTransfer(uint8_t address, uint8_t value) 677 | { 678 | uint8_t response; 679 | 680 | digitalWrite(_ss, LOW); 681 | 682 | _spi->beginTransaction(_spiSettings); 683 | _spi->transfer(address); 684 | response = _spi->transfer(value); 685 | _spi->endTransaction(); 686 | 687 | digitalWrite(_ss, HIGH); 688 | 689 | return response; 690 | } 691 | 692 | void LoRaClass::onDio0Rise() 693 | { 694 | LoRa.handleDio0Rise(); 695 | } 696 | 697 | LoRaClass LoRa; 698 | -------------------------------------------------------------------------------- /src/lib/LoRa.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Sandeep Mistry. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #ifndef LORA_H 5 | #define LORA_H 6 | 7 | #include 8 | #include 9 | 10 | #ifdef ARDUINO_SAMD_MKRWAN1300 11 | #define LORA_DEFAULT_SPI SPI1 12 | #define LORA_DEFAULT_SPI_FREQUENCY 250000 13 | #define LORA_DEFAULT_SS_PIN LORA_IRQ_DUMB 14 | #define LORA_DEFAULT_RESET_PIN -1 15 | #define LORA_DEFAULT_DIO0_PIN -1 16 | #else 17 | #define LORA_DEFAULT_SPI SPI 18 | #define LORA_DEFAULT_SPI_FREQUENCY 8E6 19 | #define LORA_DEFAULT_SS_PIN 10 20 | #define LORA_DEFAULT_RESET_PIN 9 21 | #define LORA_DEFAULT_DIO0_PIN 2 22 | #endif 23 | 24 | #define PA_OUTPUT_RFO_PIN 0 25 | #define PA_OUTPUT_PA_BOOST_PIN 1 26 | 27 | class LoRaClass : public Stream { 28 | public: 29 | LoRaClass(); 30 | 31 | int begin(long frequency); 32 | void end(); 33 | 34 | int beginPacket(int implicitHeader = false); 35 | int endPacket(bool async = false); 36 | 37 | int parsePacket(int size = 0); 38 | int packetRssi(); 39 | float packetSnr(); 40 | long packetFrequencyError(); 41 | 42 | // from Print 43 | virtual size_t write(uint8_t byte); 44 | virtual size_t write(const uint8_t *buffer, size_t size); 45 | 46 | // from Stream 47 | virtual int available(); 48 | virtual int read(); 49 | virtual int peek(); 50 | virtual void flush(); 51 | 52 | #ifndef ARDUINO_SAMD_MKRWAN1300 53 | void onReceive(void(*callback)(int)); 54 | 55 | void receive(int size = 0); 56 | #endif 57 | void idle(); 58 | void sleep(); 59 | 60 | void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN); 61 | void setFrequency(long frequency); 62 | void setSpreadingFactor(int sf); 63 | // void setSignalBandwidth(long sbw, long frequency); 64 | void setSignalBandwidth(long sbw); 65 | void setCodingRate4(int denominator); 66 | void setPreambleLength(long length); 67 | void setSyncWord(int sw); 68 | void enableCrc(); 69 | void disableCrc(); 70 | void enableInvertIQ(); 71 | void disableInvertIQ(); 72 | 73 | void setOCP(uint8_t mA); // Over Current Protection control 74 | 75 | // deprecated 76 | void crc() { enableCrc(); } 77 | void noCrc() { disableCrc(); } 78 | 79 | byte random(); 80 | 81 | void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN); 82 | void setSPI(SPIClass& spi); 83 | void setSPIFrequency(uint32_t frequency); 84 | 85 | void dumpRegisters(Stream& out); 86 | 87 | private: 88 | void explicitHeaderMode(); 89 | void implicitHeaderMode(); 90 | 91 | void handleDio0Rise(); 92 | bool isTransmitting(); 93 | 94 | int getSpreadingFactor(); 95 | long getSignalBandwidth(); 96 | 97 | void setLdoFlag(); 98 | 99 | uint8_t readRegister(uint8_t address); 100 | void writeRegister(uint8_t address, uint8_t value); 101 | uint8_t singleTransfer(uint8_t address, uint8_t value); 102 | 103 | static void onDio0Rise(); 104 | 105 | private: 106 | SPISettings _spiSettings; 107 | SPIClass* _spi; 108 | int _ss; 109 | int _reset; 110 | int _dio0; 111 | long _frequency; 112 | int _packetIndex; 113 | int _implicitHeaderMode; 114 | void (*_onReceive)(int); 115 | }; 116 | 117 | extern LoRaClass LoRa; 118 | 119 | #endif 120 | -------------------------------------------------------------------------------- /src/lib/MSP.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MSP.cpp 3 | 4 | Copyright (c) 2017, Fabrizio Di Vittorio (fdivitto2013@gmail.com) 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | 21 | #include 22 | 23 | #include "MSP.h" 24 | 25 | 26 | void MSP::begin(Stream & stream, uint32_t timeout) 27 | { 28 | _stream = &stream; 29 | _timeout = timeout; 30 | } 31 | 32 | 33 | void MSP::reset() 34 | { 35 | _stream->flush(); 36 | while (_stream->available() > 0) 37 | _stream->read(); 38 | } 39 | 40 | void MSP::send(uint8_t messageID, void * payload, uint8_t size) 41 | { 42 | _stream->write('$'); 43 | _stream->write('M'); 44 | _stream->write('<'); 45 | _stream->write(size); 46 | _stream->write(messageID); 47 | uint8_t checksum = size ^ messageID; 48 | uint8_t * payloadPtr = (uint8_t*)payload; 49 | for (uint8_t i = 0; i < size; ++i) { 50 | uint8_t b = *(payloadPtr++); 51 | checksum ^= b; 52 | _stream->write(b); 53 | } 54 | _stream->write(checksum); 55 | } 56 | 57 | uint8_t MSP::crc8_dvb_s2(uint8_t crc, byte a) 58 | { 59 | crc ^= a; 60 | for (int ii = 0; ii < 8; ++ii) { 61 | if (crc & 0x80) { 62 | crc = (crc << 1) ^ 0xD5; 63 | } else { 64 | crc = crc << 1; 65 | } 66 | } 67 | return crc; 68 | } 69 | 70 | void MSP::send2(uint16_t messageID, void * payload, uint8_t size) // 255 chars max, out of V2 specs 71 | { 72 | uint8_t _crc = 0; 73 | uint8_t message[size + 9]; 74 | message[0] = '$'; 75 | message[1] = 'X'; 76 | message[2] = '<'; 77 | message[3] = 0; //flag 78 | message[4] = messageID; //function 79 | message[5] = messageID >> 8; 80 | message[6] = size; //payload size 81 | message[7] = size >> 8; 82 | for(uint8_t i = 3; i < 8; i++) { 83 | _crc = crc8_dvb_s2(_crc, message[i]); 84 | } 85 | //Start of Payload 86 | uint8_t * payloadPtr = (uint8_t*)payload; 87 | for (uint16_t i = 0; i < size; ++i) { 88 | message[i + 8] = *(payloadPtr++); 89 | _crc = crc8_dvb_s2(_crc, message[i + 8]); 90 | } 91 | message[size + 8] = _crc; 92 | _stream->write(message, sizeof(message)); 93 | } 94 | 95 | // timeout in milliseconds 96 | bool MSP::recv(uint8_t * messageID, void * payload, uint8_t maxSize, uint8_t * recvSize) 97 | { 98 | uint32_t t0 = millis(); 99 | 100 | while (1) { 101 | 102 | // read header 103 | while (_stream->available() < 6) 104 | if (millis() - t0 >= _timeout) 105 | return false; 106 | char header[3]; 107 | _stream->readBytes((char*)header, 3); 108 | 109 | // check header 110 | if (header[0] == '$' && header[1] == 'M' && header[2] == '>') { 111 | // header ok, read payload size 112 | *recvSize = _stream->read(); 113 | 114 | // read message ID (type) 115 | *messageID = _stream->read(); 116 | 117 | uint8_t checksumCalc = *recvSize ^ *messageID; 118 | 119 | // read payload 120 | uint8_t * payloadPtr = (uint8_t*)payload; 121 | uint8_t idx = 0; 122 | while (idx < *recvSize) { 123 | if (millis() - t0 >= _timeout) 124 | return false; 125 | if (_stream->available() > 0) { 126 | uint8_t b = _stream->read(); 127 | checksumCalc ^= b; 128 | if (idx < maxSize) 129 | *(payloadPtr++) = b; 130 | ++idx; 131 | } 132 | } 133 | // zero remaining bytes if *size < maxSize 134 | for (; idx < maxSize; ++idx) 135 | *(payloadPtr++) = 0; 136 | 137 | // read and check checksum 138 | while (_stream->available() == 0) 139 | if (millis() - t0 >= _timeout) 140 | return false; 141 | uint8_t checksum = _stream->read(); 142 | if (checksumCalc == checksum) { 143 | return true; 144 | } 145 | 146 | } 147 | } 148 | 149 | } 150 | 151 | 152 | bool MSP::recv2(uint16_t * messageID, void * payload, uint8_t maxSize, uint8_t * recvSize) 153 | { 154 | uint32_t t0 = millis(); 155 | 156 | while (1) { 157 | 158 | // read header 159 | while (_stream->available() < 6) 160 | if (millis() - t0 >= _timeout) 161 | return false; 162 | char header[4]; 163 | _stream->readBytes((char*)header, 4); 164 | 165 | // check header 166 | if (header[0] == '$' && header[1] == 'X' && header[2] == '>') { 167 | 168 | // read message ID (type) 169 | *messageID = _stream->read(); 170 | 171 | 172 | // header ok, read payload size 173 | *recvSize = _stream->read(); 174 | 175 | 176 | 177 | // read payload 178 | uint8_t * payloadPtr = (uint8_t*)payload; 179 | uint8_t idx = 0; 180 | while (idx < *recvSize) { 181 | if (millis() - t0 >= _timeout) 182 | return false; 183 | if (_stream->available() > 0) { 184 | uint8_t b = _stream->read(); 185 | 186 | if (idx < maxSize) 187 | *(payloadPtr++) = b; 188 | ++idx; 189 | } 190 | } 191 | // zero remaining bytes if *size < maxSize 192 | for (; idx < maxSize; ++idx) 193 | *(payloadPtr++) = 0; 194 | 195 | 196 | 197 | return true; 198 | 199 | 200 | 201 | } 202 | } 203 | 204 | } 205 | 206 | 207 | // wait for messageID 208 | // recvSize can be NULL 209 | bool MSP::waitFor(uint8_t messageID, void * payload, uint8_t maxSize, uint8_t * recvSize) 210 | { 211 | uint8_t recvMessageID; 212 | uint8_t recvSizeValue; 213 | uint32_t t0 = millis(); 214 | while (millis() - t0 < _timeout) 215 | if (recv(&recvMessageID, payload, maxSize, (recvSize ? recvSize : &recvSizeValue)) && messageID == recvMessageID) 216 | return true; 217 | 218 | // timeout 219 | return false; 220 | } 221 | 222 | bool MSP::waitFor2(uint16_t messageID, void * payload, uint8_t maxSize, uint8_t * recvSize) 223 | { 224 | uint16_t recvMessageID; 225 | uint8_t recvSizeValue; 226 | uint32_t t0 = millis(); 227 | while (millis() - t0 < _timeout) 228 | if (recv2(&recvMessageID, payload, maxSize, (recvSize ? recvSize : &recvSizeValue)) && messageID == recvMessageID) 229 | return true; 230 | 231 | return false; 232 | } 233 | 234 | // send a message and wait for the reply 235 | // recvSize can be NULL 236 | bool MSP::request(uint8_t messageID, void * payload, uint8_t maxSize, uint8_t * recvSize) 237 | { 238 | send(messageID, NULL, 0); 239 | return waitFor(messageID, payload, maxSize, recvSize); 240 | } 241 | 242 | 243 | // send message and wait for ack 244 | bool MSP::command(uint8_t messageID, void * payload, uint8_t size, bool waitACK) 245 | { 246 | send(messageID, payload, size); 247 | 248 | // ack required 249 | if (waitACK) 250 | return waitFor(messageID, NULL, 0); 251 | 252 | return true; 253 | } 254 | 255 | bool MSP::command2(uint16_t messageID, void * payload, uint8_t size, bool waitACK) 256 | { 257 | send2(messageID, payload, size); 258 | 259 | // ack required 260 | if (waitACK) 261 | return waitFor2(messageID, NULL, 0); 262 | 263 | return true; 264 | } 265 | 266 | // map MSP_MODE_xxx to box ids 267 | // mixed values from cleanflight and inav 268 | static const uint8_t BOXIDS[30] PROGMEM = { 269 | 0, // 0: MSP_MODE_ARM 270 | 1, // 1: MSP_MODE_ANGLE 271 | 2, // 2: MSP_MODE_HORIZON 272 | 3, // 3: MSP_MODE_NAVALTHOLD (cleanflight BARO) 273 | 5, // 4: MSP_MODE_MAG 274 | 6, // 5: MSP_MODE_HEADFREE 275 | 7, // 6: MSP_MODE_HEADADJ 276 | 8, // 7: MSP_MODE_CAMSTAB 277 | 10, // 8: MSP_MODE_NAVRTH (cleanflight GPSHOME) 278 | 11, // 9: MSP_MODE_NAVPOSHOLD (cleanflight GPSHOLD) 279 | 12, // 10: MSP_MODE_PASSTHRU 280 | 13, // 11: MSP_MODE_BEEPERON 281 | 15, // 12: MSP_MODE_LEDLOW 282 | 16, // 13: MSP_MODE_LLIGHTS 283 | 19, // 14: MSP_MODE_OSD 284 | 20, // 15: MSP_MODE_TELEMETRY 285 | 21, // 16: MSP_MODE_GTUNE 286 | 22, // 17: MSP_MODE_SONAR 287 | 26, // 18: MSP_MODE_BLACKBOX 288 | 27, // 19: MSP_MODE_FAILSAFE 289 | 28, // 20: MSP_MODE_NAVWP (cleanflight AIRMODE) 290 | 29, // 21: MSP_MODE_AIRMODE (cleanflight DISABLE3DSWITCH) 291 | 30, // 22: MSP_MODE_HOMERESET (cleanflight FPVANGLEMIX) 292 | 31, // 23: MSP_MODE_GCSNAV (cleanflight BLACKBOXERASE) 293 | 32, // 24: MSP_MODE_HEADINGLOCK 294 | 33, // 25: MSP_MODE_SURFACE 295 | 34, // 26: MSP_MODE_FLAPERON 296 | 35, // 27: MSP_MODE_TURNASSIST 297 | 36, // 28: MSP_MODE_NAVLAUNCH 298 | 37, // 29: MSP_MODE_AUTOTRIM 299 | }; 300 | 301 | 302 | // returns active mode (using MSP_STATUS and MSP_BOXIDS messages) 303 | // see MSP_MODE_... for bits inside activeModes 304 | bool MSP::getActiveModes(uint32_t * activeModes) 305 | { 306 | // request status ex 307 | msp_status_t status; 308 | if (request(MSP_STATUS, &status, sizeof(status))) { 309 | // request permanent ids associated to boxes 310 | uint8_t ids[sizeof(BOXIDS)]; 311 | uint8_t recvSize; 312 | if (request(MSP_BOXIDS, ids, sizeof(ids), &recvSize)) { 313 | // compose activeModes, converting BOXIDS to bit map (setting 1 if related flag in flightModeFlags is set) 314 | *activeModes = 0; 315 | for (uint8_t i = 0; i < recvSize; ++i) { 316 | if (status.flightModeFlags & (1 << i)) { 317 | for (uint8_t j = 0; j < sizeof(BOXIDS); ++j) { 318 | if (pgm_read_byte(BOXIDS + j) == ids[i]) { 319 | *activeModes |= 1 << j; 320 | break; 321 | } 322 | } 323 | } 324 | } 325 | return true; 326 | } 327 | } 328 | 329 | return false; 330 | } 331 | -------------------------------------------------------------------------------- /src/lib/MSP.h: -------------------------------------------------------------------------------- 1 | /* 2 | MSP.h 3 | 4 | Copyright (c) 2017, Fabrizio Di Vittorio (fdivitto2013@gmail.com) 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | 21 | 22 | #pragma once 23 | 24 | #include 25 | #include 26 | 27 | // requests & replies 28 | #define MSP_API_VERSION 1 29 | #define MSP_FC_VARIANT 2 30 | #define MSP_FC_VERSION 3 31 | #define MSP_BOARD_INFO 4 32 | #define MSP_BUILD_INFO 5 33 | #define MSP_NAME 10 //out message Returns user set board name - betaflight 34 | #define MSP_CALIBRATION_DATA 14 35 | #define MSP_FEATURE 36 36 | #define MSP_BOARD_ALIGNMENT 38 37 | #define MSP_CURRENT_METER_CONFIG 40 38 | #define MSP_RX_CONFIG 44 39 | #define MSP_SONAR_ALTITUDE 58 40 | #define MSP_ARMING_CONFIG 61 41 | #define MSP_RX_MAP 64 // get channel map (also returns number of channels total) 42 | #define MSP_LOOP_TIME 73 // FC cycle time i.e looptime parameter 43 | #define MSP_STATUS 101 44 | #define MSP_RAW_IMU 102 45 | #define MSP_SERVO 103 46 | #define MSP_MOTOR 104 47 | #define MSP_RC 105 48 | #define MSP_RAW_GPS 106 49 | #define MSP_COMP_GPS 107 // distance home, direction home 50 | #define MSP_ATTITUDE 108 51 | #define MSP_ALTITUDE 109 52 | #define MSP_ANALOG 110 53 | #define MSP_RC_TUNING 111 // rc rate, rc expo, rollpitch rate, yaw rate, dyn throttle PID 54 | #define MSP_PID 112 // P I D coeff 55 | #define MSP_MISC 114 56 | #define MSP_SERVO_CONFIGURATIONS 120 57 | #define MSP_NAV_STATUS 121 // navigation status 58 | #define MSP_SENSOR_ALIGNMENT 126 // orientation of acc,gyro,mag 59 | #define MSP_STATUS_EX 150 60 | #define MSP_SENSOR_STATUS 151 61 | #define MSP_BOXIDS 119 62 | #define MSP_UID 160 // Unique device ID 63 | #define MSP_GPSSVINFO 164 // get Signal Strength (only U-Blox) 64 | #define MSP_GPSSTATISTICS 166 // get GPS debugging data 65 | #define MSP_SET_PID 202 // set P I D coeff 66 | 67 | 68 | // commands 69 | #define MSP_SET_HEAD 211 // define a new heading hold direction 70 | #define MSP_SET_RAW_RC 200 // 8 rc chan 71 | #define MSP_SET_RAW_GPS 201 // fix, numsat, lat, lon, alt, speed 72 | #define MSP_SET_WP 209 // sets a given WP (WP#, lat, lon, alt, flags) 73 | 74 | // radar commands 75 | #define MSP_SET_RADAR_POS 248 //SET radar position information 76 | #define MSP_SET_RADAR_ITD 249 //SET radar information to display 77 | 78 | // v2 commands 79 | #define MSP2_ESP32 0x2040 80 | 81 | #define MSP2_COMMON_SET_RADAR_POS 0x100B //SET radar position information 82 | #define MSP2_COMMON_SET_RADAR_ITD 0x100C //SET radar information to display 83 | 84 | // bits of getActiveModes() return value 85 | #define MSP_MODE_ARM 0 86 | #define MSP_MODE_ANGLE 1 87 | #define MSP_MODE_HORIZON 2 88 | #define MSP_MODE_NAVALTHOLD 3 /* cleanflight BARO */ 89 | #define MSP_MODE_MAG 4 90 | #define MSP_MODE_HEADFREE 5 91 | #define MSP_MODE_HEADADJ 6 92 | #define MSP_MODE_CAMSTAB 7 93 | #define MSP_MODE_NAVRTH 8 /* cleanflight GPSHOME */ 94 | #define MSP_MODE_NAVPOSHOLD 9 /* cleanflight GPSHOLD */ 95 | #define MSP_MODE_PASSTHRU 10 96 | #define MSP_MODE_BEEPERON 11 97 | #define MSP_MODE_LEDLOW 12 98 | #define MSP_MODE_LLIGHTS 13 99 | #define MSP_MODE_OSD 14 100 | #define MSP_MODE_TELEMETRY 15 101 | #define MSP_MODE_GTUNE 16 102 | #define MSP_MODE_SONAR 17 103 | #define MSP_MODE_BLACKBOX 18 104 | #define MSP_MODE_FAILSAFE 19 105 | #define MSP_MODE_NAVWP 20 /* cleanflight AIRMODE */ 106 | #define MSP_MODE_AIRMODE 21 /* cleanflight DISABLE3DSWITCH */ 107 | #define MSP_MODE_HOMERESET 22 /* cleanflight FPVANGLEMIX */ 108 | #define MSP_MODE_GCSNAV 23 /* cleanflight BLACKBOXERASE */ 109 | #define MSP_MODE_HEADINGLOCK 24 110 | #define MSP_MODE_SURFACE 25 111 | #define MSP_MODE_FLAPERON 26 112 | #define MSP_MODE_TURNASSIST 27 113 | #define MSP_MODE_NAVLAUNCH 28 114 | #define MSP_MODE_AUTOTRIM 29 115 | 116 | 117 | // MSP_API_VERSION reply 118 | struct msp_api_version_t { 119 | uint8_t protocolVersion; 120 | uint8_t APIMajor; 121 | uint8_t APIMinor; 122 | } __attribute__ ((packed)); 123 | 124 | 125 | // MSP_FC_VARIANT reply 126 | struct msp_fc_variant_t { 127 | char flightControlIdentifier[4]; 128 | } __attribute__ ((packed)); 129 | 130 | 131 | // MSP_FC_VERSION reply 132 | struct msp_fc_version_t { 133 | uint8_t versionMajor; 134 | uint8_t versionMinor; 135 | uint8_t versionPatchLevel; 136 | } __attribute__ ((packed)); 137 | 138 | 139 | // MSP_BOARD_INFO reply 140 | struct msp_board_info_t { 141 | char boardIdentifier[4]; 142 | uint16_t hardwareRevision; 143 | } __attribute__ ((packed)); 144 | 145 | 146 | // MSP_BUILD_INFO reply 147 | struct msp_build_info_t { 148 | char buildDate[11]; 149 | char buildTime[8]; 150 | char shortGitRevision[7]; 151 | } __attribute__ ((packed)); 152 | 153 | 154 | // MSP_RAW_IMU reply 155 | struct msp_raw_imu_t { 156 | int16_t acc[3]; // x, y, z 157 | int16_t gyro[3]; // x, y, z 158 | int16_t mag[3]; // x, y, z 159 | } __attribute__ ((packed)); 160 | 161 | 162 | // flags for msp_status_ex_t.sensor and msp_status_t.sensor 163 | #define MSP_STATUS_SENSOR_ACC 1 164 | #define MSP_STATUS_SENSOR_BARO 2 165 | #define MSP_STATUS_SENSOR_MAG 4 166 | #define MSP_STATUS_SENSOR_GPS 8 167 | #define MSP_STATUS_SENSOR_SONAR 16 168 | 169 | 170 | // MSP_STATUS_EX reply 171 | struct msp_status_ex_t { 172 | uint16_t cycleTime; 173 | uint16_t i2cErrorCounter; 174 | uint16_t sensor; // MSP_STATUS_SENSOR_... 175 | uint32_t flightModeFlags; // see getActiveModes() 176 | uint8_t configProfileIndex; 177 | uint16_t averageSystemLoadPercent; // 0...100 178 | uint16_t armingFlags; 179 | uint8_t accCalibrationAxisFlags; 180 | } __attribute__ ((packed)); 181 | 182 | 183 | // MSP_STATUS 184 | struct msp_status_t { 185 | uint16_t cycleTime; 186 | uint16_t i2cErrorCounter; 187 | uint16_t sensor; // MSP_STATUS_SENSOR_... 188 | uint32_t flightModeFlags; // see getActiveModes() 189 | uint8_t configProfileIndex; 190 | } __attribute__ ((packed)); 191 | 192 | 193 | // MSP_SENSOR_STATUS reply 194 | struct msp_sensor_status_t { 195 | uint8_t isHardwareHealthy; // 0...1 196 | uint8_t hwGyroStatus; 197 | uint8_t hwAccelerometerStatus; 198 | uint8_t hwCompassStatus; 199 | uint8_t hwBarometerStatus; 200 | uint8_t hwGPSStatus; 201 | uint8_t hwRangefinderStatus; 202 | uint8_t hwPitotmeterStatus; 203 | uint8_t hwOpticalFlowStatus; 204 | } __attribute__ ((packed)); 205 | 206 | 207 | #define MSP_MAX_SUPPORTED_SERVOS 8 208 | 209 | // MSP_SERVO reply 210 | struct msp_servo_t { 211 | uint16_t servo[MSP_MAX_SUPPORTED_SERVOS]; 212 | } __attribute__ ((packed)); 213 | 214 | 215 | // MSP_SERVO_CONFIGURATIONS reply 216 | struct msp_servo_configurations_t { 217 | __attribute__ ((packed)) struct { 218 | uint16_t min; 219 | uint16_t max; 220 | uint16_t middle; 221 | uint8_t rate; 222 | uint8_t angleAtMin; 223 | uint8_t angleAtMax; 224 | uint8_t forwardFromChannel; 225 | uint32_t reversedSources; 226 | } conf[MSP_MAX_SUPPORTED_SERVOS]; 227 | } __attribute__ ((packed)); 228 | 229 | 230 | /* 231 | #define MSP_MAX_SERVO_RULES (2 * MSP_MAX_SUPPORTED_SERVOS) 232 | 233 | 234 | 235 | // MSP_SERVO_MIX_RULES reply 236 | struct msp_servo_mix_rules_t { 237 | __attribute__ ((packed)) struct { 238 | uint8_t targetChannel; 239 | uint8_t inputSource; 240 | uint8_t rate; 241 | uint8_t speed; 242 | uint8_t min; 243 | uint8_t max; 244 | } mixRule[MSP_MAX_SERVO_RULES]; 245 | } __attribute__ ((packed)); 246 | */ 247 | 248 | #define MSP_MAX_SUPPORTED_MOTORS 8 249 | 250 | // MSP_MOTOR reply 251 | struct msp_motor_t { 252 | uint16_t motor[MSP_MAX_SUPPORTED_MOTORS]; 253 | } __attribute__ ((packed)); 254 | 255 | 256 | #define MSP_MAX_SUPPORTED_CHANNELS 16 257 | 258 | // MSP_RC reply 259 | struct msp_rc_t { 260 | uint16_t channelValue[MSP_MAX_SUPPORTED_CHANNELS]; 261 | } __attribute__ ((packed)); 262 | 263 | 264 | // MSP_ATTITUDE reply 265 | struct msp_attitude_t { 266 | int16_t roll; 267 | int16_t pitch; 268 | int16_t yaw; 269 | } __attribute__ ((packed)); 270 | 271 | 272 | // MSP_ALTITUDE reply 273 | struct msp_altitude_t { 274 | int32_t estimatedActualPosition; // cm 275 | int16_t estimatedActualVelocity; // cm/s 276 | int32_t baroLatestAltitude; 277 | } __attribute__ ((packed)); 278 | 279 | 280 | // MSP_SONAR_ALTITUDE reply 281 | struct msp_sonar_altitude_t { 282 | int32_t altitude; 283 | } __attribute__ ((packed)); 284 | 285 | 286 | // MSP_ANALOG reply 287 | struct msp_analog_t { 288 | uint8_t vbat; // 0...255 289 | uint16_t mAhDrawn; // milliamp hours drawn from battery 290 | uint16_t rssi; // 0..1023 291 | int16_t amperage; // send amperage in 0.01 A steps, range is -320A to 320A 292 | } __attribute__ ((packed)); 293 | 294 | 295 | // MSP_ARMING_CONFIG reply 296 | struct msp_arming_config_t { 297 | uint8_t auto_disarm_delay; 298 | uint8_t disarm_kill_switch; 299 | } __attribute__ ((packed)); 300 | 301 | 302 | // MSP_LOOP_TIME reply 303 | struct msp_loop_time_t { 304 | uint16_t looptime; 305 | } __attribute__ ((packed)); 306 | 307 | 308 | // MSP_RC_TUNING reply 309 | struct msp_rc_tuning_t { 310 | uint8_t rcRate8; // no longer used 311 | uint8_t rcExpo8; 312 | uint8_t rates[3]; // R,P,Y 313 | uint8_t dynThrPID; 314 | uint8_t thrMid8; 315 | uint8_t thrExpo8; 316 | uint16_t tpa_breakpoint; 317 | uint8_t rcYawExpo8; 318 | } __attribute__ ((packed)); 319 | 320 | 321 | // MSP_PID reply 322 | struct msp_pid_t { 323 | uint8_t roll[3]; // 0=P, 1=I, 2=D 324 | uint8_t pitch[3]; // 0=P, 1=I, 2=D 325 | uint8_t yaw[3]; // 0=P, 1=I, 2=D 326 | uint8_t pos_z[3]; // 0=P, 1=I, 2=D 327 | uint8_t pos_xy[3]; // 0=P, 1=I, 2=D 328 | uint8_t vel_xy[3]; // 0=P, 1=I, 2=D 329 | uint8_t surface[3]; // 0=P, 1=I, 2=D 330 | uint8_t level[3]; // 0=P, 1=I, 2=D 331 | uint8_t heading[3]; // 0=P, 1=I, 2=D 332 | uint8_t vel_z[3]; // 0=P, 1=I, 2=D 333 | } __attribute__ ((packed)); 334 | 335 | 336 | // MSP_MISC reply 337 | struct msp_misc_t { 338 | uint16_t midrc; 339 | uint16_t minthrottle; 340 | uint16_t maxthrottle; 341 | uint16_t mincommand; 342 | uint16_t failsafe_throttle; 343 | uint8_t gps_provider; 344 | uint8_t gps_baudrate; 345 | uint8_t gps_ubx_sbas; 346 | uint8_t multiwiiCurrentMeterOutput; 347 | uint8_t rssi_channel; 348 | uint8_t dummy; 349 | uint16_t mag_declination; 350 | uint8_t vbatscale; 351 | uint8_t vbatmincellvoltage; 352 | uint8_t vbatmaxcellvoltage; 353 | uint8_t vbatwarningcellvoltage; 354 | } __attribute__ ((packed)); 355 | 356 | 357 | // values for msp_raw_gps_t.fixType 358 | #define MSP_GPS_NO_FIX 0 359 | #define MSP_GPS_FIX_2D 1 360 | #define MSP_GPS_FIX_3D 2 361 | 362 | 363 | // MSP_RAW_GPS reply 364 | struct msp_raw_gps_t { 365 | uint8_t fixType; // MSP_GPS_NO_FIX, MSP_GPS_FIX_2D, MSP_GPS_FIX_3D 366 | uint8_t numSat; 367 | int32_t lat; // 1 / 10000000 deg 368 | int32_t lon; // 1 / 10000000 deg 369 | int16_t alt; // meters 370 | int16_t groundSpeed; // cm/s 371 | int16_t groundCourse; // unit: degree x 10 372 | uint16_t hdop; 373 | } __attribute__ ((packed)); 374 | 375 | 376 | // MSP_COMP_GPS reply 377 | struct msp_comp_gps_t { 378 | int16_t distanceToHome; // distance to home in meters 379 | int16_t directionToHome; // direction to home in degrees 380 | uint8_t heartbeat; // toggles 0 and 1 for each change 381 | } __attribute__ ((packed)); 382 | 383 | 384 | // values for msp_nav_status_t.mode 385 | #define MSP_NAV_STATUS_MODE_NONE 0 386 | #define MSP_NAV_STATUS_MODE_HOLD 1 387 | #define MSP_NAV_STATUS_MODE_RTH 2 388 | #define MSP_NAV_STATUS_MODE_NAV 3 389 | #define MSP_NAV_STATUS_MODE_EMERG 15 390 | 391 | // values for msp_nav_status_t.state 392 | #define MSP_NAV_STATUS_STATE_NONE 0 // None 393 | #define MSP_NAV_STATUS_STATE_RTH_START 1 // RTH Start 394 | #define MSP_NAV_STATUS_STATE_RTH_ENROUTE 2 // RTH Enroute 395 | #define MSP_NAV_STATUS_STATE_HOLD_INFINIT 3 // PosHold infinit 396 | #define MSP_NAV_STATUS_STATE_HOLD_TIMED 4 // PosHold timed 397 | #define MSP_NAV_STATUS_STATE_WP_ENROUTE 5 // WP Enroute 398 | #define MSP_NAV_STATUS_STATE_PROCESS_NEXT 6 // Process next 399 | #define MSP_NAV_STATUS_STATE_DO_JUMP 7 // Jump 400 | #define MSP_NAV_STATUS_STATE_LAND_START 8 // Start Land 401 | #define MSP_NAV_STATUS_STATE_LAND_IN_PROGRESS 9 // Land in Progress 402 | #define MSP_NAV_STATUS_STATE_LANDED 10 // Landed 403 | #define MSP_NAV_STATUS_STATE_LAND_SETTLE 11 // Settling before land 404 | #define MSP_NAV_STATUS_STATE_LAND_START_DESCENT 12 // Start descent 405 | 406 | // values for msp_nav_status_t.activeWpAction, msp_set_wp_t.action 407 | #define MSP_NAV_STATUS_WAYPOINT_ACTION_WAYPOINT 0x01 408 | #define MSP_NAV_STATUS_WAYPOINT_ACTION_RTH 0x04 409 | 410 | // values for msp_nav_status_t.error 411 | #define MSP_NAV_STATUS_ERROR_NONE 0 // All systems clear 412 | #define MSP_NAV_STATUS_ERROR_TOOFAR 1 // Next waypoint distance is more than safety distance 413 | #define MSP_NAV_STATUS_ERROR_SPOILED_GPS 2 // GPS reception is compromised - Nav paused - copter is adrift ! 414 | #define MSP_NAV_STATUS_ERROR_WP_CRC 3 // CRC error reading WP data from EEPROM - Nav stopped 415 | #define MSP_NAV_STATUS_ERROR_FINISH 4 // End flag detected, navigation finished 416 | #define MSP_NAV_STATUS_ERROR_TIMEWAIT 5 // Waiting for poshold timer 417 | #define MSP_NAV_STATUS_ERROR_INVALID_JUMP 6 // Invalid jump target detected, aborting 418 | #define MSP_NAV_STATUS_ERROR_INVALID_DATA 7 // Invalid mission step action code, aborting, copter is adrift 419 | #define MSP_NAV_STATUS_ERROR_WAIT_FOR_RTH_ALT 8 // Waiting to reach RTH Altitude 420 | #define MSP_NAV_STATUS_ERROR_GPS_FIX_LOST 9 // Gps fix lost, aborting mission 421 | #define MSP_NAV_STATUS_ERROR_DISARMED 10 // NAV engine disabled due disarm 422 | #define MSP_NAV_STATUS_ERROR_LANDING 11 // Landing 423 | 424 | 425 | // MSP_NAV_STATUS reply 426 | struct msp_nav_status_t { 427 | uint8_t mode; // one of MSP_NAV_STATUS_MODE_XXX 428 | uint8_t state; // one of MSP_NAV_STATUS_STATE_XXX 429 | uint8_t activeWpAction; // combination of MSP_NAV_STATUS_WAYPOINT_ACTION_XXX 430 | uint8_t activeWpNumber; 431 | uint8_t error; // one of MSP_NAV_STATUS_ERROR_XXX 432 | int16_t magHoldHeading; 433 | } __attribute__ ((packed)); 434 | 435 | 436 | // MSP_GPSSVINFO reply 437 | struct msp_gpssvinfo_t { 438 | uint8_t dummy1; 439 | uint8_t dummy2; 440 | uint8_t dummy3; 441 | uint8_t dummy4; 442 | uint8_t HDOP; 443 | } __attribute__ ((packed)); 444 | 445 | 446 | // MSP_GPSSTATISTICS reply 447 | struct msp_gpsstatistics_t { 448 | uint16_t lastMessageDt; 449 | uint32_t errors; 450 | uint32_t timeouts; 451 | uint32_t packetCount; 452 | uint16_t hdop; 453 | uint16_t eph; 454 | uint16_t epv; 455 | } __attribute__ ((packed)); 456 | 457 | 458 | // MSP_UID reply 459 | struct msp_uid_t { 460 | uint32_t uid0; 461 | uint32_t uid1; 462 | uint32_t uid2; 463 | } __attribute__ ((packed)); 464 | 465 | 466 | // MSP_FEATURE mask 467 | #define MSP_FEATURE_RX_PPM (1 << 0) 468 | #define MSP_FEATURE_VBAT (1 << 1) 469 | #define MSP_FEATURE_UNUSED_1 (1 << 2) 470 | #define MSP_FEATURE_RX_SERIAL (1 << 3) 471 | #define MSP_FEATURE_MOTOR_STOP (1 << 4) 472 | #define MSP_FEATURE_SERVO_TILT (1 << 5) 473 | #define MSP_FEATURE_SOFTSERIAL (1 << 6) 474 | #define MSP_FEATURE_GPS (1 << 7) 475 | #define MSP_FEATURE_UNUSED_3 (1 << 8) // was FEATURE_FAILSAFE 476 | #define MSP_FEATURE_UNUSED_4 (1 << 9) // was FEATURE_SONAR 477 | #define MSP_FEATURE_TELEMETRY (1 << 10) 478 | #define MSP_FEATURE_CURRENT_METER (1 << 11) 479 | #define MSP_FEATURE_3D (1 << 12) 480 | #define MSP_FEATURE_RX_PARALLEL_PWM (1 << 13) 481 | #define MSP_FEATURE_RX_MSP (1 << 14) 482 | #define MSP_FEATURE_RSSI_ADC (1 << 15) 483 | #define MSP_FEATURE_LED_STRIP (1 << 16) 484 | #define MSP_FEATURE_DASHBOARD (1 << 17) 485 | #define MSP_FEATURE_UNUSED_2 (1 << 18) 486 | #define MSP_FEATURE_BLACKBOX (1 << 19) 487 | #define MSP_FEATURE_CHANNEL_FORWARDING (1 << 20) 488 | #define MSP_FEATURE_TRANSPONDER (1 << 21) 489 | #define MSP_FEATURE_AIRMODE (1 << 22) 490 | #define MSP_FEATURE_SUPEREXPO_RATES (1 << 23) 491 | #define MSP_FEATURE_VTX (1 << 24) 492 | #define MSP_FEATURE_RX_SPI (1 << 25) 493 | #define MSP_FEATURE_SOFTSPI (1 << 26) 494 | #define MSP_FEATURE_PWM_SERVO_DRIVER (1 << 27) 495 | #define MSP_FEATURE_PWM_OUTPUT_ENABLE (1 << 28) 496 | #define MSP_FEATURE_OSD (1 << 29) 497 | 498 | 499 | // MSP_FEATURE reply 500 | struct msp_feature_t { 501 | uint32_t featureMask; // combination of MSP_FEATURE_XXX 502 | } __attribute__ ((packed)); 503 | 504 | 505 | // MSP_BOARD_ALIGNMENT reply 506 | struct msp_board_alignment_t { 507 | int16_t rollDeciDegrees; 508 | int16_t pitchDeciDegrees; 509 | int16_t yawDeciDegrees; 510 | } __attribute__ ((packed)); 511 | 512 | 513 | // values for msp_current_meter_config_t.currentMeterType 514 | #define MSP_CURRENT_SENSOR_NONE 0 515 | #define MSP_CURRENT_SENSOR_ADC 1 516 | #define MSP_CURRENT_SENSOR_VIRTUAL 2 517 | #define MSP_CURRENT_SENSOR_MAX CURRENT_SENSOR_VIRTUAL 518 | 519 | 520 | // MSP_CURRENT_METER_CONFIG reply 521 | struct msp_current_meter_config_t { 522 | int16_t currentMeterScale; 523 | int16_t currentMeterOffset; 524 | uint8_t currentMeterType; // MSP_CURRENT_SENSOR_XXX 525 | uint16_t batteryCapacity; 526 | } __attribute__ ((packed)); 527 | 528 | 529 | // msp_rx_config_t.serialrx_provider 530 | #define MSP_SERIALRX_SPEKTRUM1024 0 531 | #define MSP_SERIALRX_SPEKTRUM2048 1 532 | #define MSP_SERIALRX_SBUS 2 533 | #define MSP_SERIALRX_SUMD 3 534 | #define MSP_SERIALRX_SUMH 4 535 | #define MSP_SERIALRX_XBUS_MODE_B 5 536 | #define MSP_SERIALRX_XBUS_MODE_B_RJ01 6 537 | #define MSP_SERIALRX_IBUS 7 538 | #define MSP_SERIALRX_JETIEXBUS 8 539 | #define MSP_SERIALRX_CRSF 9 540 | 541 | 542 | // msp_rx_config_t.rx_spi_protocol values 543 | #define MSP_SPI_PROT_NRF24RX_V202_250K 0 544 | #define MSP_SPI_PROT_NRF24RX_V202_1M 1 545 | #define MSP_SPI_PROT_NRF24RX_SYMA_X 2 546 | #define MSP_SPI_PROT_NRF24RX_SYMA_X5C 3 547 | #define MSP_SPI_PROT_NRF24RX_CX10 4 548 | #define MSP_SPI_PROT_NRF24RX_CX10A 5 549 | #define MSP_SPI_PROT_NRF24RX_H8_3D 6 550 | #define MSP_SPI_PROT_NRF24RX_INAV 7 551 | 552 | 553 | // MSP_RX_CONFIG reply 554 | struct msp_rx_config_t { 555 | uint8_t serialrx_provider; // one of MSP_SERIALRX_XXX values 556 | uint16_t maxcheck; 557 | uint16_t midrc; 558 | uint16_t mincheck; 559 | uint8_t spektrum_sat_bind; 560 | uint16_t rx_min_usec; 561 | uint16_t rx_max_usec; 562 | uint8_t dummy1; 563 | uint8_t dummy2; 564 | uint16_t dummy3; 565 | uint8_t rx_spi_protocol; // one of MSP_SPI_PROT_XXX values 566 | uint32_t rx_spi_id; 567 | uint8_t rx_spi_rf_channel_count; 568 | } __attribute__ ((packed)); 569 | 570 | 571 | #define MSP_MAX_MAPPABLE_RX_INPUTS 8 572 | 573 | // MSP_RX_MAP reply 574 | struct msp_rx_map_t { 575 | uint8_t rxmap[MSP_MAX_MAPPABLE_RX_INPUTS]; // [0]=roll channel, [1]=pitch channel, [2]=yaw channel, [3]=throttle channel, [3+n]=aux n channel, etc... 576 | } __attribute__ ((packed)); 577 | 578 | 579 | // values for msp_sensor_alignment_t.gyro_align, acc_align, mag_align 580 | #define MSP_SENSOR_ALIGN_CW0_DEG 1 581 | #define MSP_SENSOR_ALIGN_CW90_DEG 2 582 | #define MSP_SENSOR_ALIGN_CW180_DEG 3 583 | #define MSP_SENSOR_ALIGN_CW270_DEG 4 584 | #define MSP_SENSOR_ALIGN_CW0_DEG_FLIP 5 585 | #define MSP_SENSOR_ALIGN_CW90_DEG_FLIP 6 586 | #define MSP_SENSOR_ALIGN_CW180_DEG_FLIP 7 587 | #define MSP_SENSOR_ALIGN_CW270_DEG_FLIP 8 588 | 589 | // MSP_SENSOR_ALIGNMENT reply 590 | struct msp_sensor_alignment_t { 591 | uint8_t gyro_align; // one of MSP_SENSOR_ALIGN_XXX 592 | uint8_t acc_align; // one of MSP_SENSOR_ALIGN_XXX 593 | uint8_t mag_align; // one of MSP_SENSOR_ALIGN_XXX 594 | } __attribute__ ((packed)); 595 | 596 | 597 | // MSP_CALIBRATION_DATA reply 598 | struct msp_calibration_data_t { 599 | int16_t accZeroX; 600 | int16_t accZeroY; 601 | int16_t accZeroZ; 602 | int16_t accGainX; 603 | int16_t accGainY; 604 | int16_t accGainZ; 605 | int16_t magZeroX; 606 | int16_t magZeroY; 607 | int16_t magZeroZ; 608 | } __attribute__ ((packed)); 609 | 610 | 611 | // MSP_SET_HEAD command 612 | struct msp_set_head_t { 613 | int16_t magHoldHeading; // degrees 614 | } __attribute__ ((packed)); 615 | 616 | 617 | // MSP_SET_RAW_RC command 618 | struct msp_set_raw_rc_t { 619 | uint16_t channel[MSP_MAX_SUPPORTED_CHANNELS]; 620 | } __attribute__ ((packed)); 621 | 622 | 623 | // MSP_SET_PID command 624 | typedef msp_pid_t msp_set_pid_t; 625 | 626 | 627 | // MSP_SET_RAW_GPS command 628 | struct msp_set_raw_gps_t { 629 | uint8_t fixType; // MSP_GPS_NO_FIX, MSP_GPS_FIX_2D, MSP_GPS_FIX_3D 630 | uint8_t numSat; 631 | int32_t lat; // 1 / 10000000 deg 632 | int32_t lon; // 1 / 10000000 deg 633 | int16_t alt; // meters 634 | int16_t groundSpeed; // cm/s 635 | } __attribute__ ((packed)); 636 | 637 | 638 | // MSP_SET_WP command 639 | // Special waypoints are 0 and 255. 0 is the RTH position, 255 is the POSHOLD position (lat, lon, alt). 640 | struct msp_set_wp_t { 641 | uint8_t waypointNumber; 642 | uint8_t action; // one of MSP_NAV_STATUS_WAYPOINT_ACTION_XXX 643 | int32_t lat; // decimal degrees latitude * 10000000 644 | int32_t lon; // decimal degrees longitude * 10000000 645 | int32_t alt; // altitude (cm) 646 | int16_t p1; // speed (cm/s) when action is MSP_NAV_STATUS_WAYPOINT_ACTION_WAYPOINT, or "land" (value 1) when action is MSP_NAV_STATUS_WAYPOINT_ACTION_RTH 647 | int16_t p2; // not used 648 | int16_t p3; // not used 649 | uint8_t flag; // 0xa5 = last, otherwise set to 0 650 | } __attribute__ ((packed)); 651 | 652 | struct msp_radar_pos_t { 653 | uint8_t id; 654 | uint8_t state; // disarmed(0) armed (1) 655 | int32_t lat; // decimal degrees latitude * 10000000 656 | int32_t lon; // decimal degrees longitude * 10000000 657 | int32_t alt; // cm 658 | uint16_t heading; // deg 659 | uint16_t speed; // cm/s 660 | uint8_t lq; // lq 661 | } __attribute__((packed)); 662 | 663 | struct msp_radar_itd_t { 664 | uint8_t type; // pps / rssi (0), staus msg (1) 665 | char msg[20]; // "2/-85.2", "booting..." 666 | } __attribute__((packed)); 667 | 668 | ///////////////////////////////////////////////////////////////////////////////////////// 669 | ///////////////////////////////////////////////////////////////////////////////////////// 670 | 671 | class MSP { 672 | 673 | public: 674 | 675 | void begin(Stream & stream, uint32_t timeout = 100); 676 | 677 | // low level functions 678 | 679 | uint8_t crc8_dvb_s2(uint8_t crc, byte a); 680 | 681 | void send(uint8_t messageID, void * payload, uint8_t size); 682 | 683 | void send2(uint16_t messageID, void * payload, uint8_t size); 684 | 685 | bool recv(uint8_t * messageID, void * payload, uint8_t maxSize, uint8_t * recvSize); 686 | 687 | bool recv2(uint16_t * messageID, void * payload, uint8_t maxSize, uint8_t * recvSize); 688 | 689 | bool waitFor(uint8_t messageID, void * payload, uint8_t maxSize, uint8_t * recvSize = NULL); 690 | 691 | bool waitFor2(uint16_t messageID, void * payload, uint8_t maxSize, uint8_t * recvSize = NULL); 692 | 693 | bool request(uint8_t messageID, void * payload, uint8_t maxSize, uint8_t * recvSize = NULL); 694 | 695 | bool command(uint8_t messageID, void * payload, uint8_t size, bool waitACK = true); 696 | 697 | bool command2(uint16_t messageID, void * payload, uint8_t size, bool waitACK = true); 698 | 699 | void reset(); 700 | 701 | // high level functions 702 | 703 | bool getActiveModes(uint32_t * activeModes); 704 | 705 | 706 | private: 707 | 708 | Stream * _stream; 709 | uint32_t _timeout; 710 | 711 | }; 712 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // -------- VARS 13 | 14 | SSD1306 display(0x3c, 4, 15); 15 | 16 | config_t cfg; 17 | system_t sys; 18 | stats_t stats; 19 | MSP msp; 20 | 21 | msp_radar_pos_t radarPos; 22 | 23 | curr_t curr; // Our peer ID 24 | peer_t peers[LORA_NODES_MAX]; // Other peers 25 | 26 | air_type0_t air_0; 27 | air_type1_t air_1; 28 | air_type2_t air_2; 29 | air_type1_t * air_r1; 30 | air_type2_t * air_r2; 31 | 32 | // -------- SYSTEM 33 | 34 | void set_mode(uint8_t mode) { 35 | 36 | switch (mode) { 37 | 38 | case 0 : // SF9 250 39 | cfg.lora_frequency = 433E6; // 433E6, 868E6, 915E6 40 | cfg.lora_bandwidth = 250000; 41 | cfg.lora_coding_rate = 5; 42 | cfg.lora_spreading_factor = 9; 43 | cfg.lora_power = 20; 44 | cfg.lora_slot_spacing = 125; 45 | cfg.lora_nodes_max = LORA_NODES_MAX; 46 | cfg.lora_cycle = cfg.lora_nodes_max * cfg.lora_slot_spacing; 47 | cfg.lora_timing_delay = -60; 48 | cfg.lora_antidrift_threshold = 5; 49 | cfg.lora_antidrift_correction = 5; 50 | cfg.lora_peer_timeout = 6000; 51 | 52 | // cfg.lora_air_mode = LORA_NODES_MIN; 53 | 54 | cfg.msp_version = 2; 55 | cfg.msp_timeout = 100; 56 | cfg.msp_fc_timeout = 6000; 57 | cfg.msp_after_tx_delay = 85; 58 | 59 | cfg.cycle_scan = 4000; 60 | cfg.cycle_display = 250; 61 | cfg.cycle_stats = 1000; 62 | 63 | break; 64 | 65 | } 66 | } 67 | 68 | int count_peers(bool active = 0) { 69 | int j = 0; 70 | for (int i = 0; i < LORA_NODES_MAX; i++) { 71 | if (active == 1) { 72 | if ((peers[i].id > 0) && !peers[i].lost) { 73 | j++; 74 | } 75 | } 76 | else { 77 | if (peers[i].id > 0) { 78 | j++; 79 | } 80 | } 81 | } 82 | return j; 83 | } 84 | 85 | void reset_peers() { 86 | sys.now_sec = millis(); 87 | for (int i = 0; i < LORA_NODES_MAX; i++) { 88 | peers[i].id = 0; 89 | peers[i].host = 0; 90 | peers[i].state = 0; 91 | peers[i].lost = 0; 92 | peers[i].broadcast = 0; 93 | peers[i].lq_updated = sys.now_sec; 94 | peers[i].lq_tick = 0; 95 | peers[i].lq = 0; 96 | peers[i].updated = 0; 97 | peers[i].rssi = 0; 98 | peers[i].distance = 0; 99 | peers[i].direction = 0; 100 | peers[i].relalt = 0; 101 | strcpy(peers[i].name, ""); 102 | } 103 | } 104 | 105 | void pick_id() { 106 | curr.id = 0; 107 | for (int i = 0; i < LORA_NODES_MAX; i++) { 108 | if ((peers[i].id == 0) && (curr.id == 0)) { 109 | curr.id = i + 1; 110 | } 111 | } 112 | } 113 | 114 | void resync_tx_slot(int16_t delay) { 115 | bool startnow = 0; 116 | for (int i = 0; (i < LORA_NODES_MAX) && (startnow == 0); i++) { // Resync 117 | if (peers[i].id > 0) { 118 | sys.lora_next_tx = peers[i].updated + (curr.id - peers[i].id) * cfg.lora_slot_spacing + cfg.lora_cycle + delay; 119 | startnow = 1; 120 | } 121 | } 122 | } 123 | 124 | // ----------------------------------------------------------------------------- calc gps distance 125 | 126 | double deg2rad(double deg) { 127 | return (deg * M_PI / 180); 128 | } 129 | 130 | double rad2deg(double rad) { 131 | return (rad * 180 / M_PI); 132 | } 133 | 134 | /** 135 | * Returns the distance between two points on the Earth. 136 | * Direct translation from http://en.wikipedia.org/wiki/Haversine_formula 137 | * @param lat1d Latitude of the first point in degrees 138 | * @param lon1d Longitude of the first point in degrees 139 | * @param lat2d Latitude of the second point in degrees 140 | * @param lon2d Longitude of the second point in degrees 141 | * @return The distance between the two points in meters 142 | */ 143 | 144 | double gpsDistanceBetween(double lat1d, double lon1d, double lat2d, double lon2d) { 145 | double lat1r, lon1r, lat2r, lon2r, u, v; 146 | lat1r = deg2rad(lat1d); 147 | lon1r = deg2rad(lon1d); 148 | lat2r = deg2rad(lat2d); 149 | lon2r = deg2rad(lon2d); 150 | u = sin((lat2r - lat1r)/2); 151 | v = sin((lon2r - lon1r)/2); 152 | return 2.0 * 6371000 * asin(sqrt(u * u + cos(lat1r) * cos(lat2r) * v * v)); 153 | } 154 | 155 | /* 156 | double gpsDistanceBetween(double lat1, double long1, double lat2, double long2) 157 | { 158 | // returns distance in meters between two positions, both specified 159 | // as signed decimal-degrees latitude and longitude. Uses great-circle 160 | // distance computation for hypothetical sphere of radius 6372795 meters. 161 | // Because Earth is no exact sphere, rounding errors may be up to 0.5%. 162 | // Courtesy of Maarten Lamers 163 | double delta = radians(long1-long2); 164 | double sdlong = sin(delta); 165 | double cdlong = cos(delta); 166 | lat1 = radians(lat1); 167 | lat2 = radians(lat2); 168 | double slat1 = sin(lat1); 169 | double clat1 = cos(lat1); 170 | double slat2 = sin(lat2); 171 | double clat2 = cos(lat2); 172 | delta = (clat1 * slat2) - (slat1 * clat2 * cdlong); 173 | delta = sq(delta); 174 | delta += sq(clat2 * sdlong); 175 | delta = sqrt(delta); 176 | double denom = (slat1 * slat2) + (clat1 * clat2 * cdlong); 177 | delta = atan2(delta, denom); 178 | return delta * 6372795; 179 | } 180 | 181 | */ 182 | 183 | double gpsCourseTo(double lat1, double long1, double lat2, double long2) 184 | { 185 | // returns course in degrees (North=0, West=270) from position 1 to position 2, 186 | // both specified as signed decimal-degrees latitude and longitude. 187 | // Because Earth is no exact sphere, calculated course may be off by a tiny fraction. 188 | // Courtesy of Maarten Lamers 189 | double dlon = radians(long2-long1); 190 | lat1 = radians(lat1); 191 | lat2 = radians(lat2); 192 | double a1 = sin(dlon) * cos(lat2); 193 | double a2 = sin(lat1) * cos(lat2) * cos(dlon); 194 | a2 = cos(lat1) * sin(lat2) - a2; 195 | a2 = atan2(a1, a2); 196 | if (a2 < 0.0) 197 | { 198 | a2 += TWO_PI; 199 | } 200 | return degrees(a2); 201 | } 202 | 203 | // -------- LoRa 204 | 205 | void lora_send() { 206 | 207 | if (sys.lora_tick % 8 == 0) { 208 | 209 | if (sys.lora_tick % 16 == 0) { 210 | air_2.id = curr.id; 211 | air_2.type = 2; 212 | air_2.vbat = curr.fcanalog.vbat; // 1 to 255 (V x 10) 213 | air_2.mah = curr.fcanalog.mAhDrawn; 214 | air_2.rssi = curr.fcanalog.rssi; // 0 to 1023 215 | 216 | while (!LoRa.beginPacket()) { } 217 | LoRa.write((uint8_t*)&air_2, sizeof(air_2)); 218 | LoRa.endPacket(false); 219 | } 220 | else { 221 | air_1.id = curr.id; 222 | air_1.type = 1; 223 | air_1.host = curr.host; 224 | air_1.state = curr.state; 225 | air_1.broadcast = 0; 226 | air_1.speed = curr.gps.groundSpeed / 100; // From cm/s to m/s 227 | strncpy(air_1.name, curr.name, LORA_NAME_LENGTH); 228 | 229 | while (!LoRa.beginPacket()) { } 230 | LoRa.write((uint8_t*)&air_1, sizeof(air_1)); 231 | LoRa.endPacket(false); 232 | } 233 | } 234 | else { 235 | 236 | air_0.id = curr.id; 237 | air_0.type = 0; 238 | air_0.lat = curr.gps.lat / 100; // From XX.1234567 to XX.12345 239 | air_0.lon = curr.gps.lon / 100; // From XX.1234567 to XX.12345 240 | air_0.alt = curr.gps.alt; // m 241 | air_0.heading = curr.gps.groundCourse / 10; // From degres x 10 to degres 242 | 243 | while (!LoRa.beginPacket()) { } 244 | LoRa.write((uint8_t*)&air_0, sizeof(air_0)); 245 | LoRa.endPacket(false); 246 | } 247 | } 248 | 249 | void lora_receive(int packetSize) { 250 | 251 | if (packetSize == 0) return; 252 | 253 | sys.lora_last_rx = millis(); 254 | sys.lora_last_rx -= (stats.last_tx_duration > 0 ) ? stats.last_tx_duration : 0; // RX time should be the same as TX time 255 | 256 | sys.last_rssi = LoRa.packetRssi(); 257 | sys.ppsc++; 258 | 259 | LoRa.readBytes((uint8_t *)&air_0, packetSize); 260 | 261 | uint8_t id = air_0.id - 1; 262 | sys.air_last_received_id = air_0.id; 263 | peers[id].id = sys.air_last_received_id; 264 | peers[id].lq_tick++; 265 | peers[id].lost = 0; 266 | peers[id].updated = sys.lora_last_rx; 267 | peers[id].rssi = sys.last_rssi; 268 | 269 | if (air_0.type == 1) { // Type 1 packet (Speed + host + state + broadcast + name) 270 | 271 | air_r1 = (air_type1_t*)&air_0; 272 | 273 | peers[id].host = (*air_r1).host; 274 | peers[id].state = (*air_r1).state; 275 | peers[id].broadcast = (*air_r1).broadcast; 276 | peers[id].gps.groundSpeed = (*air_r1).speed * 100; // From m/s to cm/s 277 | strncpy(peers[id].name, (*air_r1).name, LORA_NAME_LENGTH); 278 | peers[id].name[LORA_NAME_LENGTH] = 0; 279 | 280 | } 281 | else if (air_0.type == 2) { // Type 2 packet (vbat mAh RSSI) 282 | 283 | air_r2 = (air_type2_t*)&air_0; 284 | 285 | peers[id].fcanalog.vbat = (*air_r2).vbat; 286 | peers[id].fcanalog.mAhDrawn = (*air_r2).mah; 287 | peers[id].fcanalog.rssi = (*air_r2).rssi; 288 | 289 | } 290 | else { // Type 0 packet (GPS + heading) 291 | 292 | peers[id].gps.lat = air_0.lat * 100; // From XX.12345 to XX.1234500 293 | peers[id].gps.lon = air_0.lon * 100; // From XX.12345 to XX.1234500 294 | peers[id].gps.alt = air_0.alt; // m 295 | peers[id].gps.groundCourse = air_0.heading * 10; // From degres to degres x 10 296 | 297 | if (peers[id].gps.lat != 0 && peers[id].gps.lon != 0) { // Save the last known coordinates 298 | peers[id].gpsrec.lat = peers[id].gps.lat; 299 | peers[id].gpsrec.lon = peers[id].gps.lon; 300 | peers[id].gpsrec.alt = peers[id].gps.alt; 301 | peers[id].gpsrec.groundCourse = peers[id].gps.groundCourse; 302 | peers[id].gpsrec.groundSpeed = peers[id].gps.groundSpeed; 303 | } 304 | } 305 | 306 | sys.num_peers = count_peers(); 307 | 308 | if ((sys.air_last_received_id == curr.id) && (sys.phase > MODE_LORA_SYNC) && !sys.lora_no_tx) { // Same slot, conflict 309 | uint32_t cs1 = peers[id].name[0] + peers[id].name[1] * 26 + peers[id].name[2] * 26 * 26 ; 310 | uint32_t cs2 = curr.name[0] + curr.name[1] * 26 + curr.name[2] * 26 * 26; 311 | if (cs1 < cs2) { // Pick another slot 312 | sprintf(sys.message, "%s", "ID CONFLICT"); 313 | pick_id(); 314 | resync_tx_slot(cfg.lora_timing_delay); 315 | } 316 | } 317 | } 318 | 319 | void lora_init() { 320 | 321 | SPI.begin(5, 19, 27, 18); 322 | LoRa.setPins(SS, RST, DI0); 323 | 324 | if (!LoRa.begin(cfg.lora_frequency)) { 325 | display.drawString (94, 9, "FAIL"); 326 | while (1); 327 | } 328 | 329 | LoRa.sleep(); 330 | LoRa.setSignalBandwidth(cfg.lora_bandwidth); 331 | LoRa.setCodingRate4(cfg.lora_coding_rate); 332 | LoRa.setSpreadingFactor(cfg.lora_spreading_factor); 333 | LoRa.setTxPower(cfg.lora_power, 1); 334 | LoRa.setOCP(250); 335 | LoRa.idle(); 336 | LoRa.onReceive(lora_receive); 337 | LoRa.enableCrc(); 338 | } 339 | 340 | // ----------------------------------------------------------------------------- Display 341 | 342 | void display_init() { 343 | pinMode(16, OUTPUT); 344 | pinMode(2, OUTPUT); 345 | digitalWrite(16, LOW); 346 | delay(50); 347 | digitalWrite(16, HIGH); 348 | display.init(); 349 | display.flipScreenVertically(); 350 | display.setFont(ArialMT_Plain_10); 351 | display.setTextAlignment(TEXT_ALIGN_LEFT); 352 | } 353 | 354 | void display_draw() { 355 | display.clear(); 356 | 357 | int j = 0; 358 | int line; 359 | 360 | if (sys.display_page == 0) { 361 | 362 | display.setFont(ArialMT_Plain_24); 363 | display.setTextAlignment(TEXT_ALIGN_RIGHT); 364 | display.drawString(26, 11, String(curr.gps.numSat)); 365 | display.drawString(13, 42, String(sys.num_peers_active + 1)); 366 | display.drawString (125, 11, String(peer_slotname[curr.id])); 367 | 368 | display.setFont(ArialMT_Plain_10); 369 | 370 | // display.drawString (83, 44, String(cfg.lora_cycle) + "ms"); 371 | // display.drawString (105, 23, String(cfg.lora_nodes_max)); 372 | 373 | display.drawString (126, 29, "_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ "); 374 | display.drawString (107, 44, String(stats.percent_received)); 375 | display.drawString(107, 54, String(sys.last_rssi)); 376 | 377 | display.setTextAlignment (TEXT_ALIGN_CENTER); 378 | display.drawString (64, 0, String(sys.message)); 379 | 380 | display.setTextAlignment (TEXT_ALIGN_LEFT); 381 | display.drawString (55, 12, String(curr.name)); 382 | display.drawString (27, 23, "SAT"); 383 | display.drawString (108, 44, "%E"); 384 | 385 | display.drawString(21, 54, String(sys.pps) + "p/s"); 386 | display.drawString (109, 54, "dB"); 387 | display.drawString (55, 23, String(host_name[curr.host])); 388 | 389 | if (sys.air_last_received_id > 0) { 390 | display.drawString (36 + sys.air_last_received_id * 8, 54, String(peer_slotname[sys.air_last_received_id])); 391 | } 392 | 393 | display.drawString (15, 44, "Nod/" + String(cfg.lora_nodes_max)); 394 | 395 | if (curr.gps.fixType == 1) display.drawString (27, 12, "2D"); 396 | if (curr.gps.fixType == 2) display.drawString (27, 12, "3D"); 397 | } 398 | 399 | else if (sys.display_page == 1) { 400 | 401 | display.setFont (ArialMT_Plain_10); 402 | display.setTextAlignment (TEXT_ALIGN_LEFT); 403 | 404 | display.drawHorizontalLine(0, 11, 128); 405 | 406 | long pos[LORA_NODES_MAX]; 407 | long diff; 408 | 409 | for (int i = 0; i < LORA_NODES_MAX ; i++) { 410 | if (peers[i].id > 0 && !peers[i].lost) { 411 | diff = sys.lora_last_tx - peers[i].updated; 412 | if (diff > 0 && diff < cfg.lora_cycle) { 413 | pos[i] = 128 - round(128 * diff / cfg.lora_cycle); 414 | } 415 | } 416 | else { 417 | pos[i] = -1; 418 | } 419 | } 420 | 421 | int rect_l = stats.last_tx_duration * 128 / cfg.lora_cycle; 422 | 423 | for (int i = 0; i < LORA_NODES_MAX; i++) { 424 | 425 | display.setTextAlignment (TEXT_ALIGN_LEFT); 426 | 427 | if (pos[i] > -1) { 428 | display.drawRect(pos[i], 0, rect_l, 12); 429 | display.drawString (pos[i] + 2, 0, String(peer_slotname[peers[i].id])); 430 | } 431 | 432 | if (peers[i].id > 0 && j < 4) { 433 | line = j * 9 + 14; 434 | 435 | display.drawString (0, line, String(peer_slotname[peers[i].id])); 436 | display.drawString (12, line, String(peers[i].name)); 437 | display.drawString (60, line, String(host_name[peers[i].host])); 438 | display.setTextAlignment (TEXT_ALIGN_RIGHT); 439 | 440 | if (peers[i].lost) { // Peer timed out 441 | display.drawString (127, line, "L:" + String((int)((sys.lora_last_tx - peers[i].updated) / 1000)) + "s" ); 442 | } 443 | else { 444 | if (sys.lora_last_tx > peers[i].updated) { 445 | display.drawString (119, line, String(sys.lora_last_tx - peers[i].updated)); 446 | display.drawString (127, line, "-"); 447 | } 448 | else { 449 | display.drawString (119, line, String(cfg.lora_cycle + sys.lora_last_tx - peers[i].updated)); 450 | display.drawString (127, line, "+"); 451 | 452 | } 453 | } 454 | j++; 455 | } 456 | } 457 | } 458 | 459 | else if (sys.display_page == 2) { 460 | 461 | display.setFont (ArialMT_Plain_10); 462 | display.setTextAlignment (TEXT_ALIGN_LEFT); 463 | display.drawString(0, 0, "LORA TX"); 464 | display.drawString(0, 10, "MSP"); 465 | display.drawString(0, 20, "OLED"); 466 | display.drawString(0, 30, "CYCLE"); 467 | display.drawString(0, 40, "SLOTS"); 468 | display.drawString(0, 50, "UPTIME"); 469 | 470 | display.drawString(112, 0, "ms"); 471 | display.drawString(112, 10, "ms"); 472 | display.drawString(112, 20, "ms"); 473 | display.drawString(112, 30, "ms"); 474 | display.drawString(112, 40, "ms"); 475 | display.drawString(112, 50, "s"); 476 | 477 | display.setTextAlignment(TEXT_ALIGN_RIGHT); 478 | display.drawString (111, 0, String(stats.last_tx_duration)); 479 | display.drawString (111, 10, String(stats.last_msp_duration[0]) + " / " + String(stats.last_msp_duration[1]) + " / " + String(stats.last_msp_duration[2]) + " / " + String(stats.last_msp_duration[3])); 480 | display.drawString (111, 20, String(stats.last_oled_duration)); 481 | display.drawString (111, 30, String(cfg.lora_cycle)); 482 | display.drawString (111, 40, String(LORA_NODES_MAX) + " x " + String(cfg.lora_slot_spacing)); 483 | display.drawString (111, 50, String((int)millis() / 1000)); 484 | 485 | } 486 | else if (sys.display_page >= 3) { 487 | 488 | int i = constrain(sys.display_page + 1 - LORA_NODES_MAX, 0, LORA_NODES_MAX - 1); 489 | bool iscurrent = (i + 1 == curr.id); 490 | 491 | display.setFont(ArialMT_Plain_24); 492 | display.setTextAlignment (TEXT_ALIGN_LEFT); 493 | display.drawString (0, 0, String(peer_slotname[i + 1])); 494 | 495 | display.setFont(ArialMT_Plain_16); 496 | display.setTextAlignment(TEXT_ALIGN_RIGHT); 497 | 498 | if (iscurrent) { 499 | display.drawString (128, 0, String(curr.name)); 500 | } 501 | else { 502 | display.drawString (128, 0, String(peers[i].name)); 503 | } 504 | 505 | display.setTextAlignment (TEXT_ALIGN_LEFT); 506 | display.setFont (ArialMT_Plain_10); 507 | 508 | if (peers[i].id > 0 || iscurrent) { 509 | 510 | if (peers[i].lost && !iscurrent) { display.drawString (19, 0, "LOST"); } 511 | else if (peers[i].lq == 0 && !iscurrent) { display.drawString (19, 0, "x"); } 512 | else if (peers[i].lq == 1) { display.drawXbm(19, 2, 8, 8, icon_lq_1); } 513 | else if (peers[i].lq == 2) { display.drawXbm(19, 2, 8, 8, icon_lq_2); } 514 | else if (peers[i].lq == 3) { display.drawXbm(19, 2, 8, 8, icon_lq_3); } 515 | else if (peers[i].lq == 4) { display.drawXbm(19, 2, 8, 8, icon_lq_4); } 516 | 517 | if (iscurrent) { 518 | display.drawString (19, 0, ""); 519 | display.drawString (19, 12, String(host_name[curr.host])); 520 | } 521 | else { 522 | if (!peers[i].lost) { 523 | display.drawString (28, 0, String(peers[i].rssi) + "db"); 524 | } 525 | display.drawString (19, 12, String(host_name[peers[i].host])); 526 | } 527 | 528 | if (iscurrent) { 529 | display.drawString (50, 12, String(host_state[curr.state])); 530 | } 531 | else { 532 | display.drawString (50, 12, String(host_state[peers[i].state])); 533 | } 534 | 535 | display.setTextAlignment (TEXT_ALIGN_RIGHT); 536 | 537 | if (iscurrent) { 538 | display.drawString (128, 24, "LA " + String((float)curr.gps.lat / 10000000, 6)); 539 | display.drawString (128, 34, "LO "+ String((float)curr.gps.lon / 10000000, 6)); 540 | } 541 | else { 542 | display.drawString (128, 24, "LA " + String((float)peers[i].gpsrec.lat / 10000000, 6)); 543 | display.drawString (128, 34, "LO "+ String((float)peers[i].gpsrec.lon / 10000000, 6)); 544 | } 545 | 546 | display.setTextAlignment (TEXT_ALIGN_LEFT); 547 | 548 | if (iscurrent) { 549 | display.drawString (0, 24, "A " + String(curr.gps.alt) + "m"); 550 | display.drawString (0, 34, "S " + String(peers[i].gpsrec.groundSpeed / 100) + "m/s"); 551 | display.drawString (0, 44, "C " + String(curr.gps.groundCourse / 10) + "°"); 552 | } 553 | else { 554 | display.drawString (0, 24, "A " + String(peers[i].gpsrec.alt) + "m"); 555 | display.drawString (0, 34, "S " + String(peers[i].gpsrec.groundSpeed / 100) + "m/s"); 556 | display.drawString (0, 44, "C " + String(peers[i].gpsrec.groundCourse / 10) + "°"); 557 | } 558 | 559 | if (peers[i].gps.lat != 0 && peers[i].gps.lon != 0 && curr.gps.lat != 0 && curr.gps.lon != 0 && !iscurrent) { 560 | 561 | 562 | double lat1 = curr.gps.lat / 10000000; 563 | double lon1 = curr.gps.lon / 10000000; 564 | double lat2 = peers[i].gpsrec.lat / 10000000; 565 | double lon2 = peers[i].gpsrec.lon / 10000000; 566 | 567 | peers[i].distance = gpsDistanceBetween(lat1, lon1, lat2, lon2); 568 | peers[i].direction = gpsCourseTo(lat1, lon1, lat2, lon2); 569 | peers[i].relalt = peers[i].gpsrec.alt - curr.gps.alt; 570 | 571 | display.drawString (40, 44, "B " + String(peers[i].direction) + "°"); 572 | display.drawString (88, 44, "D " + String(peers[i].distance) + "m"); 573 | display.drawString (0, 54, "R " + String(peers[i].relalt) + "m"); 574 | } 575 | 576 | if (iscurrent) { 577 | display.drawString (40, 54, String((float)curr.fcanalog.vbat / 10) + "v"); 578 | display.drawString (88, 54, String((int)curr.fcanalog.mAhDrawn) + "mah"); 579 | } 580 | else { 581 | display.drawString (40, 54, String((float)peers[i].fcanalog.vbat / 10) + "v"); 582 | display.drawString (88, 54, String((int)peers[i].fcanalog.mAhDrawn) + "mah"); 583 | } 584 | 585 | display.setTextAlignment (TEXT_ALIGN_RIGHT); 586 | 587 | } 588 | else { 589 | display.drawString (35, 7, "SLOT IS EMPTY"); 590 | } 591 | 592 | } 593 | 594 | sys.air_last_received_id = 0; 595 | sys.message[0] = 0; 596 | display.display(); 597 | } 598 | 599 | void display_logo() { 600 | display.drawXbm(0, 0, logo_width_s, logo_height_s, logo_bits_s); 601 | display.display(); 602 | delay(2000); 603 | display.clear(); 604 | } 605 | 606 | // -------- MSP and FC 607 | 608 | 609 | void msp_get_state() { 610 | 611 | uint32_t planeModes; 612 | msp.getActiveModes(&planeModes); 613 | curr.state = bitRead(planeModes, 0); 614 | 615 | } 616 | 617 | void msp_get_name() { 618 | msp.request(MSP_NAME, &curr.name, sizeof(curr.name)); 619 | curr.name[6] = '\0'; 620 | } 621 | 622 | void msp_get_gps() { 623 | msp.request(MSP_RAW_GPS, &curr.gps, sizeof(curr.gps)); 624 | } 625 | 626 | void msp_set_fc() { 627 | char j[5]; 628 | curr.host = HOST_NONE; 629 | msp.request(MSP_FC_VARIANT, &j, sizeof(j)); 630 | 631 | if (strncmp(j, "INAV", 4) == 0) { 632 | curr.host = HOST_INAV; 633 | } 634 | else if (strncmp(j, "BTFL", 4) == 0) { 635 | curr.host = HOST_BTFL; 636 | } 637 | 638 | if (curr.host == HOST_INAV || curr.host == HOST_BTFL) { 639 | msp.request(MSP_FC_VERSION, &curr.fcversion, sizeof(curr.fcversion)); 640 | } 641 | } 642 | 643 | void msp_get_fcanalog() { 644 | msp.request(MSP_ANALOG, &curr.fcanalog, sizeof(curr.fcanalog)); 645 | } 646 | 647 | void msp_send_radar(uint8_t i) { 648 | radarPos.id = i; 649 | radarPos.state = peers[i].state; 650 | radarPos.lat = peers[i].gps.lat; // x 10E7 651 | radarPos.lon = peers[i].gps.lon; // x 10E7 652 | radarPos.alt = peers[i].gps.alt * 100; // cm 653 | radarPos.heading = peers[i].gps.groundCourse / 10; // From ° x 10 to ° 654 | radarPos.speed = peers[i].gps.groundSpeed; // cm/s 655 | radarPos.lq = peers[i].lq; 656 | msp.command2(MSP2_COMMON_SET_RADAR_POS , &radarPos, sizeof(radarPos), 0); 657 | // msp.command(MSP_SET_RADAR_POS , &radarPos, sizeof(radarPos), 0); 658 | } 659 | 660 | void msp_send_peers() { 661 | for (int i = 0; i < LORA_NODES_MAX; i++) { 662 | if (peers[i].id > 0) { 663 | msp_send_radar(i); 664 | } 665 | } 666 | } 667 | 668 | void msp_send_peer(uint8_t peer_id) { 669 | if (peers[peer_id].id > 0) { 670 | msp_send_radar(peer_id); 671 | } 672 | } 673 | 674 | // -------- INTERRUPTS 675 | 676 | const byte interruptPin = 0; 677 | volatile int interruptCounter = 0; 678 | int numberOfInterrupts = 0; 679 | 680 | portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; 681 | 682 | void IRAM_ATTR handleInterrupt() { 683 | portENTER_CRITICAL_ISR(&mux); 684 | 685 | if (sys.io_button_pressed == 0) { 686 | sys.io_button_pressed = 1; 687 | 688 | if (sys.display_page >= 3 + LORA_NODES_MAX) { 689 | sys.display_page = 0; 690 | } 691 | else { 692 | sys.display_page++; 693 | } 694 | if (sys.num_peers == 0 && sys.display_page == 1) { // No need for timings graphs when alone 695 | sys.display_page++; 696 | } 697 | sys.io_button_released = millis(); 698 | } 699 | portEXIT_CRITICAL_ISR(&mux); 700 | } 701 | 702 | 703 | // ----------------------------- setup 704 | 705 | void setup() { 706 | 707 | set_mode(LORA_PERF_MODE); 708 | 709 | display_init(); 710 | display_logo(); 711 | 712 | display.drawString(0, 0, "RADAR VERSION"); 713 | display.drawString(90, 0, VERSION); 714 | 715 | lora_init(); 716 | msp.begin(Serial1); 717 | Serial1.begin(115200, SERIAL_8N1, SERIAL_PIN_RX , SERIAL_PIN_TX); 718 | reset_peers(); 719 | 720 | pinMode(interruptPin, INPUT); 721 | sys.io_button_pressed = 0; 722 | attachInterrupt(digitalPinToInterrupt(interruptPin), handleInterrupt, RISING); 723 | 724 | display.drawString (0, 9, "HOST"); 725 | display.display(); 726 | 727 | sys.display_updated = 0; 728 | sys.cycle_scan_begin = millis(); 729 | 730 | sys.io_led_blink = 0; 731 | 732 | pinMode(LED, OUTPUT); 733 | digitalWrite(LED, HIGH); 734 | 735 | curr.host = HOST_NONE; 736 | 737 | sys.phase = MODE_HOST_SCAN; 738 | } 739 | 740 | // ----------------------------------------------------------------------------- MAIN LOOP 741 | 742 | void loop() { 743 | 744 | sys.now = millis(); 745 | 746 | // ---------------------- IO BUTTON 747 | 748 | if ((sys.now > sys.io_button_released + 150) && (sys.io_button_pressed == 1)) { 749 | sys.io_button_pressed = 0; 750 | } 751 | 752 | // ---------------------- HOST SCAN 753 | 754 | if (sys.phase == MODE_HOST_SCAN) { 755 | if ((sys.now > (sys.cycle_scan_begin + cfg.msp_fc_timeout)) || (curr.host != HOST_NONE)) { // End of the host scan 756 | 757 | if (curr.host != HOST_NONE) { 758 | msp_get_name(); 759 | } 760 | 761 | if (curr.name[0] == '\0') { 762 | for (int i = 0; i < 4; i++) { 763 | curr.name[i] = (char) random(65, 90); 764 | curr.name[4] = 0; 765 | } 766 | } 767 | 768 | curr.gps.fixType = 0; 769 | curr.gps.lat = 0; 770 | curr.gps.lon = 0; 771 | curr.gps.alt = 0; 772 | curr.id = 0; 773 | if (curr.host > 0) { 774 | display.drawString (35, 9, String(host_name[curr.host]) + " " + String(curr.fcversion.versionMajor) + "." + String(curr.fcversion.versionMinor) + "." + String(curr.fcversion.versionPatchLevel)); 775 | } 776 | else { 777 | display.drawString (35, 9, String(host_name[curr.host])); 778 | } 779 | 780 | display.drawProgressBar(0, 53, 40, 6, 100); 781 | display.drawString (0, 18, "SCAN"); 782 | display.display(); 783 | 784 | LoRa.sleep(); 785 | LoRa.receive(); 786 | 787 | sys.cycle_scan_begin = millis(); 788 | sys.phase = MODE_LORA_INIT; 789 | 790 | } else { // Still scanning 791 | if ((sys.now > sys.display_updated + cfg.cycle_display / 2) && sys.display_enable) { 792 | 793 | delay(50); 794 | msp_set_fc(); 795 | 796 | display.drawProgressBar(0, 53, 40, 6, 100 * (millis() - sys.cycle_scan_begin) / cfg.msp_fc_timeout); 797 | display.display(); 798 | sys.display_updated = millis(); 799 | } 800 | } 801 | } 802 | 803 | // ---------------------- LORA INIT 804 | 805 | if (sys.phase == MODE_LORA_INIT) { 806 | if (sys.now > (sys.cycle_scan_begin + cfg.cycle_scan)) { // End of the scan, set the ID then sync 807 | 808 | sys.num_peers = count_peers(); 809 | 810 | if (sys.num_peers >= LORA_NODES_MAX || sys.io_button_released > 0) { 811 | sys.lora_no_tx = 1; 812 | sys.display_page = 0; 813 | } 814 | else { 815 | // cfg.lora_cycle = cfg.lora_slot_spacing * cfg.lora_air_mode; 816 | pick_id(); 817 | } 818 | 819 | sys.phase = MODE_LORA_SYNC; 820 | 821 | } else { // Still scanning 822 | if ((sys.now > sys.display_updated + cfg.cycle_display / 2) && sys.display_enable) { 823 | for (int i = 0; i < LORA_NODES_MAX; i++) { 824 | if (peers[i].id > 0) { 825 | display.drawString(40 + peers[i].id * 8, 18, String(peer_slotname[peers[i].id])); 826 | } 827 | } 828 | display.drawProgressBar(40, 53, 86, 6, 100 * (millis() - sys.cycle_scan_begin) / cfg.cycle_scan); 829 | display.display(); 830 | sys.display_updated = millis(); 831 | } 832 | } 833 | } 834 | 835 | // ---------------------- LORA SYNC 836 | 837 | if (sys.phase == MODE_LORA_SYNC) { 838 | 839 | if (sys.num_peers == 0 || sys.lora_no_tx) { // Alone or no_tx mode, start at will 840 | sys.lora_next_tx = millis() + cfg.lora_cycle; 841 | } 842 | else { // Not alone, sync by slot 843 | resync_tx_slot(cfg.lora_timing_delay); 844 | } 845 | sys.display_updated = sys.lora_next_tx + cfg.lora_cycle - 30; 846 | sys.stats_updated = sys.lora_next_tx + cfg.lora_cycle - 15; 847 | 848 | sys.pps = 0; 849 | sys.ppsc = 0; 850 | sys.num_peers = 0; 851 | stats.packets_total = 0; 852 | stats.packets_received = 0; 853 | stats.percent_received = 0; 854 | 855 | digitalWrite(LED, LOW); 856 | 857 | sys.phase = MODE_LORA_RX; 858 | } 859 | 860 | // ---------------------- LORA RX 861 | 862 | if ((sys.phase == MODE_LORA_RX) && (sys.now > sys.lora_next_tx)) { 863 | 864 | // sys.lora_last_tx = sys.lora_next_tx; 865 | 866 | while (sys.now > sys.lora_next_tx) { // In case we skipped some beats 867 | sys.lora_next_tx += cfg.lora_cycle; 868 | } 869 | 870 | if (sys.lora_no_tx) { 871 | sprintf(sys.message, "%s", "SILENT MODE (NO TX)"); 872 | } 873 | else { 874 | sys.phase = MODE_LORA_TX; 875 | } 876 | 877 | sys.lora_tick++; 878 | 879 | } 880 | 881 | // ---------------------- LORA TX 882 | 883 | if (sys.phase == MODE_LORA_TX) { 884 | 885 | if ((curr.host == HOST_NONE) || (curr.gps.fixType < 1)) { 886 | curr.gps.lat = 0; 887 | curr.gps.lon = 0; 888 | curr.gps.alt = 0; 889 | curr.gps.groundCourse = 0; 890 | curr.gps.groundSpeed = 0; 891 | } 892 | 893 | sys.lora_last_tx = millis(); 894 | lora_send(); 895 | stats.last_tx_duration = millis() - sys.lora_last_tx; 896 | 897 | // Drift correction 898 | 899 | if (curr.id > 1) { 900 | int prev = curr.id - 2; 901 | if (peers[prev].id > 0) { 902 | sys.lora_drift = sys.lora_last_tx - peers[prev].updated - cfg.lora_slot_spacing; 903 | 904 | if ((abs(sys.lora_drift) > cfg.lora_antidrift_threshold) && (abs(sys.lora_drift) < (cfg.lora_slot_spacing * 0.5))) { 905 | sys.drift_correction = constrain(sys.lora_drift, -cfg.lora_antidrift_correction, cfg.lora_antidrift_correction); 906 | sys.lora_next_tx -= sys.drift_correction; 907 | sprintf(sys.message, "%s %3d", "TIMING ADJUST", -sys.drift_correction); 908 | } 909 | } 910 | } 911 | 912 | sys.lora_slot = 0; 913 | sys.msp_next_cycle = sys.lora_last_tx + cfg.msp_after_tx_delay; 914 | 915 | // Back to RX 916 | 917 | LoRa.sleep(); 918 | LoRa.receive(); 919 | sys.phase = MODE_LORA_RX; 920 | } 921 | 922 | // ---------------------- DISPLAY 923 | 924 | if ((sys.now > sys.display_updated + cfg.cycle_display) && sys.display_enable && (sys.phase > MODE_LORA_SYNC)) { 925 | 926 | stats.timer_begin = millis(); 927 | display_draw(); 928 | stats.last_oled_duration = millis() - stats.timer_begin; 929 | sys.display_updated = sys.now; 930 | } 931 | 932 | // ---------------------- SERIAL / MSP 933 | 934 | if (sys.now > sys.msp_next_cycle && curr.host != HOST_NONE && sys.phase > MODE_LORA_SYNC && sys.lora_slot < LORA_NODES_MAX) { 935 | 936 | stats.timer_begin = millis(); 937 | 938 | if (sys.lora_slot == 0) { 939 | 940 | if (sys.lora_tick % 6 == 0) { 941 | msp_get_state(); 942 | } 943 | 944 | if ((sys.lora_tick + 1) % 6 == 0) { 945 | msp_get_fcanalog(); 946 | } 947 | 948 | } 949 | 950 | msp_get_gps(); // GPS > FC > ESP 951 | msp_send_peer(sys.lora_slot); // ESP > FC > OSD 952 | 953 | stats.last_msp_duration[sys.lora_slot] = millis() - stats.timer_begin; 954 | sys.msp_next_cycle += cfg.lora_slot_spacing; 955 | sys.lora_slot++; 956 | 957 | } 958 | 959 | 960 | // ---------------------- STATISTICS & IO 961 | 962 | if ((sys.now > (cfg.cycle_stats + sys.stats_updated)) && (sys.phase > MODE_LORA_SYNC)) { 963 | 964 | sys.pps = sys.ppsc; 965 | sys.ppsc = 0; 966 | 967 | // Timed-out peers + LQ 968 | 969 | for (int i = 0; i < LORA_NODES_MAX; i++) { 970 | 971 | if (sys.now > (peers[i].lq_updated + cfg.lora_cycle * 4)) { 972 | uint16_t diff = peers[i].updated - peers[i].lq_updated; 973 | peers[i].lq = constrain(peers[i].lq_tick * 4.4 * cfg.lora_cycle / diff, 0, 4); 974 | peers[i].lq_updated = sys.now; 975 | peers[i].lq_tick = 0; 976 | } 977 | 978 | if (peers[i].id > 0 && ((sys.now - peers[i].updated) > cfg.lora_peer_timeout)) { 979 | peers[i].lost = 1; 980 | } 981 | 982 | } 983 | 984 | sys.num_peers_active = count_peers(1); 985 | stats.packets_total += sys.num_peers_active * cfg.cycle_stats / cfg.lora_cycle; 986 | stats.packets_received += sys.pps; 987 | stats.percent_received = (stats.packets_received > 0) ? constrain(100 * stats.packets_received / stats.packets_total, 0 ,100) : 0; 988 | 989 | /* 990 | if (sys.num_peers >= (cfg.lora_air_mode - 1)&& (cfg.lora_air_mode < LORA_NODES_MAX)) { 991 | cfg.lora_air_mode++; 992 | sys.lora_next_tx += cfg.lora_slot_spacing ; 993 | cfg.lora_cycle = cfg.lora_slot_spacing * cfg.lora_air_mode; 994 | } 995 | */ 996 | 997 | // Screen management 998 | 999 | if (!curr.state && !sys.display_enable) { // Aircraft is disarmed = Turning on the OLED 1000 | display.displayOn(); 1001 | sys.display_enable = 1; 1002 | } 1003 | 1004 | else if (curr.state && sys.display_enable) { // Aircraft is armed = Turning off the OLED 1005 | display.displayOff(); 1006 | sys.display_enable = 0; 1007 | } 1008 | 1009 | sys.stats_updated = sys.now; 1010 | } 1011 | 1012 | 1013 | // LED blinker 1014 | 1015 | if (sys.lora_tick % 6 == 0) { 1016 | if (sys.num_peers_active > 0) { 1017 | sys.io_led_changestate = millis() + IO_LEDBLINK_DURATION; 1018 | sys.io_led_count = 0; 1019 | sys.io_led_blink = 1; 1020 | } 1021 | } 1022 | 1023 | if (sys.io_led_blink && millis() > sys.io_led_changestate) { 1024 | 1025 | sys.io_led_count++; 1026 | sys.io_led_changestate += IO_LEDBLINK_DURATION; 1027 | 1028 | if (sys.io_led_count % 2 == 0) { 1029 | digitalWrite(LED, LOW); 1030 | } 1031 | else { 1032 | digitalWrite(LED, HIGH); 1033 | } 1034 | 1035 | if (sys.io_led_count >= sys.num_peers_active * 2) { 1036 | sys.io_led_blink = 0; 1037 | } 1038 | 1039 | } 1040 | 1041 | } 1042 | -------------------------------------------------------------------------------- /src/main.h: -------------------------------------------------------------------------------- 1 | #define VERSION "1.3" 2 | 3 | #define MODE_HOST_SCAN 0 4 | #define MODE_LORA_INIT 1 5 | #define MODE_LORA_SYNC 2 6 | #define MODE_LORA_RX 3 7 | #define MODE_LORA_TX 4 8 | 9 | #define LORA_NAME_LENGTH 6 10 | 11 | #define SERIAL_PIN_TX 23 12 | #define SERIAL_PIN_RX 17 13 | 14 | #define LORA_PERF_MODE 0 15 | 16 | #define LORA_NODES_MIN 2 17 | #define LORA_NODES_MAX 4 18 | 19 | #define LED 2 20 | #define IO_LEDBLINK_DURATION 160 21 | 22 | #define SCK 5 // GPIO5 - SX1278's SCK 23 | #define MISO 19 // GPIO19 - SX1278's MISO 24 | #define MOSI 27 // GPIO27 - SX1278's MOSI 25 | #define SS 18 // GPIO18 - SX1278's CS 26 | #define RST 14 // GPIO14 - SX1278's RESET 27 | #define DI0 26 // GPIO26 - SX1278's IRQ (interrupt request) 28 | 29 | #define HOST_NONE 0 30 | #define HOST_INAV 1 31 | #define HOST_BTFL 2 32 | 33 | char host_name[3][5]={"NoFC", "iNav", "Beta"}; 34 | char host_state[2][5]={"", "ARM"}; 35 | char peer_slotname[9][3]={"X", "A", "B", "C", "D", "E", "F", "G", "H"}; 36 | 37 | struct peer_t { 38 | uint8_t id; 39 | uint8_t host; 40 | uint8_t state; 41 | bool lost; 42 | uint8_t broadcast; 43 | uint32_t updated; 44 | uint32_t lq_updated; 45 | uint8_t lq_tick; 46 | uint8_t lq; 47 | int rssi; 48 | float distance; // --------- uint16_t 49 | int16_t direction; 50 | int16_t relalt; 51 | msp_raw_gps_t gps; 52 | msp_raw_gps_t gpsrec; 53 | msp_analog_t fcanalog; 54 | char name[LORA_NAME_LENGTH + 1]; 55 | }; 56 | 57 | struct curr_t { 58 | uint8_t id; 59 | uint8_t state; 60 | uint8_t host; 61 | char name[16]; 62 | uint8_t tick; 63 | msp_raw_gps_t gps; 64 | msp_fc_version_t fcversion; 65 | msp_analog_t fcanalog; 66 | }; 67 | 68 | struct air_type0_t { // 80 bits 69 | unsigned int id : 3; 70 | unsigned int type : 3; 71 | signed int lat : 25; // -9 000 000 to +9 000 000 -90x10e5 to +90x10e5 72 | signed int lon : 26; // -18 000 000 to +18 000 000 -180x10e5 to +180x10e5 73 | signed int alt : 14; // -8192m to +8192m 74 | unsigned int heading : 9; // 0 to 511° 75 | }; 76 | 77 | struct air_type1_t { // 80 bits 78 | unsigned int id : 3; 79 | unsigned int type : 3; 80 | unsigned int host : 3; 81 | unsigned int state : 3; 82 | unsigned int broadcast : 6; 83 | unsigned int speed : 6; // 64m/s 84 | char name[LORA_NAME_LENGTH]; // 6 char x 8 bits = 48 85 | unsigned int temp1 : 8; // Spare 86 | }; 87 | 88 | struct air_type2_t { // 80 bits 89 | unsigned int id : 3; 90 | unsigned int type : 3; 91 | unsigned int vbat : 8; 92 | unsigned int mah : 16; 93 | unsigned int rssi : 10; 94 | unsigned int temp1 : 20; // Spare 95 | unsigned int temp2 : 20; // Spare 96 | }; 97 | 98 | 99 | struct config_t { 100 | uint32_t lora_frequency; 101 | uint32_t lora_bandwidth; 102 | uint8_t lora_coding_rate; 103 | uint8_t lora_spreading_factor; 104 | uint8_t lora_power; 105 | uint8_t lora_nodes_max; 106 | uint16_t lora_slot_spacing; 107 | uint16_t lora_cycle; 108 | int16_t lora_timing_delay; 109 | uint8_t lora_antidrift_threshold; 110 | uint8_t lora_antidrift_correction; 111 | uint16_t lora_peer_timeout; 112 | 113 | uint8_t lora_air_mode; 114 | 115 | uint8_t msp_version; 116 | uint8_t msp_timeout; 117 | uint16_t msp_fc_timeout; 118 | uint16_t msp_after_tx_delay; 119 | 120 | uint16_t cycle_scan; 121 | uint16_t cycle_display; 122 | uint16_t cycle_stats; 123 | }; 124 | 125 | struct system_t { 126 | uint8_t phase; 127 | 128 | uint32_t now = 0; 129 | uint32_t now_sec = 0; 130 | 131 | uint8_t air_last_received_id = 0; 132 | int last_rssi; 133 | uint8_t pps = 0; 134 | uint8_t ppsc = 0; 135 | uint8_t num_peers = 0; 136 | uint8_t num_peers_active = 0; 137 | 138 | uint8_t lora_tick; 139 | bool lora_no_tx = 0; 140 | uint8_t lora_slot = 0; 141 | uint32_t lora_last_tx = 0; 142 | uint32_t lora_last_rx = 0; 143 | uint32_t lora_next_tx = 0; 144 | int32_t lora_drift = 0; 145 | int drift_correction = 0; 146 | 147 | uint32_t msp_next_cycle = 0; 148 | 149 | uint8_t display_page = 0; 150 | bool display_enable = 1; 151 | uint32_t display_updated = 0; 152 | 153 | uint32_t io_button_released = 0; 154 | bool io_button_pressed = 0; 155 | 156 | uint32_t cycle_scan_begin; 157 | uint32_t io_led_changestate; 158 | uint8_t io_led_count; 159 | uint8_t io_led_blink; 160 | uint32_t stats_updated = 0; 161 | 162 | char message[20]; 163 | }; 164 | 165 | struct stats_t { 166 | uint32_t timer_begin; 167 | uint32_t timer_end; 168 | float packets_total; 169 | uint32_t packets_received; 170 | uint8_t percent_received; 171 | uint16_t last_tx_duration; 172 | uint16_t last_rx_duration; 173 | uint16_t last_msp_duration[LORA_NODES_MAX]; 174 | uint16_t last_oled_duration; 175 | }; 176 | 177 | const uint8_t icon_lq_1[] PROGMEM = { 178 | B00000000, 179 | B00000000, 180 | B00000000, 181 | B00000000, 182 | B00000000, 183 | B00000000, 184 | B00000000, 185 | B00000011 186 | }; 187 | 188 | const uint8_t icon_lq_2[] PROGMEM = { 189 | B00000000, 190 | B00000000, 191 | B00000000, 192 | B00000000, 193 | B00000000, 194 | B00001111, 195 | B00000000, 196 | B00000011 197 | }; 198 | 199 | const uint8_t icon_lq_3[] PROGMEM = { 200 | B00000000, 201 | B00000000, 202 | B00000000, 203 | B00111111, 204 | B00000000, 205 | B00001111, 206 | B00000000, 207 | B00000011 208 | }; 209 | 210 | const uint8_t icon_lq_4[] PROGMEM = { 211 | B00000000, 212 | B11111111, 213 | B00000000, 214 | B00111111, 215 | B00000000, 216 | B00001111, 217 | B00000000, 218 | B00000011 219 | }; 220 | 221 | 222 | -------------------------------------------------------------------------------- /testing/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/testing/.DS_Store -------------------------------------------------------------------------------- /testing/air-to-air-433.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/testing/air-to-air-433.zip -------------------------------------------------------------------------------- /testing/air-to-air-868.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/testing/air-to-air-868.zip -------------------------------------------------------------------------------- /tools/mkspiffs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistyk/inavradar-ESP32/a5aa31f498d4c7fb4f02e86384134f62b22ffaf5/tools/mkspiffs --------------------------------------------------------------------------------