From 2fdb722e00eed7cda43f46b31e314cb74f9e7b88 Mon Sep 17 00:00:00 2001 From: jessm33 <112707725+jessm33@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:44:36 -0400 Subject: [PATCH] Add gpsd support to portduino/native (#10781) * Add gpsd support to portduino * copilot fixes * emit gps config in emit_yaml * Fix formating to standard * Address coderabitai issues --------- Co-authored-by: Ben Meadors Co-authored-by: Jonathan Bennett --- bin/config-dist.yaml | 4 +- src/gps/GPS.cpp | 15 ++- src/gps/GPS.h | 3 +- src/platform/portduino/GpsdSerial.cpp | 149 +++++++++++++++++++++++ src/platform/portduino/GpsdSerial.h | 49 ++++++++ src/platform/portduino/PortduinoGlue.cpp | 15 +++ src/platform/portduino/PortduinoGlue.h | 15 +++ 7 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 src/platform/portduino/GpsdSerial.cpp create mode 100644 src/platform/portduino/GpsdSerial.h diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index 38fc057a0..dcea78ee5 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -104,7 +104,9 @@ Lora: ### Define GPS GPS: -# SerialPath: /dev/ttyS0 +# SerialPath: /dev/ttyS0 # Use a physical serial GPS device (mutually exclusive with GpsdHost) +# GpsdHost: localhost # Use gpsd daemon as GPS source instead of a serial device +# GpsdPort: 2947 # gpsd TCP port (default: 2947) # ExtraPins: # - 22 diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 0b9d4b214..dadfda93a 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -25,6 +25,7 @@ #include "ubx.h" #ifdef ARCH_PORTDUINO +#include "GpsdSerial.h" #include "PortduinoGlue.h" #include "meshUtils.h" #include @@ -97,8 +98,9 @@ struct GPSProbeCacheRecord { bool isValidGnssModel(uint8_t model) { - // Keep persisted values bounded to known enum range. - return model <= static_cast(GNSS_MODEL_CM121); + // Only real chip identifiers belong in the probe cache. + // GNSS_MODEL_UNKNOWN and GNSS_MODEL_GENERIC_NMEA are runtime-only values. + return model != static_cast(GNSS_MODEL_UNKNOWN) && model < static_cast(GNSS_MODEL_GENERIC_NMEA); } bool isValidProbeBaud(uint32_t baud) @@ -1901,6 +1903,10 @@ std::unique_ptr GPS::createGps() // They are not used for any hardware access. _rx_gpio = 1; _tx_gpio = 1; + if (!portduino_config.gpsd_host.empty()) { + gpsdSerial.setAddress(portduino_config.gpsd_host, portduino_config.gpsd_port); + _serial_gps = &gpsdSerial; + } } else return nullptr; #endif @@ -1910,6 +1916,11 @@ std::unique_ptr GPS::createGps() auto new_gps = std::unique_ptr(new GPS()); new_gps->rx_gpio = _rx_gpio; new_gps->tx_gpio = _tx_gpio; +#ifdef ARCH_PORTDUINO + // Skip chip-specific probing for gpsd — it's a generic NMEA stream. + if (!portduino_config.gpsd_host.empty()) + new_gps->gnssModel = GNSS_MODEL_GENERIC_NMEA; +#endif GpioVirtPin *virtPin = new GpioVirtPin(); new_gps->enablePin = virtPin; // Always at least populate a virtual pin diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 8709cca90..678cc2845 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -42,7 +42,8 @@ typedef enum { GNSS_MODEL_AG3335, GNSS_MODEL_AG3352, GNSS_MODEL_LS20031, - GNSS_MODEL_CM121 + GNSS_MODEL_CM121, + GNSS_MODEL_GENERIC_NMEA // generic NMEA source (e.g. gpsd); skips chip-specific probe and init } GnssModel_t; typedef enum { diff --git a/src/platform/portduino/GpsdSerial.cpp b/src/platform/portduino/GpsdSerial.cpp new file mode 100644 index 000000000..038ba21f6 --- /dev/null +++ b/src/platform/portduino/GpsdSerial.cpp @@ -0,0 +1,149 @@ +#ifdef ARCH_PORTDUINO + +#include "GpsdSerial.h" +#include "configuration.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace arduino +{ + +// The single global instance used by PortduinoGlue and GPS::createGps(). +GpsdSerial gpsdSerial; + +void GpsdSerial::setAddress(const std::string &host, int port) +{ + _host = host; + _port = port; +} + +bool GpsdSerial::connectToGpsd() +{ + if (_host.empty()) + return false; + + struct addrinfo hints = {}, *res = nullptr; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + std::string portStr = std::to_string(_port); + + if (getaddrinfo(_host.c_str(), portStr.c_str(), &hints, &res) != 0 || !res) { + LOG_WARN("gpsdSerial: could not resolve %s", _host.c_str()); + return false; + } + + // Try every address returned by getaddrinfo (e.g. ::1 before 127.0.0.1). + int fd = -1; + for (struct addrinfo *rp = res; rp != nullptr; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd < 0) + continue; + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) + break; // connected + ::close(fd); + fd = -1; + } + freeaddrinfo(res); + + if (fd < 0) { + LOG_WARN("gpsdSerial: connect to %s:%d failed", _host.c_str(), _port); + return false; + } + + // Switch to non-blocking so available()/read() never stall the GPS thread. + fcntl(fd, F_SETFL, O_NONBLOCK); + + // Ask gpsd to stream raw NMEA sentences. + const char watchCmd[] = "?WATCH={\"enable\":true,\"nmea\":true}\n"; + ::write(fd, watchCmd, sizeof(watchCmd) - 1); + + _sockfd = fd; + _rxBuf.clear(); + LOG_INFO("gpsdSerial: connected to %s:%d", _host.c_str(), _port); + return true; +} + +void GpsdSerial::begin(unsigned long /*baud*/, uint16_t /*config*/) +{ + if (_sockfd >= 0) + end(); + _lastConnectAttemptMs = 0; // force immediate connect on begin() + connectToGpsd(); +} + +void GpsdSerial::end() +{ + if (_sockfd >= 0) { + ::close(_sockfd); + _sockfd = -1; + } + _rxBuf.clear(); +} + +void GpsdSerial::fillBuffer() +{ + if (_sockfd < 0) + return; + // Guard: if the buffer is already full, skip recv entirely so n is always + // assigned before the post-loop disconnect check below. + if (_rxBuf.size() >= RX_BUF_MAX) + return; + + uint8_t tmp[256]; + ssize_t n; + while (_rxBuf.size() < RX_BUF_MAX && (n = recv(_sockfd, tmp, sizeof(tmp), MSG_DONTWAIT)) > 0) { + size_t space = RX_BUF_MAX - _rxBuf.size(); + size_t toCopy = (static_cast(n) < space) ? static_cast(n) : space; + for (size_t i = 0; i < toCopy; i++) + _rxBuf.push_back(tmp[i]); + } + + if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) { + // gpsd closed the connection or a real error occurred. + LOG_WARN("gpsdSerial: disconnected, will retry"); + ::close(_sockfd); + _sockfd = -1; + _rxBuf.clear(); + } +} + +int GpsdSerial::available() +{ + if (_sockfd < 0) { + // Throttle reconnect attempts to avoid log spam and repeated DNS lookups + // when gpsd is unreachable. + uint32_t now = millis(); + if (now - _lastConnectAttemptMs >= RECONNECT_INTERVAL_MS) { + _lastConnectAttemptMs = now; + connectToGpsd(); + } + } + fillBuffer(); + return static_cast(_rxBuf.size()); +} + +int GpsdSerial::peek() +{ + if (_rxBuf.empty()) + return -1; + return static_cast(_rxBuf.front()); +} + +int GpsdSerial::read() +{ + if (_rxBuf.empty()) + return -1; + uint8_t c = _rxBuf.front(); + _rxBuf.pop_front(); + return static_cast(c); +} + +} // namespace arduino + +#endif // ARCH_PORTDUINO diff --git a/src/platform/portduino/GpsdSerial.h b/src/platform/portduino/GpsdSerial.h new file mode 100644 index 000000000..dd7f5a976 --- /dev/null +++ b/src/platform/portduino/GpsdSerial.h @@ -0,0 +1,49 @@ +#pragma once +#ifdef ARCH_PORTDUINO + +#include "HardwareSerial.h" +#include +#include + +namespace arduino +{ + +// Presents a gpsd TCP NMEA stream as a HardwareSerial port. +// Connect gpsd at the configured host:port, send a WATCH request to enable +// raw NMEA output, then expose the incoming bytes through the standard +// available()/read() interface so the GPS class can feed them to TinyGPS++. +class GpsdSerial : public HardwareSerial +{ + static constexpr size_t RX_BUF_MAX = 4096; + static constexpr uint32_t RECONNECT_INTERVAL_MS = 5000; + + std::string _host; + int _port = 2947; + int _sockfd = -1; + std::deque _rxBuf; + uint32_t _lastConnectAttemptMs = 0; + + bool connectToGpsd(); + void fillBuffer(); + + public: + void setAddress(const std::string &host, int port = 2947); + + void begin(unsigned long baud) override { begin(baud, 0); } + void begin(unsigned long baud, uint16_t config) override; + void end() override; + + int available() override; + int peek() override; + int read() override; + void flush() override {} + size_t write(uint8_t) override { return 1; } // gpsd controls the hardware + using Print::write; + operator bool() override { return _sockfd >= 0; } +}; + +extern GpsdSerial gpsdSerial; + +} // namespace arduino + +#endif // ARCH_PORTDUINO diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 301223a2f..399426576 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -942,8 +942,23 @@ bool loadConfig(const char *configPath) std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); if (serialPath != "") { Serial1.setPath(serialPath); + portduino_config.gps_serial_path = serialPath; portduino_config.has_gps = 1; } + std::string gpsdHost = yamlConfig["GPS"]["GpsdHost"].as(""); + if (!gpsdHost.empty()) { + if (portduino_config.has_gps) { + LOG_WARN("GPS config: both SerialPath and GpsdHost are set; GpsdHost takes priority"); + } + int gpsdPort = yamlConfig["GPS"]["GpsdPort"].as(2947); + if (gpsdPort < 1 || gpsdPort > 65535) { + LOG_ERROR("GPS config: GpsdPort %d is out of range [1, 65535]; ignoring GPS config", gpsdPort); + } else { + portduino_config.gpsd_host = gpsdHost; + portduino_config.gpsd_port = gpsdPort; + portduino_config.has_gps = 1; + } + } } if (yamlConfig["GPIO"]["ExtraPins"]) { for (auto extra_pin : yamlConfig["GPIO"]["ExtraPins"]) { diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index d13efd983..73bfbdcf7 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -120,6 +120,9 @@ extern struct portduino_config_struct { // GPS bool has_gps = false; + std::string gps_serial_path = ""; + std::string gpsd_host = ""; + int gpsd_port = 2947; // I2C std::string i2cdev = ""; @@ -360,6 +363,18 @@ extern struct portduino_config_struct { out << YAML::EndMap; // GPIO } + if (has_gps) { + out << YAML::Key << "GPS" << YAML::Value << YAML::BeginMap; + if (!gpsd_host.empty()) { + out << YAML::Key << "GpsdHost" << YAML::Value << gpsd_host; + if (gpsd_port != 2947) + out << YAML::Key << "GpsdPort" << YAML::Value << gpsd_port; + } else if (!gps_serial_path.empty()) { + out << YAML::Key << "SerialPath" << YAML::Value << gps_serial_path; + } + out << YAML::EndMap; // GPS + } + if (i2cdev != "") { out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev;