Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
},
"[powershell]": {
"editor.defaultFormatter": "ms-vscode.powershell"
}
},
"idf.currentSetup": "C:\\esp\\v6.0.1\\esp-idf"
Comment on lines +13 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "git status:"
git status --short || true

echo
echo "tracked settings files:"
git ls-files | grep -E '(^|/)(settings|extensions)\.json$' || true

echo
echo ".vscode/settings.json if present:"
if [ -f .vscode/settings.json ]; then
  nl -ba .vscode/settings.json | sed -n '1,80p'
else
  echo "missing"
fi

echo
echo "search idf.currentSetup in repo:"
rg -n '"idf\.currentSetup"|idf\.currentSetup' . || true

Repository: meshtastic/firmware

Length of output: 338


🌐 Web query:

esp-idf vscode extension idf.currentSetup setting currentSetup documentation

💡 Result:

The idf.currentSetup setting in the ESP-IDF VS Code extension is a configuration identifier used to store and track the currently selected ESP-IDF installation [1][2]. Key details regarding this setting: * Function: It serves as a pointer to the specific ESP-IDF setup (path, tools, and environment configuration) being used for the current project or workspace folder [3][4]. When you select an ESP-IDF version using the command ESP-IDF: Select Current ESP-IDF Version, the extension saves this selection in idf.currentSetup to ensure the correct environment variables are applied to your project [3][5]. * Scope: The setting has a Resource scope [1][2], meaning it is intended to be stored in workspace settings (e.g., .vscode/settings.json) or as part of a workspace folder state [3]. * Management: Users typically do not edit this setting manually. Instead, it is managed automatically by the extension through the ESP-IDF: Select Current ESP-IDF Version command [3][5]. * Precedence: If you have manually configured ESP-IDF environment variables (such as IDF_PATH, IDF_TOOLS_PATH, or IDF_PYTHON_ENV_PATH) in your VS Code settings, the extension will prioritize those over the setup selected via idf.currentSetup [4]. To utilize the idf.currentSetup configuration, you should ensure that manual environment variable overrides are cleared [4]. For further details on configuring or troubleshooting your setup, you can refer to the official ESP-IDF Extension Settings documentation or the extension's installation guide [2][4].

Citations:


🌐 Web query:

site:docs.espressif.com projects vs-code-esp-idf-extension currentSetup setting

💡 Result:

The idf.currentSetup setting in the ESP-IDF extension for Visual Studio Code is used to store the identifier for your current ESP-IDF installation configuration [1]. It is a resource-scoped setting [1]. When you use the command "ESP-IDF: Select Current ESP-IDF Version" from the VS Code Command Palette, the extension displays a list of available ESP-IDF setups [2]. Once you select a setup, the extension automatically updates idf.currentSetup with the path to the chosen ESP-IDF installation [2]. This selection ensures the extension correctly configures the required environment variables—such as IDF_PATH, IDF_TOOLS_PATH, and IDF_PYTHON_ENV_PATH—for that specific project [2]. Key considerations regarding this setting: - Manual vs. Automatic: While the extension handles this setting automatically when you use the "Select Current ESP-IDF Version" command, it acts as the primary reference for the extension to know which ESP-IDF environment to use [2]. - Priority: If you have manually defined ESP-IDF environment variables in your system or VS Code environment, the extension will prioritize those over the setup selected in idf.currentSetup [2]. To ensure the extension uses your selected idf.currentSetup, you should clear any manually set ESP-IDF environment variables [2]. - Storage: When a setup is selected, the extension configures environment variables and saves them as part of the workspace folder state [2].

Citations:


🏁 Script executed:

#!/bin/sh
set -u

echo "tracked settings files:"
git ls-files | grep -E '(^|/)(settings|extensions)\.json$' || true

echo
echo ".vscode/settings.json:"
cat -n .vscode/settings.json | sed -n '1,90p' || true

echo
echo "search idf.currentSetup in repo:"
grep -RIn '"idf\.currentSetup"' . || true

Repository: meshtastic/firmware

Length of output: 890


🌐 Web query:

GitHub esp-idf extension idf.currentSetup settings currentSetup extension current.setup

💡 Result:

The idf.currentSetup setting in the ESP-IDF VS Code extension is an identifier used to track the currently selected ESP-IDF installation [1][2]. It is primarily managed automatically by the extension rather than being manually configured by the user [3]. Key details regarding idf.currentSetup include: Purpose and Function: When you use the command ESP-IDF: Select Current ESP-IDF Version, the extension selects an ESP-IDF setup from your available installations [4][3]. This selected version is saved as the idf.currentSetup in your workspace state, allowing the extension to automatically configure the necessary environment variables (such as IDF_PATH, IDF_TOOLS_PATH, and Python paths) for that specific project [5][3]. Management: - Automatic Configuration: The value is set automatically when you switch versions via the command palette [3]. - Priority: If you have manually defined environment variables using idf.customExtraVars in your settings.json, the extension will prioritize those manual configurations over the idf.currentSetup selection [5][3]. To use an ESP-IDF setup selected via idf.currentSetup, you should ensure that conflicting environment variables in idf.customExtraVars are removed [5]. - Scope: The setting is scoped to the Resource (workspace folder) level [1][2]. Usage: If you need to change your active ESP-IDF version, you should use the VS Code Command Palette: 1. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P). 2. Type and select ESP-IDF: Select Current ESP-IDF Version. 3. Choose the desired setup from the provided list [4][6]. You can verify the current configuration and ensure it is working correctly by running the ESP-IDF: Doctor Command from the Command Palette [4][7].

Citations:


Remove the concrete ESP-IDF path from .vscode/settings.json.

Line 14 pins every workspace to C:\esp\v6.0.1\esp-idf, leaving non-Windows or other-contributor setups without a valid ESP-IDF reference. Keep this as manual/user or workspace-local ESP-IDF state instead of committing a hard-coded setup path.

Proposed fix
   "[powershell]": {
     "editor.defaultFormatter": "ms-vscode.powershell"
-  },
-  "idf.currentSetup": "C:\\esp\\v6.0.1\\esp-idf"
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
},
"idf.currentSetup": "C:\\esp\\v6.0.1\\esp-idf"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.vscode/settings.json around lines 13 - 14, Remove the committed
"idf.currentSetup" setting from the workspace settings so no concrete ESP-IDF
installation path is shared. Leave ESP-IDF setup to each contributor’s user or
workspace-local configuration while preserving the other settings.

}
3 changes: 2 additions & 1 deletion src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);

#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT) || defined(RAK_WISMESH_TAP_V2)) && \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT) || defined(HELTEC_V4_R2) || \
defined(RAK_WISMESH_TAP_V2)) && \
HAS_TFT
// switch BT off by default; use TFT programming mode or hotkey to enable
config.bluetooth.enabled = false;
Expand Down
10 changes: 10 additions & 0 deletions src/mesh/RadioInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
#include "meshUtils.h" // for pow_of_2
#include "sleep.h"
#include <assert.h>
#ifdef ARCH_ESP32
#include "driver/gpio.h"
#endif
#include <pb_decode.h>
#include <pb_encode.h>
#include <string.h>
Expand Down Expand Up @@ -374,6 +377,13 @@ std::unique_ptr<RadioInterface> initLoRa()
{
std::unique_ptr<RadioInterface> rIf = nullptr;

#ifdef ARCH_ESP32
// RadioLib calls detachInterrupt() on the IRQ pin before attaching a new one. On ESP32 this
// invokes gpio_isr_handler_remove() which requires gpio_install_isr_service() to have been
// called first. Install it here so the call is a no-op if it arrives again later.
gpio_install_isr_service(0);
#endif

#if ARCH_PORTDUINO
SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
#else
Expand Down
36 changes: 7 additions & 29 deletions src/mesh/api/PacketAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,19 @@ PacketAPI *PacketAPI::create(PacketServer *_server)
}

PacketAPI::PacketAPI(PacketServer *_server)
: concurrency::OSThread("PacketAPI"), isConnected(false), programmingMode(false), server(_server)
: concurrency::OSThread("PacketAPI"), isConnected(false), server(_server)
{
api_type = TYPE_PACKET;
}

int32_t PacketAPI::runOnce()
{
bool success = false;
#ifndef ARCH_PORTDUINO
if (config.bluetooth.enabled) {
if (!programmingMode) {
// in programmingMode we don't send any packets to the client except this one notify
programmingMode = true;
success = notifyProgrammingMode();
}
} else
#endif
{
success = sendPacket();
}
// Always run the full config sync regardless of bluetooth state. The device-ui (MUI) must
// receive deviceuiConfig first so its state advances past eBootScreenDone before the
// bluetooth config arrives; only then does updateBluetoothConfig() skip enterProgrammingMode().
// Sending only the bluetooth config via notifyProgrammingMode() when BT is enabled caused the
// TFT screen to be permanently stuck on the ">> Programming mode <<" screen.
bool success = sendPacket();
success |= receivePacket();
return success ? 10 : 50;
}
Expand Down Expand Up @@ -107,21 +100,6 @@ bool PacketAPI::sendPacket(void)
return false;
}

bool PacketAPI::notifyProgrammingMode(void)
{
// tell the client we are in programming mode by sending only the bluetooth config state
LOG_INFO("force client into programmingMode");
memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
fromRadioScratch.id = nodeDB->getNodeNum();
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag;
fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag;
fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth;
return server->sendPacket(DataPacket<meshtastic_FromRadio>(0, fromRadioScratch));
}

/**
* return true if we got (once!) contact from our client and the server send queue is not full
*/
bool PacketAPI::checkIsConnected()
{
isConnected |= server->hasData();
Expand Down
2 changes: 0 additions & 2 deletions src/mesh/api/PacketAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ class PacketAPI : public PhoneAPI, public concurrency::OSThread
private:
bool receivePacket(void);
bool sendPacket(void);
bool notifyProgrammingMode(void);

bool isConnected;
bool programmingMode;
PacketServer *server;
uint8_t txBuf[MAX_TO_FROM_RADIO_SIZE] = {0}; // dummy buf to obey PhoneAPI
};
Expand Down
2 changes: 2 additions & 0 deletions src/platform/esp32/architecture.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT
#elif defined(T_LORA_PAGER)
#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
#elif defined(HELTEC_V4_R2)
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_V4
#elif defined(HELTEC_V4)
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_V4
#elif defined(M5STACK_UNITC6L)
Expand Down
58 changes: 58 additions & 0 deletions variants/esp32s3/heltec_v4_r2/pins_arduino.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h

#include <stdint.h>

#define USB_VID 0x303a
#define USB_PID 0x1001

static const uint8_t TX = 43;
static const uint8_t RX = 44;

static const uint8_t SDA = 17;
static const uint8_t SCL = 18;

static const uint8_t SS = 8;
static const uint8_t MOSI = 10;
static const uint8_t MISO = 11;
static const uint8_t SCK = 9;

static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
static const uint8_t A6 = 7;
static const uint8_t A7 = 8;
static const uint8_t A8 = 9;
static const uint8_t A9 = 10;
static const uint8_t A10 = 11;
static const uint8_t A11 = 12;
static const uint8_t A12 = 13;
static const uint8_t A13 = 14;
static const uint8_t A14 = 15;
static const uint8_t A15 = 16;
static const uint8_t A16 = 17;
static const uint8_t A17 = 18;
static const uint8_t A18 = 19;
static const uint8_t A19 = 20;

static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
static const uint8_t T7 = 7;
static const uint8_t T8 = 8;
static const uint8_t T9 = 9;
static const uint8_t T10 = 10;
static const uint8_t T11 = 11;
static const uint8_t T12 = 12;
static const uint8_t T13 = 13;
static const uint8_t T14 = 14;

static const uint8_t Vext = 36;

#endif /* Pins_Arduino_h */
134 changes: 134 additions & 0 deletions variants/esp32s3/heltec_v4_r2/platformio.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
; Heltec WiFi LoRa 32 V4-R2 — ESP32-S3 + SX1262
; External 2.8" ILI9341V SPI display (240x320) + FT6336G capacitive touch (I2C)
; No on-board LCD screen
;
; Display SPI wiring (SPI3_HOST):
; GPIO47 → SCK GPIO33 → MOSI GPIO4 → MISO
; GPIO34 → LCD_CS GPIO21 → LCD_RS GPIO48 → LCD_RST GPIO3 → LED/BL
;
; Touch I2C wiring (Wire, port 0):
; GPIO17 → CTP_SDA GPIO18 → CTP_SCL
; GPIO45 → CTP_RST GPIO15 → CTP_INT (polling)
;
; GPIO6 → SD_CS (SD card shares SPI bus with display)
;
; NOTE: this base intentionally extends esp32s3_base (not heltec_v4_base) so
; that only variants/esp32s3/heltec_v4_r2/variant.h is on the include path.
; Inheriting heltec_v4_base would prepend -I variants/esp32s3/heltec_v4 first,
; causing the V4's variant.h (which contains #if HAS_TFT → USE_TFTDISPLAY) to
; shadow the R2 header and incorrectly compile TFTDisplay.cpp.

[heltec_v4_r2_base]
extends = esp32s3_base
board = heltec_v4
board_check = true
board_build.partitions = default_16MB.csv
build_flags =
${esp32s3_base.build_flags}
-D HELTEC_V4
-D HELTEC_V4_R2
-D HAS_LORA_FEM=1
-I variants/esp32s3/heltec_v4_r2

[env:heltec-v4-r2]
custom_meshtastic_hw_model = 110
custom_meshtastic_hw_model_slug = HELTEC_V4
custom_meshtastic_architecture = esp32-s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 2
custom_meshtastic_display_name = Heltec V4 R2 ILI9341 Touch
custom_meshtastic_tags = Heltec
custom_meshtastic_requires_dfu = true
custom_meshtastic_partition_scheme = 16MB
custom_meshtastic_has_mui = true

extends = heltec_v4_r2_base
build_flags =
${heltec_v4_r2_base.build_flags}
; ---- System ----
-D CONFIG_ARDUHAL_LOG_COLORS
-D RADIOLIB_DEBUG_SPI=0
-D RADIOLIB_DEBUG_PROTOCOL=0
-D RADIOLIB_DEBUG_BASIC=0
-D RADIOLIB_VERBOSE_ASSERT=0
-D RADIOLIB_SPI_PARANOID=0
-D CONFIG_DISABLE_HAL_LOCKS=1
-D INPUTDRIVER_BUTTON_TYPE=0
-D USE_LOG_DEBUG
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
-D USE_PACKET_API
; ---- Display ----
-D HAS_SCREEN=0
-D HAS_TFT=1
; ---- LVGL / MUI ----
-D LV_LVGL_H_INCLUDE_SIMPLE
-D LV_CONF_INCLUDE_SIMPLE
-D LV_COMP_CONF_INCLUDE_SIMPLE
-D LV_USE_SYSMON=0
-D LV_USE_PROFILER=0
-D LV_USE_PERF_MONITOR=0
-D LV_USE_MEM_MONITOR=0
-D LV_USE_LOG=0
-D LV_BUILD_TEST=0
; ---- RAM / tile budget (2 MB PSRAM) ----
-D MAP_TILES_GREY
-D RAM_SIZE=1432
-D STBI_ARENA_SIZE=450000
-D LV_CACHE_DEF_SIZE=0
; ---- LGFX Generic driver (device-ui) ----
-D LGFX_DRIVER_TEMPLATE
-D LGFX_DRIVER=LGFX_GENERIC
-D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\"
-D LGFX_CFG_HOST=SPI3_HOST
-D LGFX_PIN_SCK=47
-D LGFX_PIN_MOSI=33
-D LGFX_PIN_MISO=4
-D LGFX_PIN_DC=21
-D LGFX_PIN_CS=45
-D LGFX_PIN_RST=48
-D LGFX_PIN_BL=3
-D LGFX_PANEL=ILI9341
-D LGFX_INVERT_COLOR=false
-D LGFX_SCREEN_WIDTH=240
-D LGFX_SCREEN_HEIGHT=320
-D VIEW_320x240
-D DISPLAY_SET_RESOLUTION
-D DISPLAY_SIZE=320x240
-D LGFX_ROTATION=0
-D SPI_FREQUENCY=40000000
-D SPI_READ_FREQUENCY=16000000
-D BRIGHTNESS_DEFAULT=130
; ---- FT6336G capacitive touch (FT5x06-compatible, I2C address 0x38) ----
-D HAS_TOUCHSCREEN=1
-D LGFX_TOUCH=FT5x06
-D LGFX_TOUCH_I2C_FREQ=400000
-D LGFX_TOUCH_I2C_PORT=0
-D LGFX_TOUCH_I2C_ADDR=0x38
-D LGFX_TOUCH_I2C_SDA=17
-D LGFX_TOUCH_I2C_SCL=18
-D LGFX_TOUCH_RST=16
-D LGFX_TOUCH_INT=15
-D LGFX_TOUCH_X_MIN=0
-D LGFX_TOUCH_X_MAX=239
-D LGFX_TOUCH_Y_MIN=0
-D LGFX_TOUCH_Y_MAX=319
-D LGFX_TOUCH_ROTATION=0
; ---- SD card ----
; SD card support is disabled: GPIO5 shares the SPI3_HOST bus with the ILI9341
; display (LovyanGFX uses spi_bus_initialize() directly). Enabling SDCARD_USE_SPI1
; causes setupSDCard() to call SPI_HSPI.begin() before LovyanGFX initialises the
; bus, producing addApbChangeCallback duplicate errors that corrupt the display SPI
; and prevent the boot screen from advancing to the main UI.
; -D HAS_SDCARD
; -D SDCARD_USE_SPI1
; -D SDCARD_USER_SPI_BEGIN
; -D SPI_CS=5
; -D SDCARD_CS=SPI_CS
; -D SD_SPI_FREQUENCY=SPI_FREQUENCY


lib_deps =
${esp32s3_base.lib_deps}
${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.26
86 changes: 86 additions & 0 deletions variants/esp32s3/heltec_v4_r2/variant.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Heltec WiFi LoRa 32 V4-R2 — ESP32-S3 + SX1262
// External 2.8" ILI9341V SPI display (240x320) + FT6336G I2C capacitive touch
// No on-board LCD screen

#define VEXT_ENABLE 36 // active low, powers the LoRa antenna boost
#define VEXT_ON_VALUE LOW
#define BUTTON_PIN 0

#define ADC_CTRL 37
#define ADC_CTRL_ENABLED HIGH
#define BATTERY_PIN 1 // ADC1_CH0 — battery voltage via divider; pull ADC_CTRL high to read
#define ADC_CHANNEL ADC_CHANNEL_0
#define ADC_ATTENUATION ADC_ATTEN_DB_2_5
#define ADC_MULTIPLIER 4.9 * 1.045

#define USE_SX1262

#define LORA_DIO0 -1 // not connected on SX1262
#define LORA_RESET 12 // SX1262 RESET
#define LORA_DIO1 14 // SX1262 IRQ
#define LORA_DIO2 13 // SX1262 BUSY
#define LORA_DIO3 // not connected on PCB

#define LORA_SCK 9
#define LORA_MISO 11
#define LORA_MOSI 10
#define LORA_CS 8

#define SX126X_CS LORA_CS
#define SX126X_DIO1 LORA_DIO1
#define SX126X_BUSY LORA_DIO2
#define SX126X_RESET LORA_RESET

#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8

// Enable Traffic Management Module
#ifndef HAS_TRAFFIC_MANAGEMENT
#define HAS_TRAFFIC_MANAGEMENT 1
#endif

// ---- GC1109 RF FRONT END CONFIGURATION (V4.2) ----
// CTX -> SX1262 DIO2 (automatic via SX126X_DIO2_AS_RF_SWITCH)
// CSD -> GPIO2, CPS -> GPIO46, VCC -> GPIO7
#define LORA_PA_POWER 7 // VFEM_Ctrl
#define LORA_GC1109_PA_EN 2 // CSD — chip enable (HIGH=on)
#define LORA_GC1109_PA_TX_EN 46 // CPS — PA mode (HIGH=full PA)

// ---- KCT8103L RF FRONT END CONFIGURATION (V4.3) ----
// CPS -> SX1262 DIO2 (automatic via SX126X_DIO2_AS_RF_SWITCH)
// CSD -> GPIO2, CTX -> GPIO5, VCC -> GPIO7
#define LORA_KCT8103L_PA_CSD 2 // CSD — chip enable (HIGH=on)
#define LORA_KCT8103L_PA_CTX 5 // CTX — RX bypass (HIGH) / RX LNA (LOW)

/*
* GPS pins — L76K GNSS module
*/
#define GPS_L76K
#define PIN_GPS_RESET (42) // GNSS_RESET
#define GPS_RESET_MODE LOW
#define PIN_GPS_EN (34) // **LISTED AS SPIIO5,FSPICS0, SUBSPICS0 IN DOCUMENTATION**
#define GPS_EN_ACTIVE LOW
#define PERIPHERAL_WARMUP_MS 1000
#define PIN_GPS_STANDBY (40) // GNSS_WAKEUP
#define PIN_GPS_PPS (41) // GNSS_PPS
#define GPS_TX_PIN (38) // GNSS → CPU (GNSS_RX)
#define GPS_RX_PIN (39) // CPU → GNSS (GNSS_TX)
#define GPS_THREAD_INTERVAL 50

/*
* External display connections (set via LGFX_PIN_* in platformio.ini)
*
* Display SPI (SPI3_HOST):
* SCK → GPIO47 LCD_RS/DC → GPIO21
* MOSI → GPIO33 CS → GPIO45
* MISO → GPIO04 RST → GPIO48 [GPIO26 is PSRAM CS — do not use]
* BL → GPIO3
*
* Touch I2C (Wire, port 0 — GPIO3/4):
* SDA → GPIO4 RST → GPIO45
* SCL → GPIO18 INT → GPIO15
*/

// I2C bus used for touch and peripheral scan
#define I2C_SDA 17
#define I2C_SCL 18