├── towl-esp8266 ├── config.h ├── README └── towl-esp8266.ino ├── dnslogger-poc ├── README └── dnslogger-poc.py └── README.md /towl-esp8266/config.h: -------------------------------------------------------------------------------- 1 | // Telemetry over Opportunistic WiFi Links (T.O.W.L.) 2 | // http://phreakmonkey.com/projects/towl 3 | 4 | // --- Digistump Oak Board --- 5 | // The following enables Particle.connect() and OTA flashing for Digistump Oak. 6 | // HOMESSID must be set the same as in your Oak firmware for OTA to work. 7 | // Comment these out for most other generic ESP8266 boards. 8 | #define OAK 9 | #define HOMESSID "Linksys" 10 | // --- End Digistump Oak Config --- 11 | 12 | // The queries are sent in the form of S-{BASE32}.DEVICE_ID.SUBDOMAIN 13 | // #define DEVICE_ID and SUBDOMAIN below: 14 | #define DEVICE_ID "a01" 15 | #define SUBDOMAIN "foobar.example.com" 16 | 17 | // LED pin (currently just indicates GPS signal status via PWM) 18 | // Hint: DigiStump Oak = 1, Adafruit Huzzah = 0, others = ?? 19 | #define LED 1 20 | 21 | // NMEA GPS Serial baudrate 22 | // Note this will also be the bitrate of debug output messages on TX pin 23 | #define GPS_BAUD 115200 24 | 25 | // TSTORE_SZ = max number of telemetry entries to backlog (16 bytes RAM each) 26 | #define TSTORE_SZ 200 27 | // MAX_INTERVAL : Highest interval to track in 10 sec increments. (6 = 1 min) 28 | #define MAX_INTERVAL 18 29 | 30 | // --- end CONFIGURATION section --- 31 | -------------------------------------------------------------------------------- /dnslogger-poc/README: -------------------------------------------------------------------------------- 1 | Quick & dirty proof-of-concept code to listen to DNS requests and decode 2 | BASE32-encoded telemetry data. 3 | 4 | Dependencies: python2.7, dnslib 5 | dnslib can be found at https://pypi.python.org/pypi/dnslib or installed via: 6 | $ sudo pip install dnslib 7 | 8 | Important variables to set at the top of the code: 9 | 10 | PORT = port # to listen on. Of course, DNS servers will look for this to be 11 | port 53, so either redirect port 53 traffic to this port, or change this to 12 | 53 and run as root. 13 | 14 | SUBDOMAIN - the server will ignore queries that don't end in this. Be sure 15 | it matches both what you've set in the TOWL device and what you have your 16 | authoritative nameserver sending you NS queries for. 17 | 18 | LOGDIR - the directory where the decoded DNS data will be logged. It will 19 | auto-append to files named xyy.log where xyy = the device_id. 20 | 21 | ** SECURITY WARNING ** 22 | 23 | This is proof-of-concept code. While I believe it is reasonably safe, I make 24 | no warrantees that it doesn't contain RCE or other types of security 25 | vulnerabilities. If you are handling untrusted data directly from the Internet 26 | please audit the code yourself or take whatever precautions are necessary before 27 | running this. I would not recommend running this code as-is in a privleged 28 | environment, especially as root. 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # T.O.W.L. 2 | ## Telemetry over Opportunistic WiFi Links 3 | 4 | *Note: This is an experimental project. No warranties, functionality, or 5 | suitability for any application are implied.* 6 | 7 | 8 | - Near-real time GPS data via random open / captive portal hotspots 9 | - Uses DNS recursion to send telemetry data 10 | - Stores waypoints in RAM between successful transmissions 11 | - Designed to be subscription-free, super-low cost telemetry device 12 | 13 | ### Hardware: 14 | - Digistump Oak ESP8266 development board (http://digistump.com/products/145) 15 | - 3.3V Serial NMEA GPS module 16 | 17 | #### Example GPS modules. 18 | *YMMV. Also consider eBay.* 19 | 20 | Generic: http://www.banggood.com/1-5Hz-VK2828U7G5LF-TTL-Ublox-GPS-Module-With-Antenna-p-965540.html 21 | 22 | uBlox: http://amzn.to/2avXoXr 23 | 24 | #### Connection: 25 | 26 | Connect GPS power & ground as appropriate and wire the GPS TX line to Pin 3 27 | (RX) on the digistump oak. (I recommend testing the GPS module with an FTDI 28 | serial adapter first to ensure you're receiving NMEA data at the expected baud 29 | rate.) 30 | 31 | Be sure to configure the GPS baud rate at the top of the towl .ino file. 32 | (See README) 33 | 34 | 35 | ### Server 36 | 37 | You'll need to add an NS record to the DNS table of a domain you control, 38 | designating a subdomain namesrever for the TOWL telemetry query catcher. 39 | 40 | E.g. if you own the domain "MyDomain.com", you could designate a server to 41 | receive the TOWL queries by creating a NS record for "TOWL.MyDomain.com", 42 | pointing at the server you intend to run the catcher on. If said server is 43 | at IP address 1.2.3.4, then that record looks something akin to: 44 | 45 | TOWL IN NS 1.2.3.4 46 | 47 | Run the PoC code on the designated server. Be sure to configure both the TOWL 48 | devices and the server code for the "TOWL.MyDomain.com" domain name. (See 49 | README under each directory for instructions.) 50 | 51 | 52 | Have fun experimenting! 53 | K.C. -/- phreakmonkey@gmail.com 54 | 55 | -------------------------------------------------------------------------------- /towl-esp8266/README: -------------------------------------------------------------------------------- 1 | The .ino file in this directory was developed for the Digistump Oak ESP8266 2 | IoT chip. May work on Adafruit Huzzah or other generic ESP8266 boards. 3 | 4 | Implementation notes: 5 | 6 | Dependencies: 7 | - Digistump Oak or generic ESP8266 Arduino libraries 8 | - TinyGPS library (https://github.com/mikalhart/TinyGPS) 9 | - Arduino Time library (https://github.com/PaulStoffregen/Time) 10 | - NetRat's Base32 Arduino library (https://github.com/NetRat/Base32) 11 | 12 | Important variables to set in config.h file: 13 | 14 | *** DIGISTUMP OAK (http://digistump.com/products/145) 15 | Uncomment the "#define OAK" and "#define HOMESSID" lines for the Digistump 16 | Oak board & libraries. Comment them out (e.g. //#define OAK) for generic 17 | ESP8266 boards & libraries. You will get compile errors if you try to 18 | compile the OAK code with the generic library or vice-versa. 19 | 20 | HOMESSID - when the device is powered up, it attempts find this SSID first. 21 | If it is present, it calls Particle.connect() and waits for seven seconds to 22 | allow an OTA firmware update. Note - this must match whatever SSID you have 23 | configured in the Digistump OAK firmware in order to work. If you change one 24 | without the other, you'll have to either change the OAK config to match this 25 | or reflash the firmware from oak safe mode. 26 | 27 | Oak Flashing: THIS DEVICE REQUIRES YOU SELECT "BOARD": 28 | "Digistump Oak (PIN 1 Safe Mode, Manual Config Only)" 29 | 30 | If you flash it with "PIN 1 Safe Mode, Default" then it will reboot into config 31 | mode almost immediately after it begins scanning for open APs. 32 | 33 | With this mode you can always revert it to config/OTA mode by tying pin-1 to 34 | ground during power-up, but it won't fall back to that mode automatically if 35 | it fails to connect to your AP once it's running. 36 | *** END DIGISTUMP SPECIFIC NOTES. 37 | 38 | 39 | SUBDOMAIN - must be set to the subdomain/domain name that DNS queries will be 40 | sent to. This will be appended to each query 41 | 42 | DEVICE_ID - this is added as an additional subdomain beneath SUBDOMAIN. The 43 | PoC server expects this to be in the format "x##" where x = a letter 44 | and ## = a number between 00 and 99. E.g. "a01" or "c55". This identifier 45 | is useful for telling queries from different devices apart. 46 | 47 | GPS_BAUD - set to match the baud rate of your serial GPS. 48 | (Tested with MTK-3329 GPS and a u-blox 6 knockoff in NMEA mode. Any basic NMEA 49 | GPS module should work) 50 | 51 | 52 | Have fun! 53 | K.C. phreakmonkey@gmail.com 54 | 55 | -------------------------------------------------------------------------------- /dnslogger-poc/dnslogger-poc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import datetime 4 | import sys 5 | import time 6 | import threading 7 | import traceback 8 | import SocketServer 9 | from dnslib import * 10 | 11 | import base64 12 | import struct 13 | 14 | # Port to listen on. I use a high port to run as a non-root user and then 15 | # map to it in iptables. Alternatively change to port 53 and run as root. 16 | PORT = 5300 17 | SUBDOMAIN = '.foobar.example.com.' 18 | LOGDIR = '/var/tmp' 19 | 20 | def parseCovert(devid, data): 21 | dstr = data.upper().split('.')[0] 22 | print "dstr: %s" % dstr 23 | if dstr.startswith('L-'): 24 | dstr = dstr[2:] 25 | t = 'long' 26 | elif dstr.startswith('S-'): 27 | dstr = dstr[2:] 28 | t = 'telem' 29 | else: 30 | t = 'str' 31 | 32 | plen = len(dstr) % 8 33 | if plen: 34 | dstr = dstr + '=' * (8 - plen) 35 | res = base64.b32decode(dstr) 36 | 37 | with open(LOGDIR + '/%s.log' % devid, 'a') as lf: 38 | if t == 'long': 39 | res = struct.unpack('>i', res)[0] 40 | print 'Decoded long: %d' % res 41 | lf.write('%s : %d\n' % (datetime.datetime.now(), res)) 42 | return 99 43 | 44 | if t == 'telem': 45 | print 'len: %d' % len(res) 46 | tm,lat,lon,spd,sats,id,mode = struct.unpack('IiiBBBB', res) 47 | print 'Decoded telem: %d %d %d %d' % (tm, lat, lon, spd) 48 | lf.write('%s,%d,%f,%f,%d,%d,%d\n' % (datetime.datetime.now(), tm, 49 | lat/1000000.0, lon/1000000.0, 50 | spd, sats, mode)) 51 | return id 52 | 53 | print 'Decoded: %s' % res 54 | lf.write('%s : %s\n' % (datetime.datetime.now(), res)) 55 | return len(res) 56 | 57 | 58 | def dns_response(data): 59 | request = DNSRecord.parse(data) 60 | 61 | print request 62 | 63 | reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q) 64 | 65 | qname = request.q.qname 66 | qn = str(qname) 67 | qtype = request.q.qtype 68 | qt = QTYPE[qtype] 69 | print "qt is %s, qn is %s" % (qt, qn) 70 | 71 | if qt == 'A' and qn.lower().endswith(SUBDOMAIN): 72 | devid = qn.lower().split('.')[1] 73 | # Sanity check: devid must be xnn where x = letter and n = number 74 | if len(devid) != 3: 75 | return 76 | if (devid[0] not in string.letters or 77 | devid[1] not in string.digits or 78 | devid[2] not in string.digits): 79 | return 80 | rIP = '10.0.11.%d' % parseCovert(devid, qn) 81 | reply.add_answer( 82 | RR(rname=qname, rtype=QTYPE.A, rclass=1, ttl=300, rdata=A(rIP))) 83 | return reply.pack() 84 | 85 | 86 | class BaseRequestHandler(SocketServer.BaseRequestHandler): 87 | 88 | def get_data(self): 89 | raise NotImplementedError 90 | 91 | def send_data(self, data): 92 | raise NotImplementedError 93 | 94 | def handle(self): 95 | now = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f') 96 | print "\n\n%s request %s (%s %s):" % (self.__class__.__name__[:3], now, self.client_address[0], 97 | self.client_address[1]) 98 | try: 99 | data = self.get_data() 100 | #print len(data), data.encode('hex') # repr(data).replace('\\x', '')[1:-1] 101 | self.send_data(dns_response(data)) 102 | except Exception: 103 | traceback.print_exc(file=sys.stderr) 104 | 105 | 106 | class TCPRequestHandler(BaseRequestHandler): 107 | 108 | def get_data(self): 109 | data = self.request.recv(8192).strip() 110 | sz = int(data[:2].encode('hex'), 16) 111 | if sz < len(data) - 2: 112 | raise Exception("Wrong size of TCP packet") 113 | elif sz > len(data) - 2: 114 | raise Exception("Too big TCP packet") 115 | return data[2:] 116 | 117 | def send_data(self, data): 118 | sz = hex(len(data))[2:].zfill(4).decode('hex') 119 | return self.request.sendall(sz + data) 120 | 121 | 122 | class UDPRequestHandler(BaseRequestHandler): 123 | 124 | def get_data(self): 125 | return self.request[0].strip() 126 | 127 | def send_data(self, data): 128 | return self.request[1].sendto(data, self.client_address) 129 | 130 | 131 | if __name__ == '__main__': 132 | print "Starting nameserver..." 133 | 134 | servers = [ 135 | SocketServer.ThreadingUDPServer(('', PORT), UDPRequestHandler), 136 | SocketServer.ThreadingTCPServer(('', PORT), TCPRequestHandler), 137 | ] 138 | for s in servers: 139 | thread = threading.Thread(target=s.serve_forever) # that thread will start one more thread for each request 140 | thread.daemon = True # exit the server thread when the main thread terminates 141 | thread.start() 142 | print "%s server loop running in thread: %s" % (s.RequestHandlerClass.__name__[:3], thread.name) 143 | 144 | try: 145 | while 1: 146 | time.sleep(1) 147 | sys.stderr.flush() 148 | sys.stdout.flush() 149 | 150 | except KeyboardInterrupt: 151 | pass 152 | finally: 153 | for s in servers: 154 | s.shutdown() 155 | -------------------------------------------------------------------------------- /towl-esp8266/towl-esp8266.ino: -------------------------------------------------------------------------------- 1 | // Telemetry over Opportunistic WiFi Links (T.O.W.L.) 2 | // http://phreakmonkey.com/projects/towl 3 | 4 | // !! Be sure to check configuration settings in config.h before compiling !! 5 | 6 | #include "config.h" 7 | 8 | #ifdef OAK 9 | SYSTEM_MODE(SEMI_AUTOMATIC); 10 | #endif 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | // Globals: 25 | Base32 base32; 26 | TinyGPS gps; 27 | 28 | struct telem { 29 | uint32_t tstamp; 30 | int32_t lat; 31 | int32_t lon; 32 | uint8_t spd; 33 | uint8_t sats; 34 | uint8_t id; 35 | uint8_t mode; 36 | }; // 16 bytes total 37 | 38 | struct telem tstore[TSTORE_SZ]; 39 | uint8_t query_id = 0; 40 | uint32_t last_rec = 0; // Last record time 41 | uint32_t last_rep = 0; // Last report time 42 | uint8_t startup = 3; 43 | 44 | // Function prototypes: 45 | void parseGPS(void); 46 | void setGPSTime(void); 47 | struct telem * getTelem(void); 48 | uint16_t findSlot(uint8_t); 49 | void storeTelem(struct telem *); 50 | uint8_t sendStoredTelem(void); 51 | int connectAP(void); 52 | uint8_t sendDNSTelem(struct telem *); 53 | void serDelay(unsigned long); 54 | void setup(void); 55 | void loop(void); 56 | #ifdef OAK 57 | void homeConnect(); 58 | #endif 59 | 60 | 61 | void setup() { 62 | Serial.begin(GPS_BAUD); 63 | delay(100); 64 | Serial.println("Startup sequence."); 65 | pinMode(LED, OUTPUT); 66 | analogWrite(LED, 0); 67 | randomSeed(analogRead(0)); 68 | memset(tstore, 0, sizeof(tstore)); 69 | #ifdef OAK 70 | homeConnect(); 71 | #else 72 | WiFi.mode(WIFI_STA); 73 | #endif 74 | } 75 | 76 | void loop() { 77 | telem *currentpos; 78 | uint8_t res; 79 | parseGPS(); 80 | 81 | if (gps.satellites() != 255) { 82 | res = 0; 83 | analogWrite(LED, gps.satellites() * (1024 / 11)); 84 | if (timeStatus() == timeNotSet) setGPSTime(); 85 | else { 86 | currentpos = getTelem(); 87 | if (connectAP() == 1) res = sendDNSTelem(currentpos); 88 | if (res == 1) { 89 | last_rep = millis(); 90 | last_rec = millis(); 91 | } 92 | else if (startup > 0 || millis() - last_rec > 10000) storeTelem(currentpos); 93 | delete currentpos; 94 | while (res == 1) { 95 | res = sendStoredTelem(); 96 | parseGPS(); 97 | } 98 | } 99 | } 100 | } 101 | 102 | #ifdef OAK 103 | void homeConnect() { 104 | int numNets = WiFi.scanNetworks(); 105 | uint16_t thisNet; 106 | for (thisNet = 0; thisNet < numNets; thisNet++) 107 | if (strcmp(WiFi.SSID(thisNet).c_str(), HOMESSID) == 0) break; 108 | if (thisNet < numNets) { 109 | Particle.connect(); 110 | for (uint8_t i=0; i<70; i++) { 111 | analogWrite(LED, 1023); 112 | delay(100); 113 | analogWrite(LED, 0); 114 | delay(100); 115 | if (Particle.connected() == false) Particle.connect(); 116 | } 117 | Particle.disconnect(); 118 | WiFi.disconnect(); 119 | } 120 | return; 121 | } 122 | #endif 123 | 124 | uint16_t findSlot(uint8_t pmode) { 125 | // Find a memory slot that is empty or at a higher time 126 | // resolution than the current object. 127 | uint16_t i; 128 | for(i=0; i < TSTORE_SZ; i++) { 129 | if (tstore[i].tstamp == 0) return i; // Empty slot 130 | } 131 | if (pmode == 0) return i; 132 | 133 | // Buffer full, is there a higher res one we can overwrite? 134 | for(uint8_t j=0; j < pmode; j++) 135 | for(i=0; i < TSTORE_SZ; i++) { 136 | if (tstore[i].mode == j) return i; 137 | } 138 | return i; 139 | } 140 | 141 | void storeTelem(struct telem *tdata) { 142 | uint16_t slot; 143 | uint8_t thisMode = ((millis() - last_rep) / 10000) - 1; 144 | if (thisMode >= MAX_INTERVAL) { 145 | last_rep = millis(); // Reset counter at MAX_INTERVAL 146 | thisMode = 0; 147 | } 148 | 149 | slot = findSlot(thisMode); 150 | if (slot == TSTORE_SZ) return; // Buffer full at current res. 151 | 152 | tstore[slot].tstamp = tdata->tstamp; 153 | tstore[slot].lat = tdata->lat; 154 | tstore[slot].lon = tdata->lon; 155 | tstore[slot].spd = tdata->spd; 156 | tstore[slot].sats = tdata->sats; 157 | tstore[slot].id = tdata->id; 158 | if (tdata->mode == 255) tstore[slot].mode = 255; // Preserve startup marker 159 | else tstore[slot].mode = thisMode; 160 | last_rec = millis(); 161 | 162 | Serial.print("Telem stored in slot "); 163 | Serial.println(slot); 164 | return; 165 | } 166 | 167 | uint8_t sendStoredTelem() { 168 | uint8_t i, res = 0; 169 | for(i=0; i < TSTORE_SZ; i++) { 170 | if (tstore[i].tstamp != 0) { 171 | res = sendDNSTelem(&tstore[i]); 172 | if (res == 1) tstore[i].tstamp = 0; 173 | return res; 174 | } 175 | } 176 | return 0; 177 | } 178 | 179 | int connectAP() { 180 | uint16_t numNets; 181 | uint16_t numOpen = 0; 182 | int16_t bestcandidate[2] = {-1, -1}; 183 | int16_t bestsignal[2] = {-255, -255}; 184 | int wstatus = WL_IDLE_STATUS; 185 | char wSSID[64]; 186 | wSSID[63] = 0; 187 | 188 | Serial.println("Scanning WiFi"); 189 | numNets = WiFi.scanNetworks(); 190 | for (uint16_t thisNet = 0; thisNet < numNets; thisNet++) { 191 | if (WiFi.encryptionType(thisNet) == ENC_TYPE_NONE) { 192 | numOpen++; 193 | Serial.print("OPEN SSID: "); 194 | Serial.print(WiFi.SSID(thisNet)); 195 | Serial.print(" RSSI: "); 196 | Serial.println(WiFi.RSSI(thisNet)); 197 | for (uint8 i=0; i < 2; i++) { 198 | if (WiFi.RSSI(thisNet) > bestsignal[i]) { 199 | bestcandidate[i] = thisNet; 200 | bestsignal[i] = WiFi.RSSI(thisNet); 201 | break; 202 | } 203 | } 204 | } 205 | } 206 | 207 | if (bestcandidate[0] > -1) { 208 | uint8_t choice = 0; 209 | if (bestcandidate[1] > -1) choice = random(2); 210 | strncpy(wSSID, WiFi.SSID(bestcandidate[choice]).c_str(), sizeof(wSSID)-1); 211 | Serial.print("Attempting connect via "); 212 | Serial.print(wSSID); 213 | WiFi.disconnect(); 214 | WiFi.persistent(false); 215 | #ifdef OAK 216 | wstatus = WiFi.begin_internal(wSSID, NULL, 0, NULL); 217 | #else 218 | wstatus = WiFi.begin(wSSID, NULL, 0, NULL, true); 219 | #endif 220 | for (uint8_t i=0; i < 65; i++) { 221 | if (wstatus == WL_CONNECTED) { 222 | Serial.print(". connected. "); 223 | Serial.println(i * 100); 224 | return 1; 225 | } 226 | if (wstatus == WL_CONNECT_FAILED) { 227 | Serial.println(". connect failed."); 228 | return 0; 229 | } 230 | serDelay(100); 231 | wstatus = WiFi.status(); 232 | } 233 | Serial.println(". timeout."); 234 | } 235 | return 0; 236 | } 237 | 238 | struct telem *getTelem() { 239 | struct telem *tdata = new struct telem; 240 | unsigned long age; 241 | float flat, flon; 242 | tdata->sats = gps.satellites(); 243 | if (tdata->sats < 3 || tdata->sats > 20) { 244 | tdata->sats = 255; 245 | return tdata; // Invalid GPS Signal 246 | } 247 | tdata->tstamp = now(); 248 | gps.f_get_position(&flat, &flon, &age); 249 | tdata->lat = flat * 1000000; 250 | tdata->lon = flon * 1000000; 251 | tdata->spd = (uint16_t)gps.f_speed_mph(); 252 | tdata->id = query_id; 253 | if (startup > 0) { 254 | tdata->mode = 255; // Startup marker 255 | startup--; 256 | } else tdata->mode = 254; // Live telem marker 257 | if (query_id < 255) query_id++; 258 | else query_id = 0; 259 | return tdata; 260 | } 261 | 262 | uint8_t sendDNSTelem(struct telem *tdata) { 263 | IPAddress qresponse = {0,0,0,0}; 264 | char query[127]; 265 | unsigned int outlen; 266 | byte *b32string; 267 | 268 | memset(query, '\0', sizeof(query)); 269 | outlen = base32.toBase32((byte*)tdata, sizeof(struct telem), b32string, false); 270 | strcat(query, "S-"); 271 | strncpy(query+2, (char*)b32string, outlen); 272 | strcat(query, "."); 273 | strcat(query, DEVICE_ID); 274 | strcat(query, "."); 275 | strcat(query, SUBDOMAIN); 276 | free(b32string); // Got malloc()'d inside base32.toBase32() 277 | 278 | WiFi.hostByName(query, qresponse); 279 | if (qresponse[0] == 10 && qresponse[3] == tdata->id) { 280 | Serial.println("Telemetry ACK"); 281 | return 1; 282 | } 283 | Serial.println("No confirm."); 284 | return 0; 285 | } 286 | 287 | void parseGPS() { 288 | while(Serial.available()) 289 | gps.encode(Serial.read()); 290 | } 291 | 292 | void serDelay(unsigned long ms) { 293 | unsigned long start = millis(); 294 | do { 295 | parseGPS(); 296 | } while (millis() - start < ms); 297 | } 298 | 299 | void setGPSTime() { 300 | uint8_t sats = gps.satellites(); 301 | if (sats < 3 || sats > 20) return; 302 | 303 | unsigned long age; 304 | int Year; 305 | byte Month, Day, Hour, Minute, Second; 306 | 307 | gps.crack_datetime(&Year, &Month, &Day, &Hour, &Minute, &Second, NULL, &age); 308 | if (age > 500) return; 309 | if (Year < 2016) return; // Weirdness with MTK-3329 during initialization 310 | setTime(Hour, Minute, Second, Day, Month, Year); 311 | char timestr[64]; 312 | sprintf(timestr, "GPS Time: %d-%02d-%02d %02d:%02d:%02d", Year, Month, Day, Hour, Minute, Second); 313 | Serial.println(timestr); 314 | return; 315 | } 316 | --------------------------------------------------------------------------------