├── .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 | 
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 | [](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 | 
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 |
19 |
23 |
24 |
25 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
44 |
45 |
46 |
Connected to
47 |
DISARMED
48 |
0.00 V
49 |
GPS
50 |
51 |
52 |
53 |
54 | UAV Name
55 | State
56 | Position
57 |
58 |
59 |
60 |
61 |
62 |
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
--------------------------------------------------------------------------------