diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml
index e9bbba9ff..4e9de6a02 100644
--- a/.trunk/trunk.yaml
+++ b/.trunk/trunk.yaml
@@ -9,7 +9,7 @@ plugins:
lint:
enabled:
- checkov@3.2.471
- - renovate@41.127.2
+ - renovate@41.130.1
- prettier@3.6.2
- trufflehog@3.90.8
- yamllint@1.37.1
diff --git a/protobufs b/protobufs
index 46b81e822..082bb7cfe 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit 46b81e822af1b8e408f437092337f129dee693e6
+Subproject commit 082bb7cfeb2cba9d41be139cd324c4b43a14b3f9
diff --git a/src/configuration.h b/src/configuration.h
index 1b386ec17..91181890b 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -117,6 +117,12 @@ along with this program. If not, see .
#define SX126X_MAX_POWER 22
#endif
+#ifdef HELTEC_V4
+// Power Amps are often non-linear, so we can use an array of values for the power curve
+#define NUM_PA_POINTS 22
+#define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
+#endif
+
// Default system gain to 0 if not defined
#ifndef TX_GAIN_LORA
#define TX_GAIN_LORA 0
diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp
index 3937bcf50..dcaa5d69b 100644
--- a/src/graphics/SharedUIDisplay.cpp
+++ b/src/graphics/SharedUIDisplay.cpp
@@ -284,7 +284,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
- if (isInverted) {
+ if (isInverted && !force_no_invert) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
display->setColor(BLACK);
diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp
index 7c4f4e05f..43b3fb8ab 100644
--- a/src/graphics/draw/MenuHandler.cpp
+++ b/src/graphics/draw/MenuHandler.cpp
@@ -852,24 +852,31 @@ void menuHandler::GPSFormatMenu()
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC;
+ saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 2) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS;
+ saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 3) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM;
+ saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 4) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS;
+ saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 5) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC;
+ saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 6) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR;
+ saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 7) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS;
+ saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuQueue = position_base_menu;
diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp
index 2ccc0f861..ff8cd20c5 100644
--- a/src/graphics/draw/UIRenderer.cpp
+++ b/src/graphics/draw/UIRenderer.cpp
@@ -1105,6 +1105,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// === Fourth Row: Line 2 GPS Info ===
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
}
+
+ // === Final Row: Altitude ===
+ char altitudeLine[32] = {0};
+ int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
+ ? ourNode->position.altitude
+ : geoCoord.getAltitude();
+ if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
+ snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET);
+ } else {
+ snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
+ }
+ display->drawString(x, getTextPositions(display)[line++], altitudeLine);
}
#if !defined(M5STACK_UNITC6L)
// === Draw Compass if heading is valid ===
diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp
index 3caee78f8..7ce106317 100644
--- a/src/mesh/FloodingRouter.cpp
+++ b/src/mesh/FloodingRouter.cpp
@@ -54,6 +54,10 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
// We already enqueued the improved copy, so make sure the incoming packet stops here.
return true;
}
+
+ // No queue entry was replaced by this upgraded copy, so treat it as a duplicate to avoid
+ // delivering the same packet to applications/phone twice with different hop limits.
+ seenRecently = true;
}
if (seenRecently) {
diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp
index 4ae0a818e..dce436471 100644
--- a/src/mesh/NextHopRouter.cpp
+++ b/src/mesh/NextHopRouter.cpp
@@ -64,6 +64,10 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
// We already enqueued the improved copy, so make sure the incoming packet stops here.
return true;
}
+
+ // No queue entry was replaced by this upgraded copy, so treat it as a duplicate to avoid
+ // delivering the same packet to applications/phone twice with different hop limits.
+ seenRecently = true;
}
if (seenRecently) {
diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp
index 85cf482e7..d6a6acba2 100644
--- a/src/mesh/NodeDB.cpp
+++ b/src/mesh/NodeDB.cpp
@@ -834,7 +834,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
#ifdef USERPREFS_NETWORK_ENABLED_PROTOCOLS
config.network.enabled_protocols = USERPREFS_NETWORK_ENABLED_PROTOCOLS;
#else
- config.network.enabled_protocols = 1;
+ config.network.enabled_protocols = 0;
#endif
#endif
@@ -1919,9 +1919,6 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
return false;
}
LOG_INFO("Public Key set for node, not updating!");
- // we copy the key into the incoming packet, to prevent overwrite
- p.public_key.size = 32;
- memcpy(p.public_key.bytes, info->user.public_key.bytes, 32);
} else if (p.public_key.size == 32) {
LOG_INFO("Update Node Pubkey!");
}
diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp
index 31c68c302..b891ec89c 100644
--- a/src/mesh/RadioInterface.cpp
+++ b/src/mesh/RadioInterface.cpp
@@ -674,11 +674,25 @@ void RadioInterface::limitPower(int8_t loraMaxPower)
power = maxPower;
}
+#ifndef NUM_PA_POINTS
if (TX_GAIN_LORA > 0) {
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, TX_GAIN_LORA);
power -= TX_GAIN_LORA;
}
+#else
+ // we have an array of PA gain values. Find the highest power setting that works.
+ const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA};
+ for (int radio_dbm = 0; radio_dbm < NUM_PA_POINTS; radio_dbm++) {
+ if (((radio_dbm + tx_gain[radio_dbm]) > power) ||
+ ((radio_dbm == (NUM_PA_POINTS - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) {
+ // we've exceeded the power limit, or hit the max we can do
+ LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]);
+ power -= tx_gain[radio_dbm];
+ break;
+ }
+ }
+#endif
if (power > loraMaxPower) // Clamp power to maximum defined level
power = loraMaxPower;
diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index 3b959ff83..0d3a96b58 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -108,7 +108,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
// For subsequent hops, check if previous relay is a favorite router
// Optimized search for favorite routers with matching last byte
// Check ordering optimized for IoT devices (cheapest checks first)
- for (int i = 0; i < nodeDB->getNumMeshNodes(); i++) {
+ for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
if (!node)
continue;
@@ -123,7 +123,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
// Check 3: role check (moderate cost - multiple comparisons)
if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER,
- meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
+ meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
continue;
}
@@ -501,35 +501,6 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
}
}
-#if HAS_UDP_MULTICAST
- // Fallback: for UDP multicast, try default preset names with default PSK if normal channel match failed
- if (!decrypted && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) {
- if (channels.setDefaultPresetCryptoForHash(p->channel)) {
- memcpy(bytes, p->encrypted.bytes, rawSize);
- crypto->decrypt(p->from, p->id, rawSize, bytes);
-
- meshtastic_Data decodedtmp;
- memset(&decodedtmp, 0, sizeof(decodedtmp));
- if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
- decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
- p->decoded = decodedtmp;
- p->which_payload_variant = meshtastic_MeshPacket_decoded_tag;
- // Map to our local default channel index (name+PSK default), not necessarily primary
- ChannelIndex defaultIndex = channels.getPrimaryIndex();
- for (ChannelIndex i = 0; i < channels.getNumChannels(); ++i) {
- if (channels.isDefaultChannel(i)) {
- defaultIndex = i;
- break;
- }
- }
- chIndex = defaultIndex;
- decrypted = true;
- } else {
- LOG_WARN("UDP fallback decode attempted but failed for hash 0x%x", p->channel);
- }
- }
- }
-#endif
if (decrypted) {
// parsing was successful
p->channel = chIndex; // change to store the index instead of the hash
diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp
index 3fc2562b3..785338483 100644
--- a/src/mesh/SX126xInterface.cpp
+++ b/src/mesh/SX126xInterface.cpp
@@ -80,6 +80,9 @@ template bool SX126xInterface::init()
RadioLibInterface::init();
limitPower(SX126X_MAX_POWER);
+ // Make sure we reach the minimum power supported to turn the chip on (-9dBm)
+ if (power < -9)
+ power = -9;
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO);
// \todo Display actual typename of the adapter, not just `SX126x`
@@ -118,8 +121,8 @@ template bool SX126xInterface::init()
LOG_DEBUG("Set DIO2 as %sRF switch, result: %d", dio2AsRfSwitch ? "" : "not ", res);
}
- // If a pin isn't defined, we set it to RADIOLIB_NC, it is safe to always do external RF switching with RADIOLIB_NC as it has
- // no effect
+// If a pin isn't defined, we set it to RADIOLIB_NC, it is safe to always do external RF switching with RADIOLIB_NC as it has
+// no effect
#if ARCH_PORTDUINO
if (res == RADIOLIB_ERR_NONE) {
LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin,
diff --git a/src/mesh/generated/meshtastic/apponly.pb.h b/src/mesh/generated/meshtastic/apponly.pb.h
index f4c33bd79..db9dedaaf 100644
--- a/src/mesh/generated/meshtastic/apponly.pb.h
+++ b/src/mesh/generated/meshtastic/apponly.pb.h
@@ -55,7 +55,7 @@ extern const pb_msgdesc_t meshtastic_ChannelSet_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size
-#define meshtastic_ChannelSet_size 679
+#define meshtastic_ChannelSet_size 695
#ifdef __cplusplus
} /* extern "C" */
diff --git a/src/mesh/generated/meshtastic/channel.pb.h b/src/mesh/generated/meshtastic/channel.pb.h
index ca4310bf1..d5573a1e2 100644
--- a/src/mesh/generated/meshtastic/channel.pb.h
+++ b/src/mesh/generated/meshtastic/channel.pb.h
@@ -97,6 +97,8 @@ typedef struct _meshtastic_ChannelSettings {
/* Per-channel module settings. */
bool has_module_settings;
meshtastic_ModuleSettings module_settings;
+ /* Whether or not we should receive notifactions / alerts through this channel */
+ bool mute;
} meshtastic_ChannelSettings;
/* A pair of a channel number, mode and the (sharable) settings for that channel */
@@ -128,10 +130,10 @@ extern "C" {
/* Initializer values for message structs */
-#define meshtastic_ChannelSettings_init_default {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_default}
+#define meshtastic_ChannelSettings_init_default {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_default, 0}
#define meshtastic_ModuleSettings_init_default {0, 0}
#define meshtastic_Channel_init_default {0, false, meshtastic_ChannelSettings_init_default, _meshtastic_Channel_Role_MIN}
-#define meshtastic_ChannelSettings_init_zero {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_zero}
+#define meshtastic_ChannelSettings_init_zero {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_zero, 0}
#define meshtastic_ModuleSettings_init_zero {0, 0}
#define meshtastic_Channel_init_zero {0, false, meshtastic_ChannelSettings_init_zero, _meshtastic_Channel_Role_MIN}
@@ -145,6 +147,7 @@ extern "C" {
#define meshtastic_ChannelSettings_uplink_enabled_tag 5
#define meshtastic_ChannelSettings_downlink_enabled_tag 6
#define meshtastic_ChannelSettings_module_settings_tag 7
+#define meshtastic_ChannelSettings_mute_tag 8
#define meshtastic_Channel_index_tag 1
#define meshtastic_Channel_settings_tag 2
#define meshtastic_Channel_role_tag 3
@@ -157,7 +160,8 @@ X(a, STATIC, SINGULAR, STRING, name, 3) \
X(a, STATIC, SINGULAR, FIXED32, id, 4) \
X(a, STATIC, SINGULAR, BOOL, uplink_enabled, 5) \
X(a, STATIC, SINGULAR, BOOL, downlink_enabled, 6) \
-X(a, STATIC, OPTIONAL, MESSAGE, module_settings, 7)
+X(a, STATIC, OPTIONAL, MESSAGE, module_settings, 7) \
+X(a, STATIC, SINGULAR, BOOL, mute, 8)
#define meshtastic_ChannelSettings_CALLBACK NULL
#define meshtastic_ChannelSettings_DEFAULT NULL
#define meshtastic_ChannelSettings_module_settings_MSGTYPE meshtastic_ModuleSettings
@@ -187,8 +191,8 @@ extern const pb_msgdesc_t meshtastic_Channel_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_CHANNEL_PB_H_MAX_SIZE meshtastic_Channel_size
-#define meshtastic_ChannelSettings_size 72
-#define meshtastic_Channel_size 87
+#define meshtastic_ChannelSettings_size 74
+#define meshtastic_Channel_size 89
#define meshtastic_ModuleSettings_size 8
#ifdef __cplusplus
diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h
index 0453ecad2..327568316 100644
--- a/src/mesh/generated/meshtastic/config.pb.h
+++ b/src/mesh/generated/meshtastic/config.pb.h
@@ -26,7 +26,8 @@ typedef enum _meshtastic_Config_DeviceConfig_Role {
meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT = 3,
/* Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
- or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate. */
+ or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
+ Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain. */
meshtastic_Config_DeviceConfig_Role_REPEATER = 4,
/* Description: Broadcasts GPS position packets as priority.
Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default.
diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h
index 7fab82ff7..b5b116137 100644
--- a/src/mesh/generated/meshtastic/deviceonly.pb.h
+++ b/src/mesh/generated/meshtastic/deviceonly.pb.h
@@ -360,8 +360,8 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
/* Maximum encoded size of messages (where known) */
/* meshtastic_NodeDatabase_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
-#define meshtastic_BackupPreferences_size 2277
-#define meshtastic_ChannelFile_size 718
+#define meshtastic_BackupPreferences_size 2293
+#define meshtastic_ChannelFile_size 734
#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196
#define meshtastic_PositionLite_size 28
diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h
index 106900c48..ec6209487 100644
--- a/src/platform/portduino/PortduinoGlue.h
+++ b/src/platform/portduino/PortduinoGlue.h
@@ -224,7 +224,7 @@ extern struct portduino_config_struct {
out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power;
out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch;
if (dio3_tcxo_voltage != 0)
- out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << dio3_tcxo_voltage;
+ out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000;
if (lora_usb_pid != 0x5512)
out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid;
if (lora_usb_vid != 0x1A86)
diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini
index 1a448bc99..d0a250ad3 100644
--- a/variants/esp32s3/heltec_v4/platformio.ini
+++ b/variants/esp32s3/heltec_v4/platformio.ini
@@ -8,4 +8,3 @@ build_flags =
-D HELTEC_V4
-I variants/esp32s3/heltec_v4
-D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely.
- -D SX126X_MAX_POWER=11