// gps.hpp — u-blox NMEA GPS reader over UART // No external libs; uses POSIX termios + read(). // // Hardware (THE TRUTH): // GPIO14/TXD0 → UBLOX_TX (/dev/serial0) // GPIO15/RXD0 → UBLOX_RX // Default baud: 38400 (factory default on this module) // // Enable UART: sudo raspi-config → Interfaces → Serial Port // "login shell over serial" = No, "serial port hardware" = Yes // Then reboot. Check: ls -la /dev/serial0 #pragma once #include #include #include #include #include #include #include #include namespace gps { struct Fix { bool valid = false; double lat = 0.0; // degrees, positive=N double lon = 0.0; // degrees, positive=E float speed_knots = 0.0f; char time[12]{}; // HHMMSS.ss (raw NMEA) char date[7]{}; // DDMMYY }; struct Config { const char *dev = "/dev/serial0"; int baud = 38400; // confirmed working baud for this module bool verbose = false; }; class Reader { public: bool begin(const Config &cfg); bool begin(const char *dev = "/dev/serial0", int baud = 38400); void end(); // Read one NMEA sentence (null-terminated, includes '\n'). // timeout_ms: 0 = block forever, >0 = return false after timeout. bool readLine(char *buf, size_t cap, uint32_t timeout_ms = 0); // Parse $GPRMC or $GNRMC. Returns false if wrong sentence type or fix void. static bool parseRmc(const char *sentence, Fix &fix); // Verify NMEA checksum: XOR of bytes between '$' and '*'. static bool verifyChecksum(const char *sentence); private: int fd_ = -1; bool verbose_ = false; }; // ---- Implementation --------------------------------------------------------- inline bool Reader::begin(const Config &c) { verbose_ = c.verbose; return begin(c.dev, c.baud); } inline bool Reader::begin(const char *dev, int baud) { fd_ = ::open(dev, O_RDWR | O_NOCTTY | O_NDELAY); if (fd_ < 0) { if (verbose_) std::fprintf(stderr, "[gps] cannot open %s: %m\n", dev); return false; } ::fcntl(fd_, F_SETFL, 0); // blocking reads termios t{}; if (tcgetattr(fd_, &t) < 0) { if (verbose_) std::fprintf(stderr, "[gps] tcgetattr failed: %m\n"); ::close(fd_); fd_ = -1; return false; } speed_t spd; switch (baud) { case 4800: spd = B4800; break; case 9600: spd = B9600; break; case 19200: spd = B19200; break; case 38400: spd = B38400; break; case 57600: spd = B57600; break; case 115200: spd = B115200; break; default: if (verbose_) std::fprintf(stderr, "[gps] unknown baud %d, defaulting to 38400\n", baud); spd = B38400; } cfsetispeed(&t, spd); cfsetospeed(&t, spd); cfmakeraw(&t); t.c_cc[VMIN] = 1; t.c_cc[VTIME] = 0; if (tcsetattr(fd_, TCSANOW, &t) < 0) { if (verbose_) std::fprintf(stderr, "[gps] tcsetattr failed: %m\n"); ::close(fd_); fd_ = -1; return false; } tcflush(fd_, TCIOFLUSH); if (verbose_) std::fprintf(stderr, "[gps] opened %s at %d baud\n", dev, baud); return true; } inline void Reader::end() { if (fd_ >= 0) { ::close(fd_); fd_ = -1; } } inline bool Reader::readLine(char *buf, size_t cap, uint32_t timeout_ms) { using clock = std::chrono::steady_clock; auto deadline = clock::now() + std::chrono::milliseconds(timeout_ms); auto timed_read = [&](char &c) -> bool { if (timeout_ms == 0) { return ::read(fd_, &c, 1) == 1; } // Non-blocking check with timeout. while (true) { ssize_t r = ::read(fd_, &c, 1); if (r == 1) return true; if (clock::now() >= deadline) return false; // Brief yield to avoid spinning. struct timespec ts = { 0, 1'000'000 }; // 1 ms nanosleep(&ts, nullptr); } }; size_t n = 0; char c; // Wait for '$' (start of sentence). do { if (!timed_read(c)) return false; } while (c != '$'); buf[n++] = '$'; while (n < cap - 1) { if (!timed_read(c)) return false; buf[n++] = c; if (c == '\n') break; } buf[n] = '\0'; return true; } inline bool Reader::verifyChecksum(const char *sentence) { // Checksum is XOR of all bytes between '$' (exclusive) and '*' (exclusive). const char *p = sentence; if (*p == '$') ++p; uint8_t calc = 0; while (*p && *p != '*') calc ^= (uint8_t)*p++; if (*p != '*') return false; // no checksum present unsigned int given = 0; if (std::sscanf(p + 1, "%02X", &given) != 1) return false; return (uint8_t)given == calc; } // Parse $GPRMC or $GNRMC. // Format: $GNRMC,HHMMSS.ss,A,DDMM.MMMM,N,DDDMM.MMMM,E,spd,crs,DDMMYY,...*CS inline bool Reader::parseRmc(const char *sentence, Fix &fix) { if (!sentence) return false; if (std::strncmp(sentence, "$GPRMC", 6) != 0 && std::strncmp(sentence, "$GNRMC", 6) != 0) return false; // Make a mutable copy for tokenization (strip the *CS checksum tail). char buf[128]; std::strncpy(buf, sentence, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; // Terminate at '*' so strtok doesn't swallow the checksum as a field. char *star = std::strchr(buf, '*'); if (star) *star = '\0'; const char *fields[13]{}; int nf = 0; char *p = buf; while (*p && nf < 13) { fields[nf++] = p; char *next = std::strchr(p, ','); if (!next) break; *next = '\0'; p = next + 1; } if (nf < 10) return false; if (fields[2][0] != 'A') { fix.valid = false; return false; } std::strncpy(fix.time, fields[1], sizeof(fix.time) - 1); double raw_lat = std::atof(fields[3]); int lat_deg = (int)(raw_lat / 100.0); fix.lat = lat_deg + (raw_lat - lat_deg * 100.0) / 60.0; if (fields[4][0] == 'S') fix.lat = -fix.lat; double raw_lon = std::atof(fields[5]); int lon_deg = (int)(raw_lon / 100.0); fix.lon = lon_deg + (raw_lon - lon_deg * 100.0) / 60.0; if (fields[6][0] == 'W') fix.lon = -fix.lon; fix.speed_knots = (float)std::atof(fields[7]); std::strncpy(fix.date, fields[9], sizeof(fix.date) - 1); fix.valid = true; return true; } } // namespace gps