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 <benmmeadors@gmail.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
This commit is contained in:
co-authored by
GitHub
Ben Meadors
Jonathan Bennett
parent
23bb55d965
commit
2fdb722e00
@@ -104,7 +104,9 @@ Lora:
|
|||||||
### Define GPS
|
### Define GPS
|
||||||
|
|
||||||
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:
|
# ExtraPins:
|
||||||
# - 22
|
# - 22
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -25,6 +25,7 @@
|
|||||||
#include "ubx.h"
|
#include "ubx.h"
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
|
#include "GpsdSerial.h"
|
||||||
#include "PortduinoGlue.h"
|
#include "PortduinoGlue.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -97,8 +98,9 @@ struct GPSProbeCacheRecord {
|
|||||||
|
|
||||||
bool isValidGnssModel(uint8_t model)
|
bool isValidGnssModel(uint8_t model)
|
||||||
{
|
{
|
||||||
// Keep persisted values bounded to known enum range.
|
// Only real chip identifiers belong in the probe cache.
|
||||||
return model <= static_cast<uint8_t>(GNSS_MODEL_CM121);
|
// GNSS_MODEL_UNKNOWN and GNSS_MODEL_GENERIC_NMEA are runtime-only values.
|
||||||
|
return model != static_cast<uint8_t>(GNSS_MODEL_UNKNOWN) && model < static_cast<uint8_t>(GNSS_MODEL_GENERIC_NMEA);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isValidProbeBaud(uint32_t baud)
|
bool isValidProbeBaud(uint32_t baud)
|
||||||
@@ -1901,6 +1903,10 @@ std::unique_ptr<GPS> GPS::createGps()
|
|||||||
// They are not used for any hardware access.
|
// They are not used for any hardware access.
|
||||||
_rx_gpio = 1;
|
_rx_gpio = 1;
|
||||||
_tx_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
|
} else
|
||||||
return nullptr;
|
return nullptr;
|
||||||
#endif
|
#endif
|
||||||
@@ -1910,6 +1916,11 @@ std::unique_ptr<GPS> GPS::createGps()
|
|||||||
auto new_gps = std::unique_ptr<GPS>(new GPS());
|
auto new_gps = std::unique_ptr<GPS>(new GPS());
|
||||||
new_gps->rx_gpio = _rx_gpio;
|
new_gps->rx_gpio = _rx_gpio;
|
||||||
new_gps->tx_gpio = _tx_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();
|
GpioVirtPin *virtPin = new GpioVirtPin();
|
||||||
new_gps->enablePin = virtPin; // Always at least populate a virtual pin
|
new_gps->enablePin = virtPin; // Always at least populate a virtual pin
|
||||||
|
|||||||
+2
-1
@@ -42,7 +42,8 @@ typedef enum {
|
|||||||
GNSS_MODEL_AG3335,
|
GNSS_MODEL_AG3335,
|
||||||
GNSS_MODEL_AG3352,
|
GNSS_MODEL_AG3352,
|
||||||
GNSS_MODEL_LS20031,
|
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;
|
} GnssModel_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
#ifdef ARCH_PORTDUINO
|
||||||
|
|
||||||
|
#include "GpsdSerial.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
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<size_t>(n) < space) ? static_cast<size_t>(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<int>(_rxBuf.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
int GpsdSerial::peek()
|
||||||
|
{
|
||||||
|
if (_rxBuf.empty())
|
||||||
|
return -1;
|
||||||
|
return static_cast<int>(_rxBuf.front());
|
||||||
|
}
|
||||||
|
|
||||||
|
int GpsdSerial::read()
|
||||||
|
{
|
||||||
|
if (_rxBuf.empty())
|
||||||
|
return -1;
|
||||||
|
uint8_t c = _rxBuf.front();
|
||||||
|
_rxBuf.pop_front();
|
||||||
|
return static_cast<int>(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace arduino
|
||||||
|
|
||||||
|
#endif // ARCH_PORTDUINO
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
#ifdef ARCH_PORTDUINO
|
||||||
|
|
||||||
|
#include "HardwareSerial.h"
|
||||||
|
#include <deque>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<uint8_t> _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
|
||||||
@@ -942,8 +942,23 @@ bool loadConfig(const char *configPath)
|
|||||||
std::string serialPath = yamlConfig["GPS"]["SerialPath"].as<std::string>("");
|
std::string serialPath = yamlConfig["GPS"]["SerialPath"].as<std::string>("");
|
||||||
if (serialPath != "") {
|
if (serialPath != "") {
|
||||||
Serial1.setPath(serialPath);
|
Serial1.setPath(serialPath);
|
||||||
|
portduino_config.gps_serial_path = serialPath;
|
||||||
portduino_config.has_gps = 1;
|
portduino_config.has_gps = 1;
|
||||||
}
|
}
|
||||||
|
std::string gpsdHost = yamlConfig["GPS"]["GpsdHost"].as<std::string>("");
|
||||||
|
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<int>(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"]) {
|
if (yamlConfig["GPIO"]["ExtraPins"]) {
|
||||||
for (auto extra_pin : yamlConfig["GPIO"]["ExtraPins"]) {
|
for (auto extra_pin : yamlConfig["GPIO"]["ExtraPins"]) {
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ extern struct portduino_config_struct {
|
|||||||
|
|
||||||
// GPS
|
// GPS
|
||||||
bool has_gps = false;
|
bool has_gps = false;
|
||||||
|
std::string gps_serial_path = "";
|
||||||
|
std::string gpsd_host = "";
|
||||||
|
int gpsd_port = 2947;
|
||||||
|
|
||||||
// I2C
|
// I2C
|
||||||
std::string i2cdev = "";
|
std::string i2cdev = "";
|
||||||
@@ -360,6 +363,18 @@ extern struct portduino_config_struct {
|
|||||||
out << YAML::EndMap; // GPIO
|
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 != "") {
|
if (i2cdev != "") {
|
||||||
out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap;
|
out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap;
|
||||||
out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev;
|
out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev;
|
||||||
|
|||||||
Reference in New Issue
Block a user