From 78b4019d96b4625b0e87d37b3dfc0fde1a59fd4d Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 25 Oct 2026 21:53:11 -0400 Subject: [PATCH 1/3] Add runtime-configurable GPS pins and baud for companion External GPS modules are often wired to arbitrary header pins, but PIN_GPS_RX/TX/EN and the baud rate were fixed per variant, so such a GPS was never detected without a custom build. - New sensor settings gps_rx, gps_tx, gps_en, gps_baud via the existing custom vars. They show the effective value; setting a board default (or -1 / 0) follows the board again. - Companion persists them in NodePrefs (gps.pin_rx/pin_tx/pin_en/baud) and applies them before sensors.begin(); changes apply after reboot. - Listed even when no GPS was detected so bad pins can be fixed. - Pins used by the variant (LoRa, I2C, button, Vext, LED, ...) and ESP32 flash/PSRAM/USB-serial pins are rejected. - LocationProvider::setEnablePin() lets MicroNMEALocationProvider switch the enable pin at runtime. - Not applied to RAK WisBlock GPS boards. FAQ 5.17. Co-Authored-By: Claude Opus 5.5 --- docs/faq.md | 20 ++ examples/companion_radio/MyMesh.cpp | 8 + examples/companion_radio/MyMesh.h | 7 + examples/companion_radio/NodePrefs.h | 8 + examples/companion_radio/main.cpp | 3 + .../sensors/EnvironmentSensorManager.cpp | 211 +++++++++++++++++- .../sensors/EnvironmentSensorManager.h | 24 ++ src/helpers/sensors/LocationProvider.h | 2 + .../sensors/MicroNMEALocationProvider.h | 12 + 9 files changed, 290 insertions(+), 5 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index ade59bf0090..ba73b0264f1 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -65,6 +65,7 @@ A list of frequently-asked questions and answers for MeshCore - [5.14. Q: Are there projects built around MeshCore?](#514-q-are-there-projects-built-around-meshcore) - [5.15. Q: Are there client applications for Windows or Mac?](#515-q-are-there-client-applications-for-windows-or-mac) - [5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?](#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems) + - [5.17. Q: My external GPS is wired to different pins than the firmware expects. Do I need a custom build?](#517-q-my-external-gps-is-wired-to-different-pins-than-the-firmware-expects-do-i-need-a-custom-build) - [6. Troubleshooting](#6-troubleshooting) - [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago) - [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed) @@ -665,6 +666,25 @@ Both the Windows and Mac versions of the client app are fully unlocked and are f - MeshCore Advantages by MCarper: - MeshCore vs Meshtastic by austinmesh.org: +### 5.17. Q: My external GPS is wired to different pins than the firmware expects. Do I need a custom build? +**A:** Not on companion firmware. The GPS pins and baud rate can be changed at runtime through these custom variables (in the app, or with `meshcore-cli`): + +| Variable | Meaning | Default | +|------------|------------------------------------------------------------------|---------| +| `gps_tx` | MCU pin connected to the GPS module's **TX** (MCU receives here) | board's `PIN_GPS_TX` | +| `gps_rx` | MCU pin connected to the GPS module's **RX** (MCU transmits here) | board's `PIN_GPS_RX` | +| `gps_en` | MCU pin that powers/enables the GPS (driven high when GPS is on) | board's `PIN_GPS_EN` (`-1` = none) | +| `gps_baud` | GPS UART baud rate (4800, 9600, 19200, 38400, 57600, 115200, 230400) | board's `GPS_BAUD_RATE`, else 9600 | + +The variables always show the value in effect, so on a stock Heltec V3 they read `gps_rx:47`, `gps_tx:48`, `gps_en:26`, `gps_baud:9600`. Setting a variable to the board's built-in value (or to `-1`, or `0` for `gps_baud`) makes it follow the board default again. The values are saved and applied on the **next reboot**. They are listed even when no GPS was detected, so a wrong setting can always be fixed. Once the GPS is found, the usual `gps` on/off variable appears. + +Pins already used by the board (LoRa, display I2C, button, Vext, LED, and on ESP32 the flash/PSRAM and USB serial pins) are rejected. Pins the board definition doesn't declare can't be checked, so double-check your wiring. Not available on RAK WisBlock GPS boards, which detect the GPS socket automatically. + +Example for a Heltec V3 with the GPS on GPIO34 (TX) / GPIO33 (RX), powered from GPIO42: +``` +meshcli -s /dev/ttyUSB0 set gps_tx 34 set gps_rx 33 set gps_en 42 set gps_baud 9600 reboot +``` + --- diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6112b2dbb17..e05c0479727 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1885,6 +1885,14 @@ void MyMesh::handleCmdFrame(size_t len) { savePrefs(); } #endif + #if ENV_GPS_PIN_CONFIG + // pin/baud overrides (validated and normalised by the sensor manager); applied on next boot + if (strcmp(sp, "gps_rx") == 0 || strcmp(sp, "gps_tx") == 0 || + strcmp(sp, "gps_en") == 0 || strcmp(sp, "gps_baud") == 0) { + sensors.getGPSOverrides(_prefs.gps_pin_rx, _prefs.gps_pin_tx, _prefs.gps_pin_en, _prefs.gps_baud); + savePrefs(); + } + #endif writeOKFrame(); } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 944790df922..6d46f5306e1 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -201,6 +201,13 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { _prefs.clearDirty(); } +#if ENV_GPS_PIN_CONFIG + // must run before sensors.begin(), which opens the GPS UART + void applyGpsPinPrefs() { + sensors.configureGPS(_prefs.gps_pin_rx, _prefs.gps_pin_tx, _prefs.gps_pin_en, _prefs.gps_baud); + } +#endif + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 3cceed56668..646d5139257 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -35,6 +35,10 @@ class NodePrefs : public ConfigSerializer { // persisted to file uint8_t vibe_quiet = 0; uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled) uint32_t gps_interval = 0; // GPS read interval in seconds + int8_t gps_pin_rx = -1; // GPS pin overrides, -1 = variant default (applied at boot) + int8_t gps_pin_tx = -1; + int8_t gps_pin_en = -1; + uint32_t gps_baud = 0; // 0 = variant default uint8_t autoadd_config = 0; // bitmask for auto-add contacts config uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted) uint8_t radio_fem_rxgain = 0; // external LoRa FEM RX gain (LNA) @@ -130,6 +134,10 @@ class NodePrefs : public ConfigSerializer { // persisted to file def("en", _parent->gps_enabled); // boolean def("int", _parent->gps_interval); // interval in seconds def("adv_loc", _parent->advert_loc_policy); + def("pin_rx", _parent->gps_pin_rx); + def("pin_tx", _parent->gps_pin_tx); + def("pin_en", _parent->gps_pin_en); + def("baud", _parent->gps_baud); } public: GPSPrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 839d4958336..e1abe95b97b 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -275,6 +275,9 @@ void setup() { #endif the_mesh.startInterface(interface_manager); +#if ENV_GPS_PIN_CONFIG + the_mesh.applyGpsPinPrefs(); +#endif sensors.begin(); #if ENV_INCLUDE_GPS == 1 diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 0dc892c3a52..9b4989e32bc 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -1,6 +1,12 @@ #include "EnvironmentSensorManager.h" #include +#if ENV_GPS_PIN_CONFIG + #include // RADIOLIB_NC in variant pin defines + #ifdef ESP32 + #include + #endif +#endif #if ENV_PIN_SDA && ENV_PIN_SCL #define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors @@ -699,11 +705,42 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen } +#if ENV_GPS_PIN_CONFIG +#ifdef PIN_GPS_RX + #define GPS_DEFAULT_RX PIN_GPS_RX +#else + #define GPS_DEFAULT_RX -1 +#endif +#ifdef PIN_GPS_TX + #define GPS_DEFAULT_TX PIN_GPS_TX +#else + #define GPS_DEFAULT_TX -1 +#endif +#ifdef PIN_GPS_EN + #define GPS_DEFAULT_EN PIN_GPS_EN +#else + #define GPS_DEFAULT_EN -1 +#endif +#ifdef GPS_BAUD_RATE + #define GPS_DEFAULT_BAUD GPS_BAUD_RATE +#else + #define GPS_DEFAULT_BAUD 9600 +#endif + +// Listed even when no GPS was detected, so wrong pins can be fixed from the app. +// Values shown are the effective ones (override or board default). Changes take effect after reboot. +static const char* const GPS_CFG_NAMES[] = { "gps_rx", "gps_tx", "gps_en", "gps_baud" }; +static const int GPS_CFG_COUNT = sizeof(GPS_CFG_NAMES) / sizeof(GPS_CFG_NAMES[0]); +#endif + int EnvironmentSensorManager::getNumSettings() const { int settings = 0; #if ENV_INCLUDE_GPS if (gps_detected) settings++; // only show GPS setting if GPS is detected #endif + #if ENV_GPS_PIN_CONFIG + if (gps_cfg_enabled) settings += GPS_CFG_COUNT; + #endif return settings; } @@ -714,6 +751,11 @@ const char* EnvironmentSensorManager::getSettingName(int i) const { return "gps"; } #endif + #if ENV_GPS_PIN_CONFIG + if (gps_cfg_enabled && i >= settings && i < settings + GPS_CFG_COUNT) { + return GPS_CFG_NAMES[i - settings]; + } + #endif return NULL; } @@ -724,6 +766,17 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const { return gps_active ? "1" : "0"; } #endif + #if ENV_GPS_PIN_CONFIG + if (gps_cfg_enabled && i >= settings && i < settings + GPS_CFG_COUNT) { + int k = i - settings; + long v = k == 0 ? (gps_pin_rx != -1 ? gps_pin_rx : GPS_DEFAULT_RX) + : k == 1 ? (gps_pin_tx != -1 ? gps_pin_tx : GPS_DEFAULT_TX) + : k == 2 ? (gps_pin_en != -1 ? gps_pin_en : GPS_DEFAULT_EN) + : (long)(gps_baud != 0 ? gps_baud : GPS_DEFAULT_BAUD); + snprintf(gps_cfg_val[k], sizeof(gps_cfg_val[k]), "%ld", v); + return gps_cfg_val[k]; + } + #endif return NULL; } @@ -743,20 +796,168 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val return true; } #endif + #if ENV_GPS_PIN_CONFIG + if (gps_cfg_enabled) { + if (strcmp(name, "gps_baud") == 0) { + uint32_t baud = atol(value); + if (baud != 0 && baud != 4800 && baud != 9600 && baud != 19200 && baud != 38400 && + baud != 57600 && baud != 115200 && baud != 230400) { + return false; + } + gps_baud = baud == GPS_DEFAULT_BAUD ? 0 : baud; + return true; + } + int8_t* target = strcmp(name, "gps_rx") == 0 ? &gps_pin_rx + : strcmp(name, "gps_tx") == 0 ? &gps_pin_tx + : strcmp(name, "gps_en") == 0 ? &gps_pin_en : NULL; + if (target) { + int pin = atoi(value); + int def = target == &gps_pin_rx ? GPS_DEFAULT_RX : target == &gps_pin_tx ? GPS_DEFAULT_TX : GPS_DEFAULT_EN; + if (pin == def) pin = -1; // the board default (or -1) means: follow the board default + if (pin != -1) { + // MCU drives the GPS RX line and the enable pin; GPS TX is input-only on our side + if (!isGPSPinAllowed(pin, target != &gps_pin_tx)) return false; + // no sharing between overrides (defaults not checked, so lines can be swapped one at a time) + if (target != &gps_pin_rx && pin == gps_pin_rx) return false; + if (target != &gps_pin_tx && pin == gps_pin_tx) return false; + if (target != &gps_pin_en && pin == gps_pin_en) return false; + } + *target = pin; + return true; + } + } + #endif return false; // not supported } -#if ENV_INCLUDE_GPS -void EnvironmentSensorManager::initBasicGPS() { +#if ENV_GPS_PIN_CONFIG +void EnvironmentSensorManager::configureGPS(int rx, int tx, int en, uint32_t baud) { + gps_cfg_enabled = true; + char buf[12]; + // route through setSettingValue() so stored values get the same validation as app input + snprintf(buf, sizeof(buf), "%d", rx); + if (!setSettingValue("gps_rx", buf)) gps_pin_rx = -1; + snprintf(buf, sizeof(buf), "%d", tx); + if (!setSettingValue("gps_tx", buf)) gps_pin_tx = -1; + snprintf(buf, sizeof(buf), "%d", en); + if (!setSettingValue("gps_en", buf)) gps_pin_en = -1; + snprintf(buf, sizeof(buf), "%lu", (unsigned long)baud); + if (!setSettingValue("gps_baud", buf)) gps_baud = 0; +} - Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); +// Reject pins the board already uses, so a typo can't take out the radio, display or flash. +bool EnvironmentSensorManager::isGPSPinAllowed(int pin, bool output) const { + if (pin < 0) return false; + static const int reserved[] = { + #ifdef P_LORA_NSS + P_LORA_NSS, + #endif + #ifdef P_LORA_DIO_1 + P_LORA_DIO_1, + #endif + #ifdef P_LORA_RESET + P_LORA_RESET, + #endif + #ifdef P_LORA_BUSY + P_LORA_BUSY, + #endif + #ifdef P_LORA_SCLK + P_LORA_SCLK, + #endif + #ifdef P_LORA_MISO + P_LORA_MISO, + #endif + #ifdef P_LORA_MOSI + P_LORA_MOSI, + #endif + #ifdef P_LORA_TX_LED + P_LORA_TX_LED, + #endif + #ifdef PIN_BOARD_SDA + PIN_BOARD_SDA, + #endif + #ifdef PIN_BOARD_SCL + PIN_BOARD_SCL, + #endif + #ifdef ENV_PIN_SDA + ENV_PIN_SDA, + #endif + #ifdef ENV_PIN_SCL + ENV_PIN_SCL, + #endif + #ifdef PIN_USER_BTN + PIN_USER_BTN, + #endif + #ifdef PIN_VEXT_EN + PIN_VEXT_EN, + #endif + #ifdef PIN_BUZZER + PIN_BUZZER, + #endif + #ifdef SERIAL_RX + SERIAL_RX, + #endif + #ifdef SERIAL_TX + SERIAL_TX, + #endif + -1 + }; + for (size_t i = 0; i < sizeof(reserved) / sizeof(reserved[0]); i++) { + if (pin == reserved[i]) return false; + } + #if defined(ESP32) + if (!(output ? GPIO_IS_VALID_OUTPUT_GPIO(pin) : GPIO_IS_VALID_GPIO(pin))) return false; + #if defined(CONFIG_IDF_TARGET_ESP32S3) + if (pin >= 27 && pin <= 32) return false; // SPI flash + #ifdef BOARD_HAS_PSRAM + if (pin == 26) return false; // PSRAM CS + #endif + #if CONFIG_SPIRAM_MODE_OCT + if (pin >= 33 && pin <= 37) return false; // octal PSRAM + #endif + #if ARDUINO_USB_CDC_ON_BOOT + if (pin == 19 || pin == 20) return false; // native USB + #else + if (pin == 43 || pin == 44) return false; // UART0 (USB bridge) + #endif + #elif defined(CONFIG_IDF_TARGET_ESP32) + if (pin >= 6 && pin <= 11) return false; // SPI flash + if (pin == 1 || pin == 3) return false; // UART0 (USB bridge) + #endif + #elif defined(PINS_COUNT) + if (pin >= (int)PINS_COUNT) return false; + #endif + return true; +} +#endif +#if ENV_INCLUDE_GPS +void EnvironmentSensorManager::initBasicGPS() { + #ifdef PIN_GPS_RX + int pin_rx = PIN_GPS_RX; + #else + int pin_rx = -1; + #endif + #ifdef PIN_GPS_TX + int pin_tx = PIN_GPS_TX; + #else + int pin_tx = -1; + #endif #ifdef GPS_BAUD_RATE - Serial1.begin(GPS_BAUD_RATE); + uint32_t baud = GPS_BAUD_RATE; #else - Serial1.begin(9600); + uint32_t baud = 9600; + #endif + #if ENV_GPS_PIN_CONFIG + if (gps_pin_rx != -1) pin_rx = gps_pin_rx; + if (gps_pin_tx != -1) pin_tx = gps_pin_tx; + if (gps_baud != 0) baud = gps_baud; + if (gps_pin_en != -1) _location->setEnablePin(gps_pin_en); #endif + Serial1.setPins(pin_tx, pin_rx); + Serial1.begin(baud); + // Try to detect if GPS is physically connected to determine if we should expose the setting _location->begin(); _location->reset(); diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 17706cbc492..19b73e9e4aa 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -4,6 +4,11 @@ #include #include +// Not for RAK WisBlock GPS (same condition as RAK_WISBLOCK_GPS in the .cpp), which probes sockets instead +#if ENV_INCLUDE_GPS && !(defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)) + #define ENV_GPS_PIN_CONFIG 1 +#endif + class EnvironmentSensorManager : public SensorManager { protected: static const int MAX_ACTIVE_SENSORS = 16; @@ -25,6 +30,17 @@ class EnvironmentSensorManager : public SensorManager { #if ENV_INCLUDE_GPS LocationProvider* _location; + #if ENV_GPS_PIN_CONFIG + // Runtime overrides of PIN_GPS_RX/TX/EN and GPS_BAUD_RATE (-1 / 0 = variant default). + // Only exposed as settings once the app calls configureGPS(), since it must persist them. + bool gps_cfg_enabled = false; + int8_t gps_pin_rx = -1; + int8_t gps_pin_tx = -1; + int8_t gps_pin_en = -1; + uint32_t gps_baud = 0; + mutable char gps_cfg_val[4][12]; + bool isGPSPinAllowed(int pin, bool output) const; + #endif void start_gps(); void stop_gps(); void initBasicGPS(); @@ -39,6 +55,14 @@ class EnvironmentSensorManager : public SensorManager { EnvironmentSensorManager(LocationProvider &location): _location(&location){}; LocationProvider* getLocationProvider() { return _location; } bool isGPSDetected() const override { return gps_detected; } + #if ENV_GPS_PIN_CONFIG + // Call before begin(). Invalid values fall back to the variant defaults. + void configureGPS(int rx, int tx, int en, uint32_t baud); + // Current overrides (-1 / 0 = board default), for persisting after a setting change. + void getGPSOverrides(int8_t& rx, int8_t& tx, int8_t& en, uint32_t& baud) const { + rx = gps_pin_rx; tx = gps_pin_tx; en = gps_pin_en; baud = gps_baud; + } + #endif #else EnvironmentSensorManager(){}; #endif diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index bc4f1fe7648..03e038ce172 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -26,6 +26,8 @@ class LocationProvider { virtual void stop() = 0; virtual void loop() = 0; virtual bool isEnabled() = 0; + // Override the power/enable pin chosen at construction (-1 = none). Call before begin(). + virtual void setEnablePin(int pin) { } // Format compact diagnostics in the caller-provided buffer. Providers that // have more information (for example UART counters) can override this. diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index 37ef5762c48..bd30d073b40 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -147,6 +147,18 @@ public : release(); } + void setEnablePin(int pin) override { + if (pin == _pin_en) return; + if (_pin_en != -1) { + pinMode(_pin_en, INPUT); // release the variant's default pin + } + _pin_en = pin; + if (_pin_en != -1) { + pinMode(_pin_en, OUTPUT); + digitalWrite(_pin_en, !GPS_EN_ACTIVE); + } + } + bool isEnabled() override { // directly read the enable pin if present as gps can be // activated/deactivated outside of here ... From be2dec425ca6ef09b9aaeb2d5c42ec568b01765d Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 26 Oct 2026 11:00:28 +0000 Subject: [PATCH 2/3] Reject non-numeric and out-of-range GPS pin/baud values gps_rx/gps_tx/gps_en were parsed with atoi() and validated as int, then stored in int8_t. A value above 127 could pass validation and wrap on store: on a Heltec V3, gps_rx=300 was accepted and saved as 44 (UART0 RX, which the check itself rejects), and gps_en=290 was saved as 34 while gps_tx was 34, bypassing the duplicate-pin check. On ESP32, GPIO_IS_VALID_GPIO() shifts 1ULL by the pin with no bounds check, so pins of 64 and up were undefined behaviour. - Parse pins and baud as whole decimal numbers: "abc" or "34x" is now an error instead of 0. Pins must be -1..127 before any other check. - On ESP32, reject pins >= SOC_GPIO_PIN_COUNT before GPIO_IS_VALID_*. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/helpers/sensors/EnvironmentSensorManager.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 9b4989e32bc..81199b7cccc 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -731,6 +731,14 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen // Values shown are the effective ones (override or board default). Changes take effect after reboot. static const char* const GPS_CFG_NAMES[] = { "gps_rx", "gps_tx", "gps_en", "gps_baud" }; static const int GPS_CFG_COUNT = sizeof(GPS_CFG_NAMES) / sizeof(GPS_CFG_NAMES[0]); + +// Whole decimal number within [lo, hi]. Rejects empty or trailing text, so "abc" isn't read as 0 +// and out-of-range pins can't wrap when stored in int8_t. +static bool parseGPSCfg(const char* s, long lo, long hi, long& out) { + char* end; + out = strtol(s, &end, 10); + return end != s && *end == 0 && out >= lo && out <= hi; +} #endif int EnvironmentSensorManager::getNumSettings() const { @@ -799,7 +807,8 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val #if ENV_GPS_PIN_CONFIG if (gps_cfg_enabled) { if (strcmp(name, "gps_baud") == 0) { - uint32_t baud = atol(value); + long baud; + if (!parseGPSCfg(value, 0, 230400, baud)) return false; if (baud != 0 && baud != 4800 && baud != 9600 && baud != 19200 && baud != 38400 && baud != 57600 && baud != 115200 && baud != 230400) { return false; @@ -811,7 +820,8 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val : strcmp(name, "gps_tx") == 0 ? &gps_pin_tx : strcmp(name, "gps_en") == 0 ? &gps_pin_en : NULL; if (target) { - int pin = atoi(value); + long pin; + if (!parseGPSCfg(value, -1, INT8_MAX, pin)) return false; int def = target == &gps_pin_rx ? GPS_DEFAULT_RX : target == &gps_pin_tx ? GPS_DEFAULT_TX : GPS_DEFAULT_EN; if (pin == def) pin = -1; // the board default (or -1) means: follow the board default if (pin != -1) { @@ -906,6 +916,7 @@ bool EnvironmentSensorManager::isGPSPinAllowed(int pin, bool output) const { if (pin == reserved[i]) return false; } #if defined(ESP32) + if (pin >= SOC_GPIO_PIN_COUNT) return false; // GPIO_IS_VALID_* shift by pin unchecked if (!(output ? GPIO_IS_VALID_OUTPUT_GPIO(pin) : GPIO_IS_VALID_GPIO(pin))) return false; #if defined(CONFIG_IDF_TARGET_ESP32S3) if (pin >= 27 && pin <= 32) return false; // SPI flash From e29969a11429ed65d146b9a8f151450e4f98d41c Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 26 Oct 2026 11:31:04 +0000 Subject: [PATCH 3/3] Advance the settings index past the GPS pin settings getSettingName()/getSettingValue() returned the pin settings by range but, unlike the "gps" entry, did not advance the running index. Any setting added after them would be looked up at the wrong index and return NULL while getNumSettings() still counted it; CMD_GET_CUSTOM_VARS strcpy()s that name. Advance the index so later settings (such as gps_duty in a separate PR) can be appended in either order. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/helpers/sensors/EnvironmentSensorManager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 81199b7cccc..c702c7b792d 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -763,6 +763,7 @@ const char* EnvironmentSensorManager::getSettingName(int i) const { if (gps_cfg_enabled && i >= settings && i < settings + GPS_CFG_COUNT) { return GPS_CFG_NAMES[i - settings]; } + if (gps_cfg_enabled) settings += GPS_CFG_COUNT; // keep the index right for settings listed after these #endif return NULL; } @@ -784,6 +785,7 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const { snprintf(gps_cfg_val[k], sizeof(gps_cfg_val[k]), "%ld", v); return gps_cfg_val[k]; } + if (gps_cfg_enabled) settings += GPS_CFG_COUNT; #endif return NULL; }