├── DateTime.cpp ├── DateTime.h ├── LICENSE ├── README.md ├── ST7735_esp32_80x160.h ├── design └── display0.jpg ├── gps-ntp-eth-esp32.ino ├── minmea.c └── minmea.h /DateTime.cpp: -------------------------------------------------------------------------------- 1 | //#include 2 | #include "DateTime.h" 3 | 4 | #define SECONDS_PER_DAY 86400L 5 | #define SECONDS_FROM_1900_TO_2000 3155673600 6 | #define SECONDS_FROM_1970_TO_2000 946684800 7 | 8 | //has to be const or compiler compaints 9 | const uint16_t daysInMonth [] PROGMEM = {31,28,31,30,31,30,31,31,30,31,30,31}; 10 | 11 | // number of days since 2000/01/01, valid for 2001..2099 12 | static uint16_t date2days(uint16_t y, uint16_t m, uint16_t d) { 13 | if (y >= 2000) { 14 | y -= 2000; 15 | } 16 | uint16_t days = d; 17 | for (uint16_t i = 1; i < m; ++i) { 18 | days += pgm_read_byte(daysInMonth + i - 1); 19 | } 20 | if (m > 2 && y % 4 == 0) { 21 | ++days; 22 | } 23 | return days + 365 * y + (y + 3) / 4 - 1; 24 | } 25 | 26 | static long time2long(uint16_t days, uint16_t h, uint16_t m, uint16_t s) { 27 | return ((days * 24L + h) * 60 + m) * 60 + s; 28 | } 29 | 30 | //////////////////////////////////////////////////////////////////////////////// 31 | // DateTime implementation - ignores time zones and DST changes 32 | // NOTE: also ignores leap seconds, see http://en.wikipedia.org/wiki/Leap_second 33 | 34 | DateTime::DateTime(uint32_t t, unsigned long microsfraction) 35 | : microsfraction_(microsfraction) { 36 | // bring to 2000 timestamp from 1900 37 | t -= SECONDS_FROM_1900_TO_2000; 38 | 39 | second_ = t % 60; 40 | t /= 60; 41 | minute_ = t % 60; 42 | t /= 60; 43 | hour_ = t % 24; 44 | uint16_t days = t / 24; 45 | uint16_t leap; 46 | for (year_ = 0; ; ++year_) { 47 | leap = year_ % 4 == 0; 48 | if (days < 365 + leap) { 49 | break; 50 | } 51 | days -= 365 + leap; 52 | } 53 | for (month_ = 1; ; ++month_) { 54 | uint16_t daysPerMonth = pgm_read_byte(daysInMonth + month_ - 1); 55 | if (leap && month_ == 2) { 56 | ++daysPerMonth; 57 | } 58 | if (days < daysPerMonth) { 59 | break; 60 | } 61 | days -= daysPerMonth; 62 | } 63 | day_ = days + 1; 64 | } 65 | 66 | void DateTime::time(uint32_t t) { 67 | // bring to 2000 timestamp from 1900 68 | t -= SECONDS_FROM_1900_TO_2000; 69 | 70 | second_ = t % 60; 71 | t /= 60; 72 | minute_ = t % 60; 73 | t /= 60; 74 | hour_ = t % 24; 75 | uint16_t days = t / 24; 76 | uint16_t leap; 77 | for (year_ = 0; ; ++year_) { 78 | leap = year_ % 4 == 0; 79 | if (days < 365 + leap) { 80 | break; 81 | } 82 | days -= 365 + leap; 83 | } 84 | for (month_ = 1; ; ++month_) { 85 | uint16_t daysPerMonth = pgm_read_byte(daysInMonth + month_ - 1); 86 | if (leap && month_ == 2) { 87 | ++daysPerMonth; 88 | } 89 | if (days < daysPerMonth) { 90 | break; 91 | } 92 | days -= daysPerMonth; 93 | } 94 | day_ = days + 1; 95 | } 96 | 97 | DateTime::DateTime( 98 | uint16_t year, uint16_t month, 99 | uint16_t day, uint16_t hour, 100 | uint16_t minute, uint16_t second, 101 | unsigned long microsfraction) 102 | : year_( (year >= 2000) ? year - 2000 : year), 103 | month_(month), 104 | day_(day), 105 | hour_(hour), 106 | minute_(minute), 107 | second_(second), 108 | microsfraction_(microsfraction) {} 109 | 110 | static uint16_t conv2d(const char *p) { 111 | uint16_t v = 0; 112 | if ('0' <= *p && *p <= '9') { 113 | v = *p - '0'; 114 | } 115 | return 10 * v + *++p - '0'; 116 | } 117 | 118 | // A convenient constructor for using "the compiler's time": 119 | // DateTime now (__DATE__, __TIME__); 120 | // NOTE: using PSTR would further reduce the RAM footprint 121 | DateTime::DateTime( 122 | const char *date, 123 | const char *time, 124 | unsigned long microsfraction) 125 | : microsfraction_(microsfraction) { 126 | // sample input: date = "Dec 26 2009", time = "12:34:56" 127 | year_ = conv2d(date + 9); 128 | // Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec 129 | switch (date[0]) { 130 | case 'J': 131 | month_ = date[1] == 'a' ? 1 : (month_ = date[2] == 'n' ? 6 : 7); 132 | break; 133 | case 'F': 134 | month_ = 2; 135 | break; 136 | case 'A': 137 | month_ = date[2] == 'r' ? 4 : 8; 138 | break; 139 | case 'M': 140 | month_ = date[2] == 'r' ? 3 : 5; 141 | break; 142 | case 'S': 143 | month_ = 9; 144 | break; 145 | case 'O': 146 | month_ = 10; 147 | break; 148 | case 'N': 149 | month_ = 11; 150 | break; 151 | case 'D': 152 | month_ = 12; 153 | break; 154 | default: 155 | break; 156 | } 157 | day_ = conv2d(date + 4); 158 | hour_ = conv2d(time); 159 | minute_ = conv2d(time + 3); 160 | second_ = conv2d(time + 6); 161 | 162 | 163 | /*uint16_t DateTime::dayOfWeek() const { 164 | uint16_t day = date2days(year_, month_, day_); 165 | 166 | // Jan 1, 2000 is a Saturday, i.e. returns 6 167 | return (day + 6) % 7; 168 | 169 | */ 170 | } 171 | 172 | uint32_t DateTime::ntptime(void) const 173 | { 174 | uint32_t t; 175 | uint16_t days = date2days(year_, month_, day_); 176 | t = time2long(days, hour_, minute_, second_); 177 | t += SECONDS_FROM_1900_TO_2000; 178 | 179 | return t; 180 | } 181 | 182 | uint32_t DateTime::unixtime(void) const 183 | { 184 | uint32_t t; 185 | uint16_t days = date2days(year_, month_, day_); 186 | t = time2long(days, hour_, minute_, second_); 187 | t += SECONDS_FROM_1970_TO_2000; 188 | 189 | return t; 190 | } 191 | 192 | String DateTime::toStringTime(void) const 193 | { 194 | char chartime[9]; 195 | sprintf(chartime, "%02d:%02d:%02d", hour(), minute(), second() ); 196 | return chartime; 197 | } 198 | 199 | 200 | String DateTime::toStringDate(void) const 201 | { 202 | char charDate[11]; 203 | sprintf(charDate, "%02d/%02d/%04d", day(), month(), year() ); 204 | return charDate; 205 | } 206 | 207 | -------------------------------------------------------------------------------- /DateTime.h: -------------------------------------------------------------------------------- 1 | #ifndef DATETIME_H_ 2 | #define DATETIME_H_ 3 | 4 | #include "Arduino.h" 5 | 6 | class DateTime { 7 | public: 8 | DateTime(uint32_t t = 0, unsigned long microsfraction = 0); 9 | DateTime(uint16_t year, uint16_t month, uint16_t day, 10 | uint16_t hour = 0, uint16_t minute = 0, uint16_t second = 0, 11 | unsigned long microsfraction = 0); 12 | DateTime(const char *date, const char *time, unsigned long microsfraction = 0); 13 | 14 | void time(uint32_t t); 15 | void microsfraction(unsigned long microsfraction) { 16 | microsfraction_ = microsfraction; 17 | }; 18 | 19 | uint16_t year() const { return 2000 + year_; } 20 | uint16_t month() const { return month_; } 21 | uint16_t day() const { return day_; } 22 | uint16_t hour() const { return hour_; } 23 | uint16_t minute() const { return minute_; } 24 | uint16_t second() const { return second_; } 25 | unsigned long microsfraction() const { return microsfraction_; } 26 | //uint16_t dayOfWeek(); //const;// { return dayOfWeek_;} 27 | 28 | // 32-bit times as seconds since 1/1/2000 29 | long secondstime() const; 30 | // 32-bit times as seconds since 1/1/1900 31 | uint32_t ntptime(void) const; 32 | // 32-bit times as seconds since 1/1/1970 33 | uint32_t unixtime(void) const; 34 | 35 | String toStringTime(void) const; 36 | String toStringDate(void) const; 37 | 38 | void print(void) { 39 | 40 | long diff2 = microsfraction(); 41 | Serial.print(F("UNIX: ")); 42 | Serial.print(unixtime()); 43 | Serial.print(F(",")); 44 | if (diff2 < 10) 45 | Serial.print("000000"); 46 | else if (diff2 < 100) 47 | Serial.print("00000"); 48 | else if (diff2 < 1000) 49 | Serial.print("0000"); 50 | else if (diff2 < 10000) 51 | Serial.print("000"); 52 | else if (diff2 < 100000) 53 | Serial.print("00"); 54 | else if (diff2 < 1000000) 55 | Serial.print("0"); 56 | 57 | Serial.println(diff2); 58 | 59 | Serial.print(F("NTP: ")); 60 | Serial.print(ntptime()); 61 | Serial.print(F(",")); 62 | if (diff2 < 10) 63 | Serial.print("000000"); 64 | else if (diff2 < 100) 65 | Serial.print("00000"); 66 | else if (diff2 < 1000) 67 | Serial.print("0000"); 68 | else if (diff2 < 10000) 69 | Serial.print("000"); 70 | else if (diff2 < 100000) 71 | Serial.print("00"); 72 | else if (diff2 < 1000000) 73 | Serial.print("0"); 74 | 75 | Serial.println(diff2); 76 | 77 | Serial.print(F("DATE: ")); 78 | Serial.print(day()); 79 | Serial.print(F(".")); 80 | Serial.print(month()); 81 | Serial.print(F(".")); 82 | Serial.println(year()); 83 | 84 | Serial.print(F("TIME: ")); 85 | Serial.print(hour()); 86 | Serial.print(F(":")); 87 | Serial.print(minute()); 88 | Serial.print(F(":")); 89 | Serial.print(second()); 90 | Serial.print(F(",")); 91 | if (diff2 < 10) 92 | Serial.print("000000"); 93 | else if (diff2 < 100) 94 | Serial.print("00000"); 95 | else if (diff2 < 1000) 96 | Serial.print("0000"); 97 | else if (diff2 < 10000) 98 | Serial.print("000"); 99 | else if (diff2 < 100000) 100 | Serial.print("00"); 101 | else if (diff2 < 1000000) 102 | Serial.print("0"); 103 | 104 | Serial.println(diff2); 105 | 106 | Serial.println(); 107 | }; 108 | 109 | protected: 110 | uint16_t year_, month_, day_, hour_, minute_, second_; 111 | unsigned long microsfraction_; 112 | }; 113 | 114 | #endif // DATETIME_H_ 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPS NTP Server based on WT32-ETH01 (ESP32) and UBLOX 2 | 3 | In this repository you will find a small project that implements the `NTP Server`. Date and time are synchronized through `GPS`, accuracy is achieved by means of `PPS`. Devices can work by `WiFi` and` Ethernet`. 4 | 5 | ## Used Hardware 6 | 7 | - GPS: ublox NEO-M8 8 | - Microcontroller: WT32-S 9 | - Ethernet: LAN8720 10 | - Display: ST7735 80x160 TFT 11 | - Antenna: active GPS antenna with 2m wire 12 | 13 | ## Wiring 14 | 15 | ``` 16 | GPS <-> WT32-ETH01 17 | ----------------- 18 | GND --> GND 19 | Tx --> PIN35 (Rx) 20 | Rx --> PIN17 (Tx) 21 | PPS --> PIN33 22 | VCC --> +5V 23 | 24 | TFT <-> WT32-ETH01 25 | ----------------- 26 | GND --> GND 27 | VCC --> +5V 28 | BLK --> +5V 29 | #MISO --> PIN36 30 | MOSI --> PIN15 31 | SCLK --> PIN14 32 | DC --> PIN4 33 | CS --> PIN12 34 | RST --> PIN2 35 | ``` 36 | 37 | ## Configuration 38 | 39 | Select connection mode. Choose suitable for you. 40 | ``` 41 | //#define OPERATE_AS_AP 42 | //#define OPERATE_AS_STA 43 | //#define OPERATE_AS_ETH 44 | ``` 45 | In the case of a wireless connection, specify the `ssid`,` pass` 46 | Since this device doesn't take leap seconds into account, the 'correct' time will be calculated with a simple offset. 47 | This offset affects the microsecond counter so fine control can be achieved. 48 | NOTE that this only ever can be a positive number! 49 | ``` 50 | #define TIMING_OFFSET_US 0001 51 | ``` 52 | 53 | ## A note on accuracy 54 | 55 | **Do NOT run your data center from this device! Go and buy a proper NTP server with an atomic clock in it!** 56 | 57 | There is no leap-second check and fine tuning is done via a manual set in a `#define` directive. If you decide to use the device in wifi client mode, the timing accuracy will also depend on the network the device is working on. 58 | The timing data might be erroneous when the 32-bit microsecond counter value overflows (which happens every 71.583 minutes), or when the GPS decides to send back garbage data due to incorrect information or weak satellite signal. 59 | Nothing checks whether the GPS data is actually realistic, so if you see on the display that it's 18th of January 1972 or 9th of May 2025, this is the time and date the NTP server will report. 60 | 61 | According to my measurements, the achievable timing accuracy is about +/- 50 milliseconds 90% confidence interval when the offset is adjusted correctly. 62 | Sometimes, when the server is processing packets rapidly due to hammering, an extra second delay might be introduced. A normal output with `sntp` should be something like: 63 | ``` 64 | $ sntp 192.168.4.1 65 | sntp 4.2.8p10@1.3728-o Tue Mar 21 14:36:42 UTC 2017 (136.200.1~2544) 66 | 2019-03-07 16:14:05.328536 (-0400) +0.010 +/- 0.008126 192.168.4.1 s1 no-leap 67 | 68 | $ ntpdate -q 192.168.4.1 69 | server 192.168.4.1, stratum 1, offset -0.000596, delay 0.02802 70 | 2 Mar 19:13:38 ntpdate[92566]: adjust time server 192.168.4.1 offset -0.000596 sec 71 | ``` 72 | If you see that the +/- deviation is in the range of a few hundred milliseconds, try syncing time again until you get a few milliseconds like in the example above. 73 | 74 | 75 | ## What's on the display: 76 | 77 | - In the first line you can observe the number of active satellites 78 | - The second line displays the connection type and IP 79 | - Large digits displayed UTC Time 80 | - The last line displays the current date. 81 | 82 | It is worth noting that the output of information on the screen is not a priority. Accuracy of readings may be unreliable. 83 | 84 | 85 | ![display0](design/display0.jpg) 86 | 87 | ## Credit 88 | 89 | https://github.com/ldijkman/WT32-ETH01-LAN-8720-RJ45- 90 | 91 | https://github.com/kosma/minmea 92 | 93 | https://github.com/UT2UH/PPS-ntp-server 94 | 95 | https://github.com/ha5dzs/PPS-ntp-server 96 | 97 | https://github.com/DennisSc/PPS-ntp-server 98 | 99 | https://wiki.iarduino.ru/page/NMEA-0183/ 100 | 101 | https://www.youtube.com/watch?v=AADmsRuzRRA 102 | 103 | https://github.com/Bodmer/TFT_eSPI 104 | -------------------------------------------------------------------------------- /ST7735_esp32_80x160.h: -------------------------------------------------------------------------------- 1 | // Setup for ESP32 and ST7735 80 x 160 TFT 2 | 3 | // See SetupX_Template.h for all options available 4 | 5 | #define ST7735_DRIVER 6 | 7 | 8 | #define TFT_WIDTH 80 9 | #define TFT_HEIGHT 160 10 | 11 | 12 | #define ST7735_GREENTAB160x80 13 | 14 | // For ST7735, ST7789 and ILI9341 ONLY, define the colour order IF the blue and red are swapped on your display 15 | // Try ONE option at a time to find the correct colour order for your display 16 | 17 | // #define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue 18 | #define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red 19 | 20 | #define TFT_MISO 36 21 | #define TFT_MOSI 15 22 | #define TFT_SCLK 14 23 | #define TFT_CS 12 // Chip select control pin 24 | #define TFT_DC 4 // Data Command control pin 25 | #define TFT_RST 2 // Reset pin (could connect to RST pin) 26 | //#define TFT_RST -1 // Set TFT_RST to -1 if display RESET is connected to ESP32 board RST 27 | 28 | #define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH 29 | #define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters 30 | #define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters 31 | #define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm 32 | #define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. 33 | #define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. 34 | //#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT 35 | #define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts 36 | 37 | #define SMOOTH_FONT 38 | 39 | 40 | //#define SPI_FREQUENCY 20000000 41 | #define SPI_FREQUENCY 27000000 // Actually sets it to 26.67MHz = 80/3 42 | -------------------------------------------------------------------------------- /design/display0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuratovAS/gps-ntp-eth-esp32/d9318eb47663cd38570d7ef85c3e5ce822eb9ac6/design/display0.jpg -------------------------------------------------------------------------------- /gps-ntp-eth-esp32.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * This piece of code uses the hardware UART to connect to the GPS, and fetches the time. 3 | * ntp seconds: Seconds since 01/01/1900! 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | /// ESP-related stuff. You can get these via the board manager 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | // You can get these libraries from the arduino library manager 17 | #include // Graphics and font library for ILI9341 driver chip 18 | #include 19 | 20 | // Local stuff. 21 | #include "minmea.h" 22 | #include "DateTime.h" // this helps with the NTP time stamp calculations. 23 | 24 | /* 25 | * Global settings 26 | */ 27 | // Debug mode 28 | #define DEBUG 29 | 30 | // Wi-Fi stuff. 31 | //#define OPERATE_AS_AP 32 | //OR 33 | //#define OPERATE_AS_STA 34 | //OR 35 | #define OPERATE_AS_ETH 36 | 37 | #ifdef OPERATE_AS_STA 38 | // Change this info with your network's name and password, if you want to use the client mode. 39 | const char ssid[] = "SSID"; //SSID of your network 40 | const char pass[] = "PASS"; //password of your WPA Network 41 | #endif 42 | 43 | #ifdef OPERATE_AS_AP 44 | const char ssid[] = "NTP"; //SSID of your network 45 | const char pass[] = "NTP"; //password of your WPA Network 46 | #define CHANNEL 9 // Wifi channel. Between 1 and 13, to your taste. 2.4 GHz. 47 | #define HIDE_SSID false // Don't hide SSID. 48 | #define MAX_CONNECTION 3 // How many clients we should handle simultaneously. between 0 and 8. 49 | // the IP will be 192.168.4.1. It can be cinfigured further, but I don't think it matters. 50 | #endif 51 | 52 | #ifdef OPERATE_AS_ETH 53 | #include 54 | #define ETH_ADDR 1 55 | #define ETH_POWER_PIN 16 // -1 // ??? Do not use it, it can cause conflict during the software reset. 56 | #define ETH_POWER_PIN_ALTERNATIVE 16 // 17 // ??? 57 | #define ETH_MDC_PIN 23 58 | #define ETH_MDIO_PIN 18 59 | #define ETH_TYPE ETH_PHY_LAN8720 60 | //#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT // ETH_CLOCK_GPIO0_IN // ??? 61 | #endif 62 | 63 | #define NTP_PORT 123 64 | #define NTP_PACKET_SIZE 48 65 | 66 | String ip = ""; // we use DHCP 67 | 68 | /* 69 | * Hardware pins 70 | */ 71 | // GPS 72 | #define GPS_BAUDRATE 115200 73 | #define GPS_PPS_PIN 33 // If your GPS doesn't have a PPS output, just comment out this line. 74 | #define GPS_RX_PIN 35 //16 75 | #define GPS_TX_PIN 17 76 | //#define GPS_EN_PIN 12 // If your GPS doesn't have a EN input 77 | 78 | /* IMPORTANT: 79 | * ALWAYS VERIFY TIMING ACCURACY BEFORE USING THIS DEVICE!!! 80 | * 81 | * Since this device doesn't take leap seconds into account, the 'correct' time will be calculated with a simple offset. 82 | * This offset affects the microsecond counter so fine control can be achieved. 83 | * NOTE that this only ever can be a positive number! 84 | * 85 | */ 86 | #define TIMING_OFFSET_US 0001 87 | 88 | /* 89 | * Library-provided high-level objects 90 | */ 91 | TFT_eSPI tft = TFT_eSPI(); // Invoke library, pins defined in User_Setup.h 92 | Ticker display_timer; // This controls how often the screen is updated. 93 | 94 | WiFiUDP Udp; // UDP handler. 95 | 96 | /* 97 | * Global variables 98 | */ 99 | struct event_s { 100 | uint8_t notSatellite : 1; 101 | uint8_t notPPS : 1; 102 | uint8_t newIP : 1; 103 | }; 104 | union sysStatus_u { 105 | uint8_t all; 106 | struct event_s event; 107 | } sysStatus; 108 | uint8_t sysStatusCache = 255; 109 | String chackIPCache; 110 | 111 | bool can_respond_to_packets = false; // Set to true when the data is parsed, set to false just after sending an NTP packet. 112 | char there_is_new_data = 0; // This is for the UART 113 | String uart_string; // A GPS string should not be any longer than this. 114 | unsigned int uart_string_length = 0; // This tells how long an NMEA sentence is, in bytes. 115 | bool update_the_display = false; // This is controlled from a timer. 116 | 117 | struct minmea_sentence_rmc rmc_frame; // $GPRMC frame, after minmea parsed it. 118 | struct minmea_sentence_gga gga_frame; // $GPRMC frame, after minmea parsed it. 119 | char gps_sentence[MINMEA_MAX_LENGTH]; // Character array, initialised as per the minmea lib. 120 | 121 | // these are for keeping time. 122 | unsigned long microsecond_counter = 0; // CPU microseconds 123 | int satellites_tracked = 0; // 124 | DateTime reference_time; // This is being updated by the G 125 | DateTime uart_time; // this is the DateTime structure decoded by the GPS. 126 | DateTime receive_time; // This is set on an incoming NTP request 127 | DateTime transmit_time; // This is set when transmitting the NTP packet. 128 | byte origTimeTs[9]; // The remote host's local time stamp. 129 | 130 | byte packetBuffer[NTP_PACKET_SIZE]; 131 | 132 | void setup() { 133 | // For randomisation, we need this: the ADC is not connected, so it reads noise, 134 | randomSeed(analogRead(A0)); 135 | 136 | // Wifi. 137 | #ifdef OPERATE_AS_AP 138 | // Stand-alone access point. 139 | WiFi.disconnect(true); // This re-initialises the wifi. 140 | WiFi.softAP(ssid, CHANNEL, HIDE_SSID, MAX_CONNECTION); 141 | #endif 142 | 143 | #ifdef OPERATE_AS_STA 144 | // If client, use these. 145 | WiFi.disconnect(true); // This re-initialises the wifi. 146 | WiFi.mode(WIFI_STA); 147 | WiFi.begin(ssid, pass); 148 | while (WiFi.status() != WL_CONNECTED) { 149 | delay(500); 150 | } 151 | #endif 152 | 153 | #ifdef OPERATE_AS_ETH 154 | pinMode(ETH_POWER_PIN_ALTERNATIVE, OUTPUT); 155 | digitalWrite(ETH_POWER_PIN_ALTERNATIVE, HIGH); 156 | #endif 157 | 158 | #ifdef GPS_EN_PIN 159 | pinMode(GPS_EN_PIN, OUTPUT); 160 | digitalWrite(GPS_EN_PIN, 0); // Disable the GPS receiver 161 | #endif 162 | 163 | // DEBUG: Serial port 164 | Serial.begin(115200); // GPS is connected to the uart's RX pin. 165 | 166 | // GPS: Serial port and enable pin 167 | Serial2.begin(GPS_BAUDRATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); 168 | 169 | #ifdef GPS_EN_PIN 170 | digitalWrite(GPS_EN_PIN, 1); // This turns on the GPS receiver, if hooked up. 171 | #endif 172 | 173 | // GPS: PPS-pin handling, if the PPS-pin is specified. 174 | #ifdef GPS_PPS_PIN 175 | pinMode(GPS_PPS_PIN, INPUT_PULLUP); 176 | attachInterrupt(digitalPinToInterrupt(GPS_PPS_PIN), pps_interrupt, FALLING); // Interrupt is triggered on falling edge. 177 | #endif 178 | uart_string.reserve(200); 179 | 180 | // Timer. 181 | display_timer.attach_ms(900, set_display_to_update); // This starts a display frame update, every 500 milliseconds. 182 | 183 | // Network. 184 | #ifdef OPERATE_AS_ETH 185 | //WiFi.onEvent(EthEvent); //attach ETH PHY event handler 186 | ETH.begin(ETH_ADDR, ETH_POWER_PIN, ETH_MDC_PIN, ETH_MDIO_PIN, ETH_TYPE, ETH_CLK_MODE); // Enable ETH 187 | ip = ETH.localIP().toString(); 188 | #endif 189 | 190 | Udp.begin(NTP_PORT); // start udp server 191 | 192 | // TFT screen 193 | tft.init(); 194 | tft.setRotation(3); 195 | tft.fillScreen(TFT_BLACK); 196 | 197 | // Debug message via uart. 198 | #ifdef DEBUG 199 | Serial.println("Hardware initialised, now entering loop."); 200 | 201 | #ifdef OPERATE_AS_ETH 202 | Serial.print("ETH IP:"); 203 | Serial.println(ETH.localIP()); 204 | #endif 205 | 206 | #ifdef OPERATE_AS_AP 207 | Serial.print("AP IP:"); 208 | Serial.println(WiFi.softAPIP()); 209 | #endif 210 | 211 | #ifdef OPERATE_AS_STA 212 | Serial.print("STA IP:"); 213 | Serial.println(WiFi.localIP()); 214 | #endif 215 | #endif 216 | } 217 | 218 | void loop() { 219 | /* 220 | * Do we have a GPS sentence? 221 | */ 222 | noInterrupts(); // disable interrupts while waiting for a sentence. 223 | if(Serial2.available()) 224 | { 225 | uart_string = Serial2.readStringUntil('\n'); // Read string until carriage return. 226 | there_is_new_data = 1; 227 | #ifdef DEBUG 228 | //Serial.println("Echo the GPS sentence"); 229 | //Serial.println(uart_string); // Echo the GPS sentence. 230 | #endif 231 | } 232 | interrupts(); // Re-enable interrupts after the string is received. 233 | 234 | /* 235 | * Parse the GPS sentence. 236 | */ 237 | if(there_is_new_data) 238 | { 239 | uart_string_length = uart_string.length(); 240 | uart_string.toCharArray(gps_sentence, uart_string_length); // Copy the string to a character array so minmea can process it. 241 | there_is_new_data = 0; 242 | uart_string = ""; // Clear this string. 243 | Serial2.flush(); // If there was something remaining in the buffer, it's gone now. 244 | parse_rmc(); // Update the date and time using gps_sentence 245 | can_respond_to_packets = true; // Now we can respond to NTP requests. 246 | } 247 | 248 | /* 249 | * Can we update the display? 250 | * (Ticker calls timer function, sets semaphore, and then execute this statement once.) 251 | */ 252 | 253 | if(update_the_display) 254 | { 255 | drive_display(); // Update the contents of the screen. 256 | update_the_display = false; // Make sure this is being executed once per call. 257 | } 258 | 259 | /* 260 | * Have we got a time sync request?? 261 | */ 262 | 263 | // process NTP requests 264 | IPAddress remoteIP; // this will store the remote hosts's IP address 265 | int remotePort; // the port it was sent from 266 | 267 | int packetSize = Udp.parsePacket(); 268 | 269 | if (packetSize && !sysStatus.event.notPPS && !sysStatus.event.notSatellite && can_respond_to_packets) // we've got a packet, and there is GPS fix, and we have received new GPS data since the last NTP reply 270 | { 271 | 272 | /* 273 | * Process NTP request. 274 | */ 275 | 276 | // Disable interrupts for this time. 277 | //noInterrupts(); 278 | receive_time = get_time_now(); // Log the time the packet came in 279 | 280 | //store sender ip and port for later use 281 | remoteIP = Udp.remoteIP(); 282 | remotePort = Udp.remotePort(); 283 | 284 | #ifdef DEBUG 285 | // Some very useful debug stuff. But it takes time! 286 | Serial.print("*********************************\r\nReceived UDP packet with "); 287 | Serial.print(packetSize); 288 | Serial.print(" bytes size - "); 289 | Serial.print("SourceIP "); 290 | for (uint8_t i =0; i < 4; i++) 291 | { 292 | Serial.print(remoteIP[i], DEC); 293 | if (i < 3) 294 | { 295 | Serial.print("."); 296 | } 297 | } 298 | Serial.print(", Port "); 299 | Serial.println(remotePort); 300 | Serial.print("query: "); 301 | Serial.print(receive_time.toStringTime()); 302 | Serial.print(","); 303 | Serial.print(receive_time.microsfraction()); 304 | Serial.println(); 305 | #endif 306 | 307 | // We've received a packet, read the data from it 308 | // read the packet into the buffer 309 | Udp.read(packetBuffer, NTP_PACKET_SIZE); 310 | 311 | //get client transmit time (becomes originTime in reply packet) 312 | for (int i = 0; i < 8; i++) 313 | origTimeTs[i] = packetBuffer[40+i]; 314 | 315 | sendNTPpacket(remoteIP, remotePort); 316 | 317 | #ifdef DEBUG 318 | //send NTP reply 319 | Serial.print("reply: "); 320 | Serial.print(transmit_time.toStringTime()); 321 | Serial.print(","); 322 | Serial.println(transmit_time.microsfraction()); 323 | Serial.println("NTP reply sent.\r\n*********************************"); 324 | #endif 325 | 326 | //Re-enable the interrupts 327 | //interrupts(); 328 | 329 | //can_respond_to_packets == false; // This is set so no NTP responses will be sent until we have new GPS data. 330 | } 331 | } 332 | 333 | void parse_rmc() 334 | { 335 | // This function parses the $GPRMC NMEA sentence. This function works with global variables, and updates the reference_time accordingly. 336 | switch(minmea_sentence_id(gps_sentence, true)) 337 | { 338 | // In this function, the boolean value enables GPS sentence checksum verification. 339 | case MINMEA_SENTENCE_RMC: 340 | if(minmea_parse_rmc(&rmc_frame, gps_sentence)) 341 | { 342 | sysStatus.event.notSatellite = false; 343 | 344 | // Checking for zero data with GPS 345 | if (rmc_frame.date.year == -1 || rmc_frame.date.month == -1 || rmc_frame.date.day == -1 || rmc_frame.time.hours == -1 || rmc_frame.time.minutes == -1 || rmc_frame.time.seconds == -1) 346 | sysStatus.event.notSatellite = true; 347 | else 348 | uart_time = DateTime((uint16_t)rmc_frame.date.year, (uint16_t)rmc_frame.date.month, (uint16_t)rmc_frame.date.day, (uint16_t)rmc_frame.time.hours, (uint16_t)rmc_frame.time.minutes, (uint16_t)rmc_frame.time.seconds, TIMING_OFFSET_US); // (uint16_t)61, (unsigned long)rmc_frame.time.microseconds); 349 | 350 | // For some reason, my GPS returns dummy data when it can't find fix. Not sure why this happens. 351 | // well, this will be a problem in 2036. This was written in 2019, so there is time to fix it :) 352 | if(reference_time.unixtime() == (uint32_t)2085978497) 353 | sysStatus.event.notSatellite = true; 354 | 355 | // Update the microsecond counter too, if no PPS pin is assigned. 356 | #ifndef GPS_PPS_PIN 357 | pps_interrupt(); // If the system had a GPS_PPS_PIN, this function would be executed in an interrupt. 358 | #endif 359 | //Serial.println("RMC sentence received."); 360 | } 361 | else 362 | { 363 | // If the parsing failed 364 | //Serial.println("Bad RMC sentence!"); 365 | } 366 | break; 367 | 368 | case MINMEA_SENTENCE_GGA: 369 | if(minmea_parse_gga(&gga_frame, gps_sentence)) 370 | satellites_tracked = gga_frame.satellites_tracked; 371 | //Serial.println("GGA sentence received."); 372 | break; 373 | case MINMEA_SENTENCE_GSA: 374 | //Serial.println("GSA sentence received."); 375 | break; 376 | 377 | case MINMEA_SENTENCE_VTG: 378 | //Serial.println("VTG sentence received."); 379 | break; 380 | 381 | case MINMEA_SENTENCE_ZDA: 382 | //Serial.println("ZDA sentence received."); 383 | break; 384 | 385 | default: 386 | //Do nothing. 387 | break; 388 | 389 | } 390 | 391 | } 392 | 393 | DateTime get_time_now(void) 394 | { 395 | /* 396 | * this function derives the time from the reference time, and gets the number of microseconds since the last GPS update. 397 | * It derives time from reference_time, which is set by the GPS, and the PPS interrupt, if any. 398 | */ 399 | DateTime stuff_to_return; // This is what we are going to return. 400 | unsigned long current_microsecond_counter = micros(); // Get the number of microseconds 401 | uint32_t current_reference_time = reference_time.ntptime(); // This gets the time as per NTP 402 | // This calculates the time difference since the last $GPRMC NMEA sentence in microseconds, and takes a manual offset into account 403 | unsigned long microsecond_difference = (current_microsecond_counter - microsecond_counter) + TIMING_OFFSET_US; 404 | 405 | // debug stuff 406 | /*Serial.print("current_microsecond_counter: "); 407 | Serial.println(current_microsecond_counter); 408 | Serial.print("microsecond_counter: "); 409 | Serial.println(microsecond_counter); 410 | Serial.print("microsecond_difference: "); 411 | Serial.println(microsecond_difference);*/ 412 | 413 | // Did the PPS impulses stop happening? Make this crash! 414 | if( (microsecond_difference > 5000000) || (microsecond_difference > -5000000)) 415 | { 416 | sysStatus.event.notPPS = true; 417 | #ifdef DEBUG 418 | Serial.println("No GPS data or PPS pulse was received in the past 5 seconds."); 419 | #endif 420 | } 421 | else 422 | sysStatus.event.notPPS = false; 423 | 424 | 425 | // Did we have a variable overflow? 426 | if(microsecond_counter > current_microsecond_counter) 427 | { 428 | microsecond_difference = 0; // This is going to introduce a an error up to a second at variable overflow. 429 | //microsecond_difference = (current_microsecond_counter - ((unsigned long)-1 - microsecond_counter)) + TIMING_OFFSET_US; 430 | } 431 | // Did we experience time longer than a second? A Lot longer too? Compensate. 432 | while( (microsecond_difference >= 10000000)) 433 | { 434 | // If we get a larger than 1 second here, we have a problem. 435 | microsecond_difference = microsecond_difference - 1000000; // Remove the extra second offset 436 | //current_reference_time += 1; //...and add it to the time. 437 | 438 | } 439 | 440 | // Now let's assemble the new DateTime object so we can return it. 441 | stuff_to_return = DateTime(current_reference_time, microsecond_difference); 442 | return stuff_to_return; 443 | } 444 | 445 | uint64_t DateTimeToNtp64(DateTime time_to_send) 446 | { 447 | /* 448 | * This function generates the time information required for the NTP packet. 449 | */ 450 | uint64_t time_stamp; // 64-bit time stamp. 451 | 452 | time_stamp = (((uint64_t)time_to_send.ntptime()) << 32); // Shove it to the top 32 bits. 453 | time_stamp |= (uint64_t)(time_to_send.microsfraction() * 4294.967296); // Add the lower 32-bit nibble, which is the precise information 454 | 455 | return (time_stamp); 456 | } 457 | 458 | // send NTP reply to the given address 459 | void sendNTPpacket(IPAddress remoteIP, int remotePort) 460 | { 461 | /* 462 | * This function assembles an NTP packet, and sends it back to the host requesting it. 463 | */ 464 | 465 | // LI: 0, Version: 4, Mode: 4 (server) 466 | //packetBuffer[0] = 0b00100100; 467 | // Not a leap second (LI=0), NTP version: 3, Mode: 4 (server) 468 | packetBuffer[0] = 0b00011100; 469 | 470 | // Stratum, or type of clock. Since we have the clock derived from a GPS, we are stratum 1, no matter how inaccurate are we. 471 | packetBuffer[1] = 0b00000001; 472 | 473 | // Polling Interval: the log2 value of the maximum interval between souccessive messages 474 | packetBuffer[2] = 2; // Was 4. 475 | 476 | // Peer Clock Precision 477 | // log2(sec) 478 | // 0xF6 <--> -10 <--> 0.0009765625 s 479 | // 0xF7 <--> -9 <--> 0.001953125 s 480 | // 0xF8 <--> -8 <--> 0.00390625 s 481 | // 0xF9 <--> -7 <--> 0.0078125 s 482 | // 0xFA <--> -6 <--> 0.0156250 s 483 | // 0xFB <--> -5 <--> 0.0312500 s 484 | // 0xFC <--> -4 <--> 0.0625 s 485 | // 0xFD <--> -3 <--> 0.125 s 486 | 487 | #ifndef GPS_PPS_PIN 488 | // report a worse precision if a GPS without PPS output was used. 489 | packetBuffer[3] = 0xFC; 490 | #else 491 | packetBuffer[3] = 0xF6; // the actual clock precision is better, but let's be conservative, this is just a microcontroller! 492 | #endif 493 | 494 | // 8 bytes for Root Delay & Root Dispersion 495 | // Root delay is 0, becuase we got our clock from a GPS. 496 | packetBuffer[4] = 0; 497 | packetBuffer[5] = 0; 498 | packetBuffer[6] = 0; 499 | packetBuffer[7] = 0; 500 | 501 | // Root dispersion. Refers to clock frequency tolerance. Well, I guess this is a bit optimistic :) 502 | packetBuffer[8] = 0; 503 | packetBuffer[9] = 0; 504 | packetBuffer[10] = 0; 505 | packetBuffer[11] = 0x50; 506 | 507 | // Time source is GPS. The external reference source code is GPS. 508 | packetBuffer[12] = 71; // G 509 | packetBuffer[13] = 80; // P 510 | packetBuffer[14] = 83; // S 511 | packetBuffer[15] = 0; 512 | 513 | // Reference Time. 514 | uint64_t refT = DateTimeToNtp64(get_time_now()); // This one fetches the current time. 515 | 516 | packetBuffer[16] = (int)((refT >> 56) & 0xFF); 517 | packetBuffer[17] = (int)((refT >> 48) & 0xFF); 518 | packetBuffer[18] = (int)((refT >> 40) & 0xFF); 519 | packetBuffer[19] = (int)((refT >> 32) & 0xFF); 520 | packetBuffer[20] = (int)((refT >> 24) & 0xFF); 521 | packetBuffer[21] = (int)((refT >> 16) & 0xFF); 522 | packetBuffer[22] = (int)((refT >> 8) & 0xFF); 523 | packetBuffer[23] = (int)(refT & 0xFF); 524 | 525 | // Origin Time 526 | //copy old transmit time to origtime 527 | 528 | for (int i = 24; i < 32; i++) 529 | packetBuffer[i] = origTimeTs[i-24]; 530 | 531 | // write Receive Time to bytes 32-39 532 | refT = DateTimeToNtp64(receive_time); 533 | 534 | packetBuffer[32] = (int)((refT >> 56) & 0xFF); 535 | packetBuffer[33] = (int)((refT >> 48) & 0xFF); 536 | packetBuffer[34] = (int)((refT >> 40) & 0xFF); 537 | packetBuffer[35] = (int)((refT >> 32) & 0xFF); 538 | packetBuffer[36] = (int)((refT >> 24) & 0xFF); 539 | packetBuffer[37] = (int)((refT >> 16) & 0xFF); 540 | packetBuffer[38] = (int)((refT >> 8) & 0xFF); 541 | packetBuffer[39] = (int)(refT & 0xFF); 542 | 543 | // get current time + write as Transmit Time to bytes 40-47 544 | transmit_time = get_time_now(); 545 | refT = DateTimeToNtp64(transmit_time); 546 | 547 | packetBuffer[40] = (int)((refT >> 56) & 0xFF); 548 | packetBuffer[41] = (int)((refT >> 48) & 0xFF); 549 | packetBuffer[42] = (int)((refT >> 40) & 0xFF); 550 | packetBuffer[43] = (int)((refT >> 32) & 0xFF); 551 | packetBuffer[44] = (int)((refT >> 24) & 0xFF); 552 | packetBuffer[45] = (int)((refT >> 16) & 0xFF); 553 | packetBuffer[46] = (int)((refT >> 8) & 0xFF); 554 | packetBuffer[47] = (int)(refT & 0xFF); 555 | 556 | // send reply: 557 | Udp.beginPacket(remoteIP, remotePort); 558 | Udp.write(packetBuffer, NTP_PACKET_SIZE); 559 | Udp.endPacket(); 560 | 561 | } 562 | 563 | // This is the interrupt function. 564 | void pps_interrupt() 565 | { 566 | // Log the microseconds counter, so we know what the time was when the interrupt happened. 567 | microsecond_counter = micros(); 568 | // This function sets the reference time, every second. 569 | uint32_t reference_time_to_process_in_ntp = uart_time.ntptime(); // Save the current time as NTP time. 570 | 571 | #ifdef GPS_PPS_PIN 572 | reference_time_to_process_in_ntp++; // Increase the number of seconds, when using PPS interrupt. 573 | #endif 574 | reference_time = DateTime(reference_time_to_process_in_ntp, (unsigned long)TIMING_OFFSET_US); // This updates the global reference time. 575 | 576 | // Print the current time, so we can check the fraction stuff. 577 | // reference_time.print(); 578 | } 579 | 580 | void set_display_to_update(void) 581 | { 582 | // This function adjusts a semaphore, which controls the display update. The Adafruit library doesn't seem to like being used from an interrupt function. 583 | update_the_display = true; // Set this global variable. The display is updated from the loop() function. 584 | } 585 | 586 | void drive_display() 587 | { 588 | chackIP(); 589 | 590 | // Refresh screen after changing error flags 591 | if(sysStatusCache != sysStatus.all) 592 | { 593 | sysStatusCache = sysStatus.all; 594 | tft.fillScreen(TFT_BLACK); 595 | 596 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 597 | tft.setCursor(5, 16 * 0, 2); // 16 598 | tft.println("GPS NTP Server"); 599 | 600 | tft.setTextColor(TFT_GREEN,TFT_BLACK); 601 | tft.setCursor(5, 16 * 1, 2); 602 | #ifdef OPERATE_AS_ETH 603 | tft.print("ETH: "); 604 | #endif 605 | #ifdef OPERATE_AS_AP 606 | tft.print("AP: "); 607 | #endif 608 | #ifdef OPERATE_AS_STA 609 | tft.print("STA: "); 610 | #endif 611 | tft.println(ip); 612 | sysStatus.event.newIP = 0; 613 | } 614 | 615 | //satellites tracked 616 | tft.setTextColor(TFT_ORANGE,TFT_BLACK); 617 | tft.setCursor(135, 0, 2); 618 | char charDate[4]; 619 | sprintf(charDate, "S%02d", satellites_tracked); 620 | tft.print(charDate); 621 | 622 | // 623 | DateTime time_tmp = get_time_now(); 624 | if(!(sysStatus.event.notSatellite || sysStatus.event.notPPS)) 625 | { 626 | tft.setTextColor(TFT_MAGENTA,TFT_BLACK); 627 | tft.setCursor(28, 5 + 16 * 2, 4); // 26 628 | tft.println(time_tmp.toStringTime()); 629 | 630 | tft.setTextColor(0xFC9F,TFT_BLACK); 631 | tft.setCursor(38, 5 + 16 * 2 + 21, 2); 632 | tft.println(time_tmp.toStringDate());//reference_time 633 | } 634 | else 635 | { 636 | if(sysStatus.event.notSatellite) 637 | { 638 | tft.setTextColor(TFT_RED,TFT_BLACK); 639 | tft.setCursor(38, 16 * 2, 2); 640 | tft.println("GPS ERROR"); 641 | } 642 | if(sysStatus.event.notPPS) 643 | { 644 | tft.setTextColor(TFT_RED,TFT_BLACK); 645 | tft.setCursor(38, 16 * 3, 2); 646 | tft.println("PPS ERROR"); 647 | } 648 | 649 | } 650 | } 651 | 652 | //String IP; 653 | int countNotIP = 0; 654 | void chackIP() 655 | { 656 | #ifdef OPERATE_AS_ETH 657 | ip = ETH.localIP().toString(); 658 | //There is a bug. disconnected once a day 659 | if(ip == "0.0.0.0") 660 | { 661 | countNotIP++; 662 | if(countNotIP >= 5) 663 | { 664 | //Serial.print("init eth"); 665 | countNotIP = 0; 666 | ETH.begin(ETH_ADDR, ETH_POWER_PIN, ETH_MDC_PIN, ETH_MDIO_PIN, ETH_TYPE, ETH_CLK_MODE); // Enable ETH 667 | } 668 | } 669 | #endif 670 | #ifdef OPERATE_AS_AP 671 | ip = WiFi.softAPIP().toString(); 672 | #endif 673 | #ifdef OPERATE_AS_STA 674 | ip = WiFi.localIP().toString(); 675 | #endif 676 | if(chackIPCache != ip) 677 | { 678 | chackIPCache = ip; 679 | sysStatus.event.newIP = 1; 680 | } 681 | } 682 | -------------------------------------------------------------------------------- /minmea.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014 Kosma Moczek 3 | * This program is free software. It comes without any warranty, to the extent 4 | * permitted by applicable law. You can redistribute it and/or modify it under 5 | * the terms of the Do What The Fuck You Want To Public License, Version 2, as 6 | * published by Sam Hocevar. See the COPYING file for more details. 7 | */ 8 | 9 | // from https://github.com/kosma/minmea.git 10 | 11 | #include "minmea.h" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #define boolstr(s) ((s) ? "true" : "false") 20 | 21 | static int hex2int(char c) 22 | { 23 | if (c >= '0' && c <= '9') 24 | return c - '0'; 25 | if (c >= 'A' && c <= 'F') 26 | return c - 'A' + 10; 27 | if (c >= 'a' && c <= 'f') 28 | return c - 'a' + 10; 29 | return -1; 30 | } 31 | 32 | uint8_t minmea_checksum(const char *sentence) 33 | { 34 | // Support senteces with or without the starting dollar sign. 35 | if (*sentence == '$') 36 | sentence++; 37 | 38 | uint8_t checksum = 0x00; 39 | 40 | // The optional checksum is an XOR of all bytes between "$" and "*". 41 | while (*sentence && *sentence != '*') 42 | checksum ^= *sentence++; 43 | 44 | return checksum; 45 | } 46 | 47 | bool minmea_check(const char *sentence, bool strict) 48 | { 49 | uint8_t checksum = 0x00; 50 | 51 | // Sequence length is limited. 52 | if (strlen(sentence) > MINMEA_MAX_LENGTH + 3) 53 | return false; 54 | 55 | // A valid sentence starts with "$". 56 | if (*sentence++ != '$') 57 | return false; 58 | 59 | // The optional checksum is an XOR of all bytes between "$" and "*". 60 | while (*sentence && *sentence != '*' && isprint((unsigned char) *sentence)) 61 | checksum ^= *sentence++; 62 | 63 | // If checksum is present... 64 | if (*sentence == '*') { 65 | // Extract checksum. 66 | sentence++; 67 | int upper = hex2int(*sentence++); 68 | if (upper == -1) 69 | return false; 70 | int lower = hex2int(*sentence++); 71 | if (lower == -1) 72 | return false; 73 | int expected = upper << 4 | lower; 74 | 75 | // Check for checksum mismatch. 76 | if (checksum != expected) 77 | return false; 78 | } else if (strict) { 79 | // Discard non-checksummed frames in strict mode. 80 | return false; 81 | } 82 | 83 | // The only stuff allowed at this point is a newline. 84 | if (*sentence && strcmp(sentence, "\n") && strcmp(sentence, "\r\n")) 85 | return false; 86 | 87 | return true; 88 | } 89 | 90 | static inline bool minmea_isfield(char c) { 91 | return isprint((unsigned char) c) && c != ',' && c != '*'; 92 | } 93 | 94 | bool minmea_scan(const char *sentence, const char *format, ...) 95 | { 96 | bool result = false; 97 | bool optional = false; 98 | va_list ap; 99 | va_start(ap, format); 100 | 101 | const char *field = sentence; 102 | #define next_field() \ 103 | do { \ 104 | /* Progress to the next field. */ \ 105 | while (minmea_isfield(*sentence)) \ 106 | sentence++; \ 107 | /* Make sure there is a field there. */ \ 108 | if (*sentence == ',') { \ 109 | sentence++; \ 110 | field = sentence; \ 111 | } else { \ 112 | field = NULL; \ 113 | } \ 114 | } while (0) 115 | 116 | while (*format) { 117 | char type = *format++; 118 | 119 | if (type == ';') { 120 | // All further fields are optional. 121 | optional = true; 122 | continue; 123 | } 124 | 125 | if (!field && !optional) { 126 | // Field requested but we ran out if input. Bail out. 127 | goto parse_error; 128 | } 129 | 130 | switch (type) { 131 | case 'c': { // Single character field (char). 132 | char value = '\0'; 133 | 134 | if (field && minmea_isfield(*field)) 135 | value = *field; 136 | 137 | *va_arg(ap, char *) = value; 138 | } break; 139 | 140 | case 'd': { // Single character direction field (int). 141 | int value = 0; 142 | 143 | if (field && minmea_isfield(*field)) { 144 | switch (*field) { 145 | case 'N': 146 | case 'E': 147 | value = 1; 148 | break; 149 | case 'S': 150 | case 'W': 151 | value = -1; 152 | break; 153 | default: 154 | goto parse_error; 155 | } 156 | } 157 | 158 | *va_arg(ap, int *) = value; 159 | } break; 160 | 161 | case 'f': { // Fractional value with scale (struct minmea_float). 162 | int sign = 0; 163 | int_least32_t value = -1; 164 | int_least32_t scale = 0; 165 | 166 | if (field) { 167 | while (minmea_isfield(*field)) { 168 | if (*field == '+' && !sign && value == -1) { 169 | sign = 1; 170 | } else if (*field == '-' && !sign && value == -1) { 171 | sign = -1; 172 | } else if (isdigit((unsigned char) *field)) { 173 | int digit = *field - '0'; 174 | if (value == -1) 175 | value = 0; 176 | if (value > (INT_LEAST32_MAX-digit) / 10) { 177 | /* we ran out of bits, what do we do? */ 178 | if (scale) { 179 | /* truncate extra precision */ 180 | break; 181 | } else { 182 | /* integer overflow. bail out. */ 183 | goto parse_error; 184 | } 185 | } 186 | value = (10 * value) + digit; 187 | if (scale) 188 | scale *= 10; 189 | } else if (*field == '.' && scale == 0) { 190 | scale = 1; 191 | } else if (*field == ' ') { 192 | /* Allow spaces at the start of the field. Not NMEA 193 | * conformant, but some modules do this. */ 194 | if (sign != 0 || value != -1 || scale != 0) 195 | goto parse_error; 196 | } else { 197 | goto parse_error; 198 | } 199 | field++; 200 | } 201 | } 202 | 203 | if ((sign || scale) && value == -1) 204 | goto parse_error; 205 | 206 | if (value == -1) { 207 | /* No digits were scanned. */ 208 | value = 0; 209 | scale = 0; 210 | } else if (scale == 0) { 211 | /* No decimal point. */ 212 | scale = 1; 213 | } 214 | if (sign) 215 | value *= sign; 216 | 217 | *va_arg(ap, struct minmea_float *) = (struct minmea_float) {value, scale}; 218 | } break; 219 | 220 | case 'i': { // Integer value, default 0 (int). 221 | int value = 0; 222 | 223 | if (field) { 224 | char *endptr; 225 | value = strtol(field, &endptr, 10); 226 | if (minmea_isfield(*endptr)) 227 | goto parse_error; 228 | } 229 | 230 | *va_arg(ap, int *) = value; 231 | } break; 232 | 233 | case 's': { // String value (char *). 234 | char *buf = va_arg(ap, char *); 235 | 236 | if (field) { 237 | while (minmea_isfield(*field)) 238 | *buf++ = *field++; 239 | } 240 | 241 | *buf = '\0'; 242 | } break; 243 | 244 | case 't': { // NMEA talker+sentence identifier (char *). 245 | // This field is always mandatory. 246 | if (!field) 247 | goto parse_error; 248 | 249 | if (field[0] != '$') 250 | goto parse_error; 251 | for (int f=0; f<5; f++) 252 | if (!minmea_isfield(field[1+f])) 253 | goto parse_error; 254 | 255 | char *buf = va_arg(ap, char *); 256 | memcpy(buf, field+1, 5); 257 | buf[5] = '\0'; 258 | } break; 259 | 260 | case 'D': { // Date (int, int, int), -1 if empty. 261 | struct minmea_date *date = va_arg(ap, struct minmea_date *); 262 | 263 | int d = -1, m = -1, y = -1; 264 | 265 | if (field && minmea_isfield(*field)) { 266 | // Always six digits. 267 | for (int f=0; f<6; f++) 268 | if (!isdigit((unsigned char) field[f])) 269 | goto parse_error; 270 | 271 | char dArr[] = {field[0], field[1], '\0'}; 272 | char mArr[] = {field[2], field[3], '\0'}; 273 | char yArr[] = {field[4], field[5], '\0'}; 274 | d = strtol(dArr, NULL, 10); 275 | m = strtol(mArr, NULL, 10); 276 | y = strtol(yArr, NULL, 10); 277 | } 278 | 279 | date->day = d; 280 | date->month = m; 281 | date->year = y; 282 | } break; 283 | 284 | case 'T': { // Time (int, int, int, int), -1 if empty. 285 | struct minmea_time *time_ = va_arg(ap, struct minmea_time *); 286 | 287 | int h = -1, i = -1, s = -1, u = -1; 288 | 289 | if (field && minmea_isfield(*field)) { 290 | // Minimum required: integer time. 291 | for (int f=0; f<6; f++) 292 | if (!isdigit((unsigned char) field[f])) 293 | goto parse_error; 294 | 295 | char hArr[] = {field[0], field[1], '\0'}; 296 | char iArr[] = {field[2], field[3], '\0'}; 297 | char sArr[] = {field[4], field[5], '\0'}; 298 | h = strtol(hArr, NULL, 10); 299 | i = strtol(iArr, NULL, 10); 300 | s = strtol(sArr, NULL, 10); 301 | field += 6; 302 | 303 | // Extra: fractional time. Saved as microseconds. 304 | if (*field++ == '.') { 305 | uint32_t value = 0; 306 | uint32_t scale = 1000000LU; 307 | while (isdigit((unsigned char) *field) && scale > 1) { 308 | value = (value * 10) + (*field++ - '0'); 309 | scale /= 10; 310 | } 311 | u = value * scale; 312 | } else { 313 | u = 0; 314 | } 315 | } 316 | 317 | time_->hours = h; 318 | time_->minutes = i; 319 | time_->seconds = s; 320 | time_->microseconds = u; 321 | } break; 322 | 323 | case '_': { // Ignore the field. 324 | } break; 325 | 326 | default: { // Unknown. 327 | goto parse_error; 328 | } 329 | } 330 | 331 | next_field(); 332 | } 333 | 334 | result = true; 335 | 336 | parse_error: 337 | va_end(ap); 338 | return result; 339 | } 340 | 341 | bool minmea_talker_id(char talker[3], const char *sentence) 342 | { 343 | char type[6]; 344 | if (!minmea_scan(sentence, "t", type)) 345 | return false; 346 | 347 | talker[0] = type[0]; 348 | talker[1] = type[1]; 349 | talker[2] = '\0'; 350 | 351 | return true; 352 | } 353 | 354 | enum minmea_sentence_id minmea_sentence_id(const char *sentence, bool strict) 355 | { 356 | if (!minmea_check(sentence, strict)) 357 | return MINMEA_INVALID; 358 | 359 | char type[6]; 360 | if (!minmea_scan(sentence, "t", type)) 361 | return MINMEA_INVALID; 362 | 363 | if (!strcmp(type+2, "RMC")) 364 | return MINMEA_SENTENCE_RMC; 365 | if (!strcmp(type+2, "GGA")) 366 | return MINMEA_SENTENCE_GGA; 367 | if (!strcmp(type+2, "GSA")) 368 | return MINMEA_SENTENCE_GSA; 369 | if (!strcmp(type+2, "GLL")) 370 | return MINMEA_SENTENCE_GLL; 371 | if (!strcmp(type+2, "GST")) 372 | return MINMEA_SENTENCE_GST; 373 | if (!strcmp(type+2, "GSV")) 374 | return MINMEA_SENTENCE_GSV; 375 | if (!strcmp(type+2, "VTG")) 376 | return MINMEA_SENTENCE_VTG; 377 | if (!strcmp(type+2, "ZDA")) 378 | return MINMEA_SENTENCE_ZDA; 379 | 380 | return MINMEA_UNKNOWN; 381 | } 382 | 383 | bool minmea_parse_rmc(struct minmea_sentence_rmc *frame, const char *sentence) 384 | { 385 | // $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62 386 | char type[6]; 387 | char validity; 388 | int latitude_direction; 389 | int longitude_direction; 390 | int variation_direction; 391 | if (!minmea_scan(sentence, "tTcfdfdffDfd", 392 | type, 393 | &frame->time, 394 | &validity, 395 | &frame->latitude, &latitude_direction, 396 | &frame->longitude, &longitude_direction, 397 | &frame->speed, 398 | &frame->course, 399 | &frame->date, 400 | &frame->variation, &variation_direction)) 401 | return false; 402 | if (strcmp(type+2, "RMC")) 403 | return false; 404 | 405 | frame->valid = (validity == 'A'); 406 | frame->latitude.value *= latitude_direction; 407 | frame->longitude.value *= longitude_direction; 408 | frame->variation.value *= variation_direction; 409 | 410 | return true; 411 | } 412 | 413 | bool minmea_parse_gga(struct minmea_sentence_gga *frame, const char *sentence) 414 | { 415 | // $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 416 | char type[6]; 417 | int latitude_direction; 418 | int longitude_direction; 419 | 420 | if (!minmea_scan(sentence, "tTfdfdiiffcfcf_", 421 | type, 422 | &frame->time, 423 | &frame->latitude, &latitude_direction, 424 | &frame->longitude, &longitude_direction, 425 | &frame->fix_quality, 426 | &frame->satellites_tracked, 427 | &frame->hdop, 428 | &frame->altitude, &frame->altitude_units, 429 | &frame->height, &frame->height_units, 430 | &frame->dgps_age)) 431 | return false; 432 | if (strcmp(type+2, "GGA")) 433 | return false; 434 | 435 | frame->latitude.value *= latitude_direction; 436 | frame->longitude.value *= longitude_direction; 437 | 438 | return true; 439 | } 440 | 441 | bool minmea_parse_gsa(struct minmea_sentence_gsa *frame, const char *sentence) 442 | { 443 | // $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39 444 | char type[6]; 445 | 446 | if (!minmea_scan(sentence, "tciiiiiiiiiiiiifff", 447 | type, 448 | &frame->mode, 449 | &frame->fix_type, 450 | &frame->sats[0], 451 | &frame->sats[1], 452 | &frame->sats[2], 453 | &frame->sats[3], 454 | &frame->sats[4], 455 | &frame->sats[5], 456 | &frame->sats[6], 457 | &frame->sats[7], 458 | &frame->sats[8], 459 | &frame->sats[9], 460 | &frame->sats[10], 461 | &frame->sats[11], 462 | &frame->pdop, 463 | &frame->hdop, 464 | &frame->vdop)) 465 | return false; 466 | if (strcmp(type+2, "GSA")) 467 | return false; 468 | 469 | return true; 470 | } 471 | 472 | bool minmea_parse_gll(struct minmea_sentence_gll *frame, const char *sentence) 473 | { 474 | // $GPGLL,3723.2475,N,12158.3416,W,161229.487,A,A*41$; 475 | char type[6]; 476 | int latitude_direction; 477 | int longitude_direction; 478 | 479 | if (!minmea_scan(sentence, "tfdfdTc;c", 480 | type, 481 | &frame->latitude, &latitude_direction, 482 | &frame->longitude, &longitude_direction, 483 | &frame->time, 484 | &frame->status, 485 | &frame->mode)) 486 | return false; 487 | if (strcmp(type+2, "GLL")) 488 | return false; 489 | 490 | frame->latitude.value *= latitude_direction; 491 | frame->longitude.value *= longitude_direction; 492 | 493 | return true; 494 | } 495 | 496 | bool minmea_parse_gst(struct minmea_sentence_gst *frame, const char *sentence) 497 | { 498 | // $GPGST,024603.00,3.2,6.6,4.7,47.3,5.8,5.6,22.0*58 499 | char type[6]; 500 | 501 | if (!minmea_scan(sentence, "tTfffffff", 502 | type, 503 | &frame->time, 504 | &frame->rms_deviation, 505 | &frame->semi_major_deviation, 506 | &frame->semi_minor_deviation, 507 | &frame->semi_major_orientation, 508 | &frame->latitude_error_deviation, 509 | &frame->longitude_error_deviation, 510 | &frame->altitude_error_deviation)) 511 | return false; 512 | if (strcmp(type+2, "GST")) 513 | return false; 514 | 515 | return true; 516 | } 517 | 518 | bool minmea_parse_gsv(struct minmea_sentence_gsv *frame, const char *sentence) 519 | { 520 | // $GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74 521 | // $GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D 522 | // $GPGSV,4,2,11,08,51,203,30,09,45,215,28*75 523 | // $GPGSV,4,4,13,39,31,170,27*40 524 | // $GPGSV,4,4,13*7B 525 | char type[6]; 526 | 527 | if (!minmea_scan(sentence, "tiii;iiiiiiiiiiiiiiii", 528 | type, 529 | &frame->total_msgs, 530 | &frame->msg_nr, 531 | &frame->total_sats, 532 | &frame->sats[0].nr, 533 | &frame->sats[0].elevation, 534 | &frame->sats[0].azimuth, 535 | &frame->sats[0].snr, 536 | &frame->sats[1].nr, 537 | &frame->sats[1].elevation, 538 | &frame->sats[1].azimuth, 539 | &frame->sats[1].snr, 540 | &frame->sats[2].nr, 541 | &frame->sats[2].elevation, 542 | &frame->sats[2].azimuth, 543 | &frame->sats[2].snr, 544 | &frame->sats[3].nr, 545 | &frame->sats[3].elevation, 546 | &frame->sats[3].azimuth, 547 | &frame->sats[3].snr 548 | )) { 549 | return false; 550 | } 551 | if (strcmp(type+2, "GSV")) 552 | return false; 553 | 554 | return true; 555 | } 556 | 557 | bool minmea_parse_vtg(struct minmea_sentence_vtg *frame, const char *sentence) 558 | { 559 | // $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48 560 | // $GPVTG,156.1,T,140.9,M,0.0,N,0.0,K*41 561 | // $GPVTG,096.5,T,083.5,M,0.0,N,0.0,K,D*22 562 | // $GPVTG,188.36,T,,M,0.820,N,1.519,K,A*3F 563 | char type[6]; 564 | char c_true, c_magnetic, c_knots, c_kph, c_faa_mode; 565 | 566 | if (!minmea_scan(sentence, "tfcfcfcfc;c", 567 | type, 568 | &frame->true_track_degrees, 569 | &c_true, 570 | &frame->magnetic_track_degrees, 571 | &c_magnetic, 572 | &frame->speed_knots, 573 | &c_knots, 574 | &frame->speed_kph, 575 | &c_kph, 576 | &c_faa_mode)) 577 | return false; 578 | if (strcmp(type+2, "VTG")) 579 | return false; 580 | // check chars 581 | if (c_true != 'T' || 582 | c_magnetic != 'M' || 583 | c_knots != 'N' || 584 | c_kph != 'K') 585 | return false; 586 | frame->faa_mode = (enum minmea_faa_mode)c_faa_mode; 587 | 588 | return true; 589 | } 590 | 591 | bool minmea_parse_zda(struct minmea_sentence_zda *frame, const char *sentence) 592 | { 593 | // $GPZDA,201530.00,04,07,2002,00,00*60 594 | char type[6]; 595 | 596 | if(!minmea_scan(sentence, "tTiiiii", 597 | type, 598 | &frame->time, 599 | &frame->date.day, 600 | &frame->date.month, 601 | &frame->date.year, 602 | &frame->hour_offset, 603 | &frame->minute_offset)) 604 | return false; 605 | if (strcmp(type+2, "ZDA")) 606 | return false; 607 | 608 | // check offsets 609 | if (abs(frame->hour_offset) > 13 || 610 | frame->minute_offset > 59 || 611 | frame->minute_offset < 0) 612 | return false; 613 | 614 | return true; 615 | } 616 | 617 | int minmea_gettime(struct timespec *ts, const struct minmea_date *date, const struct minmea_time *time_) 618 | { 619 | if (date->year == -1 || time_->hours == -1) 620 | return -1; 621 | 622 | struct tm tm; 623 | memset(&tm, 0, sizeof(tm)); 624 | if (date->year < 80) { 625 | tm.tm_year = 2000 + date->year - 1900; // 2000-2079 626 | } else if (date->year >= 1900) { 627 | tm.tm_year = date->year - 1900; // 4 digit year, use directly 628 | } else { 629 | tm.tm_year = date->year; // 1980-1999 630 | } 631 | tm.tm_mon = date->month - 1; 632 | tm.tm_mday = date->day; 633 | tm.tm_hour = time_->hours; 634 | tm.tm_min = time_->minutes; 635 | tm.tm_sec = time_->seconds; 636 | 637 | time_t timestamp = timegm(&tm); /* See README.md if your system lacks timegm(). */ 638 | if (timestamp != (time_t)-1) { 639 | ts->tv_sec = timestamp; 640 | ts->tv_nsec = time_->microseconds * 1000; 641 | return 0; 642 | } else { 643 | return -1; 644 | } 645 | } 646 | 647 | /* vim: set ts=4 sw=4 et: */ 648 | -------------------------------------------------------------------------------- /minmea.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014 Kosma Moczek 3 | * This program is free software. It comes without any warranty, to the extent 4 | * permitted by applicable law. You can redistribute it and/or modify it under 5 | * the terms of the Do What The Fuck You Want To Public License, Version 2, as 6 | * published by Sam Hocevar. See the COPYING file for more details. 7 | */ 8 | 9 | #ifndef MINMEA_H 10 | #define MINMEA_H 11 | 12 | #ifdef __cplusplus 13 | extern "C" { 14 | #endif 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #ifdef MINMEA_INCLUDE_COMPAT 23 | #include 24 | #endif 25 | 26 | #define MINMEA_MAX_LENGTH 80 27 | 28 | enum minmea_sentence_id { 29 | MINMEA_INVALID = -1, 30 | MINMEA_UNKNOWN = 0, 31 | MINMEA_SENTENCE_RMC, 32 | MINMEA_SENTENCE_GGA, 33 | MINMEA_SENTENCE_GSA, 34 | MINMEA_SENTENCE_GLL, 35 | MINMEA_SENTENCE_GST, 36 | MINMEA_SENTENCE_GSV, 37 | MINMEA_SENTENCE_VTG, 38 | MINMEA_SENTENCE_ZDA, 39 | }; 40 | 41 | struct minmea_float { 42 | int_least32_t value; 43 | int_least32_t scale; 44 | }; 45 | 46 | struct minmea_date { 47 | int day; 48 | int month; 49 | int year; 50 | }; 51 | 52 | struct minmea_time { 53 | int hours; 54 | int minutes; 55 | int seconds; 56 | int microseconds; 57 | }; 58 | 59 | struct minmea_sentence_rmc { 60 | struct minmea_time time; 61 | bool valid; 62 | struct minmea_float latitude; 63 | struct minmea_float longitude; 64 | struct minmea_float speed; 65 | struct minmea_float course; 66 | struct minmea_date date; 67 | struct minmea_float variation; 68 | }; 69 | 70 | struct minmea_sentence_gga { 71 | struct minmea_time time; 72 | struct minmea_float latitude; 73 | struct minmea_float longitude; 74 | int fix_quality; 75 | int satellites_tracked; 76 | struct minmea_float hdop; 77 | struct minmea_float altitude; char altitude_units; 78 | struct minmea_float height; char height_units; 79 | struct minmea_float dgps_age; 80 | }; 81 | 82 | enum minmea_gll_status { 83 | MINMEA_GLL_STATUS_DATA_VALID = 'A', 84 | MINMEA_GLL_STATUS_DATA_NOT_VALID = 'V', 85 | }; 86 | 87 | // FAA mode added to some fields in NMEA 2.3. 88 | enum minmea_faa_mode { 89 | MINMEA_FAA_MODE_AUTONOMOUS = 'A', 90 | MINMEA_FAA_MODE_DIFFERENTIAL = 'D', 91 | MINMEA_FAA_MODE_ESTIMATED = 'E', 92 | MINMEA_FAA_MODE_MANUAL = 'M', 93 | MINMEA_FAA_MODE_SIMULATED = 'S', 94 | MINMEA_FAA_MODE_NOT_VALID = 'N', 95 | MINMEA_FAA_MODE_PRECISE = 'P', 96 | }; 97 | 98 | struct minmea_sentence_gll { 99 | struct minmea_float latitude; 100 | struct minmea_float longitude; 101 | struct minmea_time time; 102 | char status; 103 | char mode; 104 | }; 105 | 106 | struct minmea_sentence_gst { 107 | struct minmea_time time; 108 | struct minmea_float rms_deviation; 109 | struct minmea_float semi_major_deviation; 110 | struct minmea_float semi_minor_deviation; 111 | struct minmea_float semi_major_orientation; 112 | struct minmea_float latitude_error_deviation; 113 | struct minmea_float longitude_error_deviation; 114 | struct minmea_float altitude_error_deviation; 115 | }; 116 | 117 | enum minmea_gsa_mode { 118 | MINMEA_GPGSA_MODE_AUTO = 'A', 119 | MINMEA_GPGSA_MODE_FORCED = 'M', 120 | }; 121 | 122 | enum minmea_gsa_fix_type { 123 | MINMEA_GPGSA_FIX_NONE = 1, 124 | MINMEA_GPGSA_FIX_2D = 2, 125 | MINMEA_GPGSA_FIX_3D = 3, 126 | }; 127 | 128 | struct minmea_sentence_gsa { 129 | char mode; 130 | int fix_type; 131 | int sats[12]; 132 | struct minmea_float pdop; 133 | struct minmea_float hdop; 134 | struct minmea_float vdop; 135 | }; 136 | 137 | struct minmea_sat_info { 138 | int nr; 139 | int elevation; 140 | int azimuth; 141 | int snr; 142 | }; 143 | 144 | struct minmea_sentence_gsv { 145 | int total_msgs; 146 | int msg_nr; 147 | int total_sats; 148 | struct minmea_sat_info sats[4]; 149 | }; 150 | 151 | struct minmea_sentence_vtg { 152 | struct minmea_float true_track_degrees; 153 | struct minmea_float magnetic_track_degrees; 154 | struct minmea_float speed_knots; 155 | struct minmea_float speed_kph; 156 | enum minmea_faa_mode faa_mode; 157 | }; 158 | 159 | struct minmea_sentence_zda { 160 | struct minmea_time time; 161 | struct minmea_date date; 162 | int hour_offset; 163 | int minute_offset; 164 | }; 165 | 166 | /** 167 | * Calculate raw sentence checksum. Does not check sentence integrity. 168 | */ 169 | uint8_t minmea_checksum(const char *sentence); 170 | 171 | /** 172 | * Check sentence validity and checksum. Returns true for valid sentences. 173 | */ 174 | bool minmea_check(const char *sentence, bool strict); 175 | 176 | /** 177 | * Determine talker identifier. 178 | */ 179 | bool minmea_talker_id(char talker[3], const char *sentence); 180 | 181 | /** 182 | * Determine sentence identifier. 183 | */ 184 | enum minmea_sentence_id minmea_sentence_id(const char *sentence, bool strict); 185 | 186 | /** 187 | * Scanf-like processor for NMEA sentences. Supports the following formats: 188 | * c - single character (char *) 189 | * d - direction, returned as 1/-1, default 0 (int *) 190 | * f - fractional, returned as value + scale (int *, int *) 191 | * i - decimal, default zero (int *) 192 | * s - string (char *) 193 | * t - talker identifier and type (char *) 194 | * T - date/time stamp (int *, int *, int *) 195 | * Returns true on success. See library source code for details. 196 | */ 197 | bool minmea_scan(const char *sentence, const char *format, ...); 198 | 199 | /* 200 | * Parse a specific type of sentence. Return true on success. 201 | */ 202 | bool minmea_parse_rmc(struct minmea_sentence_rmc *frame, const char *sentence); 203 | bool minmea_parse_gga(struct minmea_sentence_gga *frame, const char *sentence); 204 | bool minmea_parse_gsa(struct minmea_sentence_gsa *frame, const char *sentence); 205 | bool minmea_parse_gll(struct minmea_sentence_gll *frame, const char *sentence); 206 | bool minmea_parse_gst(struct minmea_sentence_gst *frame, const char *sentence); 207 | bool minmea_parse_gsv(struct minmea_sentence_gsv *frame, const char *sentence); 208 | bool minmea_parse_vtg(struct minmea_sentence_vtg *frame, const char *sentence); 209 | bool minmea_parse_zda(struct minmea_sentence_zda *frame, const char *sentence); 210 | 211 | /** 212 | * Convert GPS UTC date/time representation to a UNIX timestamp. 213 | */ 214 | int minmea_gettime(struct timespec *ts, const struct minmea_date *date, const struct minmea_time *time_); 215 | 216 | /** 217 | * Rescale a fixed-point value to a different scale. Rounds towards zero. 218 | */ 219 | static inline int_least32_t minmea_rescale(struct minmea_float *f, int_least32_t new_scale) 220 | { 221 | if (f->scale == 0) 222 | return 0; 223 | if (f->scale == new_scale) 224 | return f->value; 225 | if (f->scale > new_scale) 226 | return (f->value + ((f->value > 0) - (f->value < 0)) * f->scale/new_scale/2) / (f->scale/new_scale); 227 | else 228 | return f->value * (new_scale/f->scale); 229 | } 230 | 231 | /** 232 | * Convert a fixed-point value to a floating-point value. 233 | * Returns NaN for "unknown" values. 234 | */ 235 | static inline float minmea_tofloat(struct minmea_float *f) 236 | { 237 | if (f->scale == 0) 238 | return NAN; 239 | return (float) f->value / (float) f->scale; 240 | } 241 | 242 | /** 243 | * Convert a raw coordinate to a floating point DD.DDD... value. 244 | * Returns NaN for "unknown" values. 245 | */ 246 | static inline float minmea_tocoord(struct minmea_float *f) 247 | { 248 | if (f->scale == 0) 249 | return NAN; 250 | int_least32_t degrees = f->value / (f->scale * 100); 251 | int_least32_t minutes = f->value % (f->scale * 100); 252 | return (float) degrees + (float) minutes / (60 * f->scale); 253 | } 254 | 255 | #ifdef __cplusplus 256 | } 257 | #endif 258 | 259 | #endif /* MINMEA_H */ 260 | 261 | /* vim: set ts=4 sw=4 et: */ 262 | --------------------------------------------------------------------------------