Harden observer preferences and portable controls

This commit is contained in:
mikecarper
2026-07-25 07:59:44 -07:00
parent c1caa5ad81
commit ae2af7ef4d
28 changed files with 1271 additions and 172 deletions
+18 -9
View File
@@ -1709,17 +1709,28 @@ apply_esp32_lora_ota_size_profile() {
# 0x10000..0x150000 app slot. The WebConfig portal is deliberately omitted.
# WiFi/MQTT observers retain their small WiFi updater, while radio-only roles
# avoid linking WiFi solely for that updater. Companions retain their target
# defaults because they are installed over USB.
# defaults because they are installed over USB. Keep ENV_INCLUDE_GPS for
# boards with onboard GPS; their target sensor managers require that support.
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DWEBCONFIG_DISABLED=1"
if is_mqtt_bridge_target "$env_name"; then
export MESHCORE_PORTABLE_NANO_LIBC=1
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DLIGHTWEIGHT_WIFI_OTA=1 -DPORTABLE_MQTT_OBSERVER=1 -DPORTABLE_ESP32_MINIMAL_CLI=1 -DPORTABLE_ESP32_NANO_LIBC=1"
append_platformio_build_unflags "-DWITH_SNMP=1 -DMQTT_DEBUG=1 -DMQTT_MEMORY_DEBUG=1 -DDISPLAY_CLASS=SSD1306Display -DENV_INCLUDE_GPS=1 -DENV_INCLUDE_AHTX0=1 -DENV_INCLUDE_BME280=1 -DENV_INCLUDE_BMP280=1 -DENV_INCLUDE_SHTC3=1 -DENV_INCLUDE_SHT4X=1 -DENV_INCLUDE_LPS22HB=1 -DENV_INCLUDE_INA3221=1 -DENV_INCLUDE_INA219=1 -DENV_INCLUDE_INA226=1 -DENV_INCLUDE_INA260=1 -DENV_INCLUDE_MLX90614=1 -DENV_INCLUDE_VL53L0X=1 -DENV_INCLUDE_BME680=1 -DENV_INCLUDE_BMP085=1 -DENV_INCLUDE_RAK12035=1 -DENV_INCLUDE_BME680_BSEC=1"
# Keep the standard ESP-IDF libc. Use its ABI with the chip-ROM formatter,
# and retain a compact CLI for radio and active-bridge settings.
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DLIGHTWEIGHT_WIFI_OTA=1 -DPORTABLE_MQTT_OBSERVER=1 -DPORTABLE_ESP32_RADIO_CLI=1 -UDISPLAY_CLASS"
if [ "$FIRMWARE_FILENAME_INFIX" != "logging" ] \
&& [ "${MESHDEBUG_OVERRIDE,,}" != "on" ] \
&& [ "${PACKET_LOGGING_OVERRIDE,,}" != "on" ]; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DPORTABLE_ESP32_ROM_NANO_FORMAT=1"
fi
append_platformio_build_unflags "-DWITH_SNMP=1 -DMQTT_DEBUG=1 -DMQTT_MEMORY_DEBUG=1 -DDISPLAY_CLASS=SSD1306Display -DENV_INCLUDE_AHTX0=1 -DENV_INCLUDE_BME280=1 -DENV_INCLUDE_BMP280=1 -DENV_INCLUDE_SHTC3=1 -DENV_INCLUDE_SHT4X=1 -DENV_INCLUDE_LPS22HB=1 -DENV_INCLUDE_INA3221=1 -DENV_INCLUDE_INA219=1 -DENV_INCLUDE_INA226=1 -DENV_INCLUDE_INA260=1 -DENV_INCLUDE_MLX90614=1 -DENV_INCLUDE_VL53L0X=1 -DENV_INCLUDE_BME680=1 -DENV_INCLUDE_BMP085=1 -DENV_INCLUDE_RAK12035=1 -DENV_INCLUDE_BME680_BSEC=1"
elif [[ "${env_name,,}" == *bridge_espnow* ]]; then
export MESHCORE_PORTABLE_NANO_LIBC=1
append_platformio_build_unflags "-DLIGHTWEIGHT_WIFI_OTA=1"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -ULIGHTWEIGHT_WIFI_OTA -DDISABLE_WIFI_OTA=1 -DPORTABLE_ESP32_MINIMAL_CLI=1 -DPORTABLE_ESP32_NANO_LIBC=1"
append_platformio_build_unflags "-DDISPLAY_CLASS=SSD1306Display -DENV_INCLUDE_GPS=1 -DENV_INCLUDE_AHTX0=1 -DENV_INCLUDE_BME280=1 -DENV_INCLUDE_BMP280=1 -DENV_INCLUDE_SHTC3=1 -DENV_INCLUDE_SHT4X=1 -DENV_INCLUDE_LPS22HB=1 -DENV_INCLUDE_INA3221=1 -DENV_INCLUDE_INA219=1 -DENV_INCLUDE_INA226=1 -DENV_INCLUDE_INA260=1 -DENV_INCLUDE_MLX90614=1 -DENV_INCLUDE_VL53L0X=1 -DENV_INCLUDE_BME680=1 -DENV_INCLUDE_BMP085=1 -DENV_INCLUDE_RAK12035=1 -DENV_INCLUDE_BME680_BSEC=1"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -ULIGHTWEIGHT_WIFI_OTA -DDISABLE_WIFI_OTA=1 -DPORTABLE_ESP32_RADIO_CLI=1 -UDISPLAY_CLASS"
if [ "$FIRMWARE_FILENAME_INFIX" != "logging" ] \
&& [ "${MESHDEBUG_OVERRIDE,,}" != "on" ] \
&& [ "${PACKET_LOGGING_OVERRIDE,,}" != "on" ]; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DPORTABLE_ESP32_ROM_NANO_FORMAT=1"
fi
append_platformio_build_unflags "-DDISPLAY_CLASS=SSD1306Display -DENV_INCLUDE_AHTX0=1 -DENV_INCLUDE_BME280=1 -DENV_INCLUDE_BMP280=1 -DENV_INCLUDE_SHTC3=1 -DENV_INCLUDE_SHT4X=1 -DENV_INCLUDE_LPS22HB=1 -DENV_INCLUDE_INA3221=1 -DENV_INCLUDE_INA219=1 -DENV_INCLUDE_INA226=1 -DENV_INCLUDE_INA260=1 -DENV_INCLUDE_MLX90614=1 -DENV_INCLUDE_VL53L0X=1 -DENV_INCLUDE_BME680=1 -DENV_INCLUDE_BMP085=1 -DENV_INCLUDE_RAK12035=1 -DENV_INCLUDE_BME680_BSEC=1"
else
append_platformio_build_unflags "-DLIGHTWEIGHT_WIFI_OTA=1"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -ULIGHTWEIGHT_WIFI_OTA -DDISABLE_WIFI_OTA=1"
@@ -2121,7 +2132,6 @@ build_firmware() {
fi
export PLATFORMIO_BUILD_FLAGS="${original_platformio_build_flags} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_BUILD_EPOCH=${firmware_build_epoch} -DFIRMWARE_VERSION='\"${embedded_version_string}\"' -DOTA_VARIANT='\"${env_name}\"'${mota_target_flag}"
unset MESHCORE_PORTABLE_NANO_LIBC
disable_debug_flags
apply_debug_overrides
apply_mqtt_bridge_override
@@ -2156,7 +2166,6 @@ build_firmware() {
fi
restore_platformio_build_flags "$had_platformio_build_flags" "$original_platformio_build_flags"
unset MESHCORE_PORTABLE_NANO_LIBC
unset MESHCORE_ESP32_FULL_BUILD
if [ "$had_platformio_build_unflags" -eq 1 ]; then
export PLATFORMIO_BUILD_UNFLAGS="$original_platformio_build_unflags"
+9 -4
View File
@@ -240,6 +240,7 @@ set flood.filter response 9+
set flood.filter 0x06 3+
set flood.filter 0x07 9+
set flood.filter path 9+
set flood.filter control 1+
get flood.filter
```
@@ -250,13 +251,17 @@ get flood.filter
| `0x06 3+` (`grp_data`) | 3 or more path entries |
| `0x07 9+` (`anon_req`) | 9 or more path entries |
| `path 9+` | 9 or more path entries |
| `control 1+` | 1 or more path entries |
On a new table, the factory OTA rule occupies slot 1, so these unnumbered
commands normally fill slots 2 through 6. Existing tables may choose different
commands normally fill slots 2 through 7. Existing tables may choose different
free slots. The `response`, `anon_req`, and `path` thresholds are above their
protected `0-6` range, so all five rules take effect at the thresholds shown.
These rules affect only retransmission by the repeater; local reception and
logging remain unchanged.
protected `0-6` range, so all six rules take effect at the thresholds shown.
The Control rule allows a flood received with path count `0` to be forwarded
once, then stops it at the next repeater. Normal node-discovery Control packets
are direct zero-hop packets and never enter `flood.filter`. These rules affect
only retransmission by the repeater; local reception and logging remain
unchanged.
Accepted payload names are:
+16 -8
View File
@@ -122,22 +122,30 @@ GPS and other board-native telemetry remain enabled. RP2040 and STM32 targets ar
platforms do not yet have a safe bootloader/apply path.
Two WiFi-heavy non-companion profiles need additional reductions to remain portable. MQTT observer builds
keep MQTT/TLS and their WiFi pull-updater, but omit WebConfig, SNMP, debug logging, SSD1306 display support,
GPS, and optional external sensor drivers. Their compact CLI keeps observer controls and uses UTC or fixed UTC/GMT
keep MQTT/TLS, onboard GPS, and their WiFi pull-updater, but omit WebConfig, SNMP, debug logging, display
support, and optional external sensor drivers. Their compact CLI keeps observer controls plus the radio,
TX power, CAD, interference-threshold, AGC, repeat, and retained bridge controls. It uses UTC or fixed UTC/GMT
offsets instead of the full named-timezone table. Built-in TLS presets keep their pinned CA roots; the 66 KB
general CA bundle for custom TLS brokers is omitted, so portable observers use a built-in preset or a custom
non-TLS broker. Portable observers also use compact C-library formatting and generic ESP-IDF/mbedTLS error
text; error codes and MQTT status remain available. Classic T-Beam observers retain AXP192/AXP2101 radio
and GPS rail setup plus battery-voltage readings, but omit unrelated PMU policy. ESP-NOW bridge builds keep
the ESP-NOW bridge but likewise omit SSD1306 display support, GPS, optional external sensors, and the general
configuration parser. These reductions do
non-TLS broker. Size-constrained classic ESP32 observers without PSRAM may use Espressif's compact printf
implementation from chip ROM while retaining the normal ESP-IDF C library and ABI. Generic ESP-IDF/mbedTLS
error text keeps error codes and MQTT status available. Classic T-Beam observers retain AXP192/AXP2101 radio
and GPS rail setup plus battery-voltage
readings, but omit unrelated PMU policy. ESP-NOW bridge builds keep the ESP-NOW bridge, onboard GPS, and the
same radio-capable compact CLI, but omit display support and optional external sensors. These reductions do
not apply to companion builds. Ordinary repeater builds remain sensor-enabled; only explicitly named
`*_lora_ota_no_external_sensors` siblings omit sensors for LoRa distribution.
MQTT observer radio and bridge preferences use verified temporary files plus a recoverable backup. A reset
during a settings save restores the last committed common preference image or publishes the completed new
image; it does not leave a partially written `/com_prefs` file to fail on the next boot. A truncated legacy
image is rejected before any partial radio or string fields are applied, then rewritten from safe defaults.
Option 3 in `build.sh` also emits `*-full-ota-*` ESP32 artifacts for non-companion roles where the portable
profile removes a compiled feature and for the constrained companion fallbacks described above. Menu
option 8, or `build-full-esp32-firmwares`, builds only those FULL
artifacts. FULL builds restore WebConfig, the full CLI and observer feature set,
artifacts. FULL builds restore WebConfig, display support, optional external sensors, the full CLI and
observer feature set,
full ElegantOTA where that target declares the required library, and LoRa OTA for every included role,
including room servers, sensors, observers, and bridges. They use expanded A/B partition
tables: 1984 KiB application slots on 4 MiB boards and the framework's larger dual-OTA tables on 8 MiB
+4 -2
View File
@@ -6590,10 +6590,12 @@ void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
} else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
strncpy(ip, WiFi.softAPIP().toString().c_str(), sizeof(ip) - 1);
}
char snr_text[16];
StrHelper::ftoaFixed(snr_text, sizeof(snr_text), radio_driver.getLastSNR(), 1);
int pos = snprintf(buf, buf_size,
"{\"uptime_s\":%lu,\"batt_mv\":%u,"
"\"heap_free\":%lu,\"heap_min\":%lu,\"heap_max_alloc\":%lu,"
"\"noise\":%d,\"rssi\":%d,\"snr\":%.1f,"
"\"noise\":%d,\"rssi\":%d,\"snr\":%s,"
"\"airtime_s\":%lu,\"rx_airtime_s\":%lu,"
"\"recv\":%lu,\"sent\":%lu,\"rx_err\":%lu,"
"\"sent_flood\":%lu,\"sent_direct\":%lu,\"recv_flood\":%lu,\"recv_direct\":%lu,"
@@ -6602,7 +6604,7 @@ void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
(unsigned long)ESP.getFreeHeap(), (unsigned long)ESP.getMinFreeHeap(),
(unsigned long)ESP.getMaxAllocHeap(),
(int)_radio->getNoiseFloor(), (int)radio_driver.getLastRSSI(),
radio_driver.getLastSNR(),
snr_text,
(unsigned long)(getTotalAirTime() / 1000), (unsigned long)(getReceiveAirTime() / 1000),
(unsigned long)radio_driver.getPacketsRecv(), (unsigned long)radio_driver.getPacketsSent(),
(unsigned long)radio_driver.getPacketsRecvErrors(),
+4 -2
View File
@@ -1415,10 +1415,12 @@ void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
} else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
strncpy(ip, WiFi.softAPIP().toString().c_str(), sizeof(ip) - 1);
}
char snr_text[16];
StrHelper::ftoaFixed(snr_text, sizeof(snr_text), radio_driver.getLastSNR(), 1);
int pos = snprintf(buf, buf_size,
"{\"uptime_s\":%lu,\"batt_mv\":%u,"
"\"heap_free\":%lu,\"heap_min\":%lu,\"heap_max_alloc\":%lu,"
"\"noise\":%d,\"rssi\":%d,\"snr\":%.1f,"
"\"noise\":%d,\"rssi\":%d,\"snr\":%s,"
"\"airtime_s\":%lu,\"rx_airtime_s\":%lu,"
"\"recv\":%lu,\"sent\":%lu,\"rx_err\":%lu,"
"\"sent_flood\":%lu,\"sent_direct\":%lu,\"recv_flood\":%lu,\"recv_direct\":%lu,"
@@ -1427,7 +1429,7 @@ void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
(unsigned long)ESP.getFreeHeap(), (unsigned long)ESP.getMinFreeHeap(),
(unsigned long)ESP.getMaxAllocHeap(),
(int)_radio->getNoiseFloor(), (int)radio_driver.getLastRSSI(),
radio_driver.getLastSNR(),
snr_text,
(unsigned long)(getTotalAirTime() / 1000), (unsigned long)(getReceiveAirTime() / 1000),
(unsigned long)radio_driver.getPacketsRecv(), (unsigned long)radio_driver.getPacketsSent(),
(unsigned long)radio_driver.getPacketsRecvErrors(),
+1
View File
@@ -261,6 +261,7 @@ build_src_filter =
+<../src/Dispatcher.cpp>
+<../src/Identity.cpp>
+<../src/Mesh.cpp>
+<../src/helpers/TxtDataHelpers.cpp>
+<../src/helpers/StaticPoolPacketManager.cpp>
+<../src/helpers/ota/MerkleTree.cpp>
+<../src/helpers/ota/MotaContainer.cpp>
+8 -5
View File
@@ -179,18 +179,21 @@ class InputError(RuntimeError):
def main():
# Portable observer presets pin their required roots directly in
# MQTTPresets.h. Keep the embedded bundle structurally valid but empty so
# custom TLS trust stores do not consume 66 KB of the legacy app slot.
# Portable observer presets pin their MQTT roots directly in
# MQTTPresets.h. Keep only the GlobalSign root needed by the manifest and
# firmware hosts so HTTPS pull OTA remains verified without carrying the
# roughly 66 KB general-purpose bundle in the legacy app slot.
if (
"observer_mqtt" in env.subst("$PIOENV").lower()
and os.environ.get("MESHCORE_ESP32_FULL_BUILD") != "1"
):
bundle = CertificateBundle()
bundle.add_from_file(os.path.join(certs_dir, "globalsign_root_ca_r1.pem"))
os.makedirs(binary_dir, exist_ok=True)
output_file = os.path.join(binary_dir, ca_bundle_bin_file)
with open(output_file, 'wb') as f:
f.write(struct.pack('>H', 0))
status('Created empty portable-observer certificate bundle')
f.write(bundle.create_bundle())
status('Created portable-observer OTA certificate bundle')
return
bundle = CertificateBundle()
+22 -11
View File
@@ -14,15 +14,26 @@ def has_define(name):
return False
build_flags = str(env.get("BUILD_FLAGS", "")) + " " + os.environ.get("PLATFORMIO_BUILD_FLAGS", "")
pio_env = env.subst("$PIOENV").lower()
full_build = os.environ.get("MESHCORE_ESP32_FULL_BUILD") == "1"
build_flags = (
str(env.get("BUILD_FLAGS", ""))
+ " "
+ os.environ.get("PLATFORMIO_BUILD_FLAGS", "")
)
if (has_define("PORTABLE_ESP32_NANO_LIBC") or
"PORTABLE_ESP32_NANO_LIBC" in build_flags or
os.environ.get("MESHCORE_PORTABLE_NANO_LIBC") == "1" or
(not full_build and ("observer_mqtt" in pio_env or "bridge_espnow" in pio_env))):
# newlib-nano provides the same C ABI with a substantially smaller printf
# implementation. These profiles omit display, GPS, external sensors, and
# the general CLI paths that require floating-point printf.
env.Append(LINKFLAGS=["--specs=nano.specs"])
if (
has_define("PORTABLE_ESP32_ROM_NANO_FORMAT")
or "PORTABLE_ESP32_ROM_NANO_FORMAT" in build_flags
):
# ESP-IDF supplies linker tables for the compact printf implementation in
# chip ROM. This retains the framework's normal C library and ABI; it does
# not substitute libc_nano. The classic ESP32 table must not be used with
# that chip's PSRAM cache workaround. The ESP32-S3 has its own ROM table and
# no equivalent restriction in the framework linker script.
mcu = str(env.BoardConfig().get("build.mcu", "")).lower()
classic_esp32_psram = mcu == "esp32" and (
has_define("BOARD_HAS_PSRAM") or "BOARD_HAS_PSRAM" in build_flags
)
if mcu == "esp32" and not classic_esp32_psram:
env.Append(LINKFLAGS=["-T", "%s.rom.newlib-nano.ld" % mcu])
elif mcu == "esp32s3":
env.Append(LINKFLAGS=["-T", "%s.rom.newlib-nano.ld" % mcu])
+41
View File
@@ -1,5 +1,7 @@
#pragma once
#include <stdint.h>
namespace mesh {
namespace cli {
@@ -18,5 +20,44 @@ inline void normalizeCommandVerb(char* command) {
}
}
// Parse the decimal syntax used by CLI settings without pulling a general
// strtod implementation into small firmware images. Scientific notation,
// NaN, infinity, and trailing junk are intentionally rejected.
inline bool parseDecimalStrict(const char* text, float& result) {
if (text == nullptr) return false;
while (*text == ' ' || *text == '\t') text++;
bool negative = false;
if (*text == '-' || *text == '+') {
negative = *text == '-';
text++;
}
uint32_t whole = 0;
bool saw_digit = false;
while (*text >= '0' && *text <= '9') {
uint8_t digit = static_cast<uint8_t>(*text++ - '0');
if (whole > (0xFFFFFFFFUL - digit) / 10UL) return false;
whole = whole * 10UL + digit;
saw_digit = true;
}
float value = static_cast<float>(whole);
if (*text == '.') {
float place = 0.1f;
text++;
while (*text >= '0' && *text <= '9') {
value += static_cast<float>(*text++ - '0') * place;
place *= 0.1f;
saw_digit = true;
}
}
while (*text == ' ' || *text == '\t') text++;
if (!saw_digit || *text != 0) return false;
result = negative ? -value : value;
return true;
}
} // namespace cli
} // namespace mesh
+659 -59
View File
@@ -9,6 +9,7 @@
#endif
#include <RTClib.h>
#include <Utils.h>
#include <math.h>
#include <stddef.h>
#if defined(NRF52_PLATFORM)
@@ -52,6 +53,7 @@ static void resetToUf2Bootloader() {
#endif
#ifdef WITH_MQTT_BRIDGE
#include "bridges/MQTTBridge.h"
#include "CommonPrefsRecovery.h"
#include "MQTTDefaults.h"
#include "MQTTPrefsAtomicStore.h"
#include "MQTTPrefsCodec.h"
@@ -329,7 +331,8 @@ static bool parseScheduledRadioArgs(const char* args, bool temporary, float& fre
uint32_t sf_u32 = 0;
uint32_t cr_u32 = 0;
if (!looksNumeric(parts[0]) || !looksNumeric(parts[1])
if (!mesh::cli::parseDecimalStrict(parts[0], freq)
|| !mesh::cli::parseDecimalStrict(parts[1], bw)
|| !parseUint32Strict(parts[2], sf_u32)
|| !parseUint32Strict(parts[3], cr_u32)
|| !parseUint32Strict(parts[4], start_time)) {
@@ -339,8 +342,6 @@ static bool parseScheduledRadioArgs(const char* args, bool temporary, float& fre
return false;
}
freq = atof(parts[0]);
bw = atof(parts[1]);
sf = (uint8_t)sf_u32;
cr = (uint8_t)cr_u32;
if (temporary && !parseUint32Strict(parts[5], end_time)) {
@@ -353,7 +354,8 @@ static bool parseScheduledRadioArgs(const char* args, bool temporary, float& fre
}
static int16_t parseSnrDbX4(const char* s) {
float db = atof(s);
float db = 0.0f;
mesh::cli::parseDecimalStrict(s, db);
return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f));
}
@@ -844,6 +846,9 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
#ifdef WITH_MQTT_BRIDGE
bool node_prefs_needs_migration = false;
if (!recoverCommonPrefsFiles(fs)) {
MESH_DEBUG_PRINTLN("Prefs: common preference recovery is incomplete");
}
#endif
if (fs->exists("/com_prefs")) {
@@ -965,6 +970,16 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
File file = fs->open(filename);
#endif
if (file) {
// Every supported layout contains the fixed 290-byte common core. Reject
// a truncated in-place write before it can leave strings unterminated or
// feed partial radio values into startup. loadPrefs() rewrites the image.
if (file.size() < 290) {
MESH_DEBUG_PRINTLN("Prefs: %s is truncated (%u bytes); using defaults",
filename, (unsigned)file.size());
file.close();
_com_prefs_needs_upgrade = true;
return;
}
uint8_t pad[8];
file.read((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
@@ -1010,6 +1025,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
_prefs->node_name[sizeof(_prefs->node_name) - 1] = '\0';
_prefs->password[sizeof(_prefs->password) - 1] = '\0';
_prefs->guest_password[sizeof(_prefs->guest_password) - 1] = '\0';
_prefs->bridge_secret[sizeof(_prefs->bridge_secret) - 1] = '\0';
_prefs->owner_info[sizeof(_prefs->owner_info) - 1] = '\0';
// MQTT/observer settings are no longer stored in /com_prefs - they live in
// /mqtt_prefs (loaded by loadMQTTPrefs). Old fork firmware wrote a zero-filled
// MQTT gap here followed by a trailing observer block; detect that layout by the
@@ -1300,6 +1320,31 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
}
}
// Sanitize non-finite values before constrain(), whose comparisons leave
// NaN unchanged. Safe defaults keep a damaged image from reaching radio
// initialization or advert coordinate encoding.
if (!isfinite(_prefs->airtime_factor)) _prefs->airtime_factor = 1.0f;
#ifdef DEFAULT_RX_DELAY_BASE
if (!isfinite(_prefs->rx_delay_base)) _prefs->rx_delay_base = DEFAULT_RX_DELAY_BASE;
#else
if (!isfinite(_prefs->rx_delay_base)) _prefs->rx_delay_base = 0.0f;
#endif
if (!isfinite(_prefs->tx_delay_factor)) _prefs->tx_delay_factor = 0.5f;
if (!isfinite(_prefs->direct_tx_delay_factor)) _prefs->direct_tx_delay_factor = 0.3f;
#ifdef LORA_FREQ
if (!isfinite(_prefs->freq)) _prefs->freq = (float)LORA_FREQ;
#else
if (!isfinite(_prefs->freq)) _prefs->freq = 915.0f;
#endif
if (!isfinite(_prefs->bw)) _prefs->bw = defaultLoRaBandwidth();
if (!isfinite(_prefs->adc_multiplier)) _prefs->adc_multiplier = 0.0f;
if (!isfinite(_prefs->node_lat) || _prefs->node_lat < -90.0 || _prefs->node_lat > 90.0) {
_prefs->node_lat = 0.0;
}
if (!isfinite(_prefs->node_lon) || _prefs->node_lon < -180.0 || _prefs->node_lon > 180.0) {
_prefs->node_lon = 0.0;
}
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
_prefs->tx_delay_factor = constrain(_prefs->tx_delay_factor, 0, 2.0f);
@@ -1540,6 +1585,13 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
#endif
void CommonCLI::savePrefs(FILESYSTEM* fs, bool save_mqtt) {
#ifdef WITH_MQTT_BRIDGE
// Observer builds use a verified temp/backup transaction for common prefs.
// Radio and bridge changes must never leave a truncated boot-time image.
saveCommonPrefsImageAtomically(fs);
if (save_mqtt) saveMQTTPrefs(fs);
return;
#else
#if defined(NRF52_PLATFORM)
mesh::AtomicFileWriter file(fs, "/com_prefs");
#elif defined(STM32_PLATFORM)
@@ -1680,10 +1732,6 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, bool save_mqtt) {
file.close();
#endif
}
#ifdef WITH_MQTT_BRIDGE
// Observer config (MQTT/WiFi/timezone/SNMP/alert) is persisted separately. The
// observer CLI writes _mqtt_prefs directly, so no NodePrefs->MQTTPrefs sync runs.
if (save_mqtt) saveMQTTPrefs(fs);
#endif
}
@@ -1869,9 +1917,9 @@ private:
#endif // WITH_MQTT_BRIDGE
#ifdef WITH_MQTT_BRIDGE
// The old /node_prefs name is only removed after this transaction has published
// a complete /com_prefs image. At this migration point /com_prefs is absent, so
// rename never needs a platform-specific replace-existing implementation.
// Common preferences use the same verified temp/backup transaction shape as
// MQTT preferences. This protects both old-name migration and normal runtime
// radio/bridge changes from power loss between truncate, write, and close.
class CommonPrefsFileStore {
public:
explicit CommonPrefsFileStore(FILESYSTEM* fs)
@@ -1884,13 +1932,9 @@ public:
bool begin() {
_finished = false;
_open = false;
_owns_temp = false;
_bytes_written = 0;
// The old-name migration only starts when /com_prefs is absent. Refuse to
// overwrite a destination that appeared unexpectedly before this handoff.
if (_fs->exists("/com_prefs")) return false;
// Clear only stale, unpublished output. Guarded on exists() for the same
// reason as the /mqtt_prefs store above: remove() on a missing file logs a
// spurious VFS error on ESP32.
if (_fs->exists("/com_prefs.bak")) return false;
if (_fs->exists("/com_prefs.tmp")) {
_fs->remove("/com_prefs.tmp");
if (_fs->exists("/com_prefs.tmp")) return false; // could not clear it
@@ -1903,6 +1947,7 @@ public:
_file = _fs->open("/com_prefs.tmp", "w", true);
#endif
_open = _file;
_owns_temp = _open;
return _open;
}
@@ -1931,14 +1976,25 @@ public:
}
bool commit() {
return _finished && _fs->rename("/com_prefs.tmp", "/com_prefs");
if (!_finished) return false;
if (_fs->exists("/com_prefs.bak")) return false;
if (_fs->exists("/com_prefs")
&& !_fs->rename("/com_prefs", "/com_prefs.bak")) {
return false;
}
if (!_fs->rename("/com_prefs.tmp", "/com_prefs")) return false;
if (_fs->exists("/com_prefs.bak")) _fs->remove("/com_prefs.bak");
return true;
}
void abort() {
if (_open) _file.close();
_open = false;
if (_owns_temp && !_finished && _fs->exists("/com_prefs.tmp")) {
_fs->remove("/com_prefs.tmp");
}
_finished = false;
if (_fs->exists("/com_prefs.tmp")) _fs->remove("/com_prefs.tmp");
_owns_temp = false;
}
private:
@@ -1946,6 +2002,7 @@ private:
File _file;
bool _open = false;
bool _finished = false;
bool _owns_temp = false;
size_t _bytes_written = 0;
};
@@ -1960,14 +2017,59 @@ static const char* commonPrefsSaveResultName(MQTTPrefsAtomicStore::ImageResult r
return "unknown";
}
bool CommonCLI::recoverCommonPrefsFiles(FILESYSTEM* fs) {
using CommonPrefsRecovery::Action;
const Action action = CommonPrefsRecovery::select(
fs->exists("/com_prefs"),
fs->exists("/com_prefs.tmp"),
fs->exists("/com_prefs.bak"));
switch (action) {
case Action::KeepPrimary:
if (fs->exists("/com_prefs.tmp")) fs->remove("/com_prefs.tmp");
if (fs->exists("/com_prefs.bak")) fs->remove("/com_prefs.bak");
return !fs->exists("/com_prefs.tmp") && !fs->exists("/com_prefs.bak");
case Action::PromoteTemp:
if (fs->rename("/com_prefs.tmp", "/com_prefs")) {
if (fs->exists("/com_prefs.bak")) fs->remove("/com_prefs.bak");
return fs->exists("/com_prefs");
}
// The verified new image could not be published. Restore the previous
// image so boot can continue with the last committed radio settings.
if (fs->rename("/com_prefs.bak", "/com_prefs")) {
if (fs->exists("/com_prefs.tmp")) fs->remove("/com_prefs.tmp");
return true;
}
return false;
case Action::PromoteBackup:
return fs->rename("/com_prefs.bak", "/com_prefs");
case Action::DiscardTemp:
// With no backup, a reset may have interrupted the very first write
// before finish(). Keep defaults or /node_prefs and retry the save.
fs->remove("/com_prefs.tmp");
return !fs->exists("/com_prefs.tmp");
case Action::None:
return true;
}
return false;
}
bool CommonCLI::saveCommonPrefsImageAtomically(FILESYSTEM* fs) {
if (!recoverCommonPrefsFiles(fs)) {
MESH_DEBUG_PRINTLN("Prefs: refusing save while recovery is incomplete");
return false;
}
CommonPrefsFileStore store(fs);
const MQTTPrefsAtomicStore::ImageResult result = MQTTPrefsAtomicStore::writeImage(
store, [this](CommonPrefsFileStore& target) {
return writeCommonPrefsImage(target, _prefs);
});
if (!MQTTPrefsAtomicStore::imageCommitted(result)) {
MESH_DEBUG_PRINTLN("Prefs: atomic /com_prefs migration save failed at %s; /node_prefs preserved",
MESH_DEBUG_PRINTLN("Prefs: atomic /com_prefs save failed at %s; previous image preserved",
commonPrefsSaveResultName(result));
return false;
}
@@ -2226,6 +2328,238 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
// Observer-only top-level commands (ota check/update, tls.bundletest, alert test)
// live in CommonCLI_Observer.cpp.
if (handleObserverCommand(sender_timestamp, command, reply)) return;
#if defined(PORTABLE_ESP32_RADIO_CLI)
// Portable WiFi-heavy images keep the commands needed to commission,
// update, and diagnose the node. Their feature-complete FULL siblings keep
// the complete administration, logging, and external-sensor command tree.
if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
_board->powerOff(); // doesn't return
} else if (strcmp(command, "reboot") == 0) {
_board->reboot(); // doesn't return
} else if (strcmp(command, "advert.zerohop") == 0) {
_callbacks->sendSelfAdvertisement(1500, false);
strcpy(reply, "OK - zerohop advert sent");
} else if (strcmp(command, "advert") == 0) {
_callbacks->sendSelfAdvertisement(1500, true);
strcpy(reply, "OK - Advert sent");
} else if (strcmp(command, "clock sync") == 0) {
uint32_t curr = getRTCClock()->getCurrentTime();
if (sender_timestamp > curr) {
getRTCClock()->setCurrentTime(sender_timestamp + 1);
_callbacks->onManualClockSet();
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC",
dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else {
strcpy(reply, "ERR: clock cannot go backwards");
}
#ifdef ESP_PLATFORM
} else if (strcmp(command, "memory") == 0) {
sprintf(reply, "Free: %d, Min: %d, Max: %d, Queue: %d, IntFree: %d, IntMax: %d, PSRAM: %d/%d",
ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap(),
_callbacks->getQueueSize(),
(int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
(int)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL),
(int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM),
(int)heap_caps_get_total_size(MALLOC_CAP_SPIRAM));
#endif
} else if (memcmp(command, "start ota", 9) == 0
&& (command[9] == 0 || command[9] == ' ')) {
const bool force_ap = command[9] == ' ' && strcmp(&command[10], "ap") == 0;
if (command[9] == ' ' && !force_ap) {
strcpy(reply, "ERR: usage start ota [ap]");
} else if (!_board->startOTAUpdate(_prefs->node_name, reply, force_ap)) {
strcpy(reply, "Error");
}
#if defined(WITH_MQTT_BRIDGE) && defined(LIGHTWEIGHT_WIFI_OTA)
else {
_callbacks->setBridgeState(false);
}
#endif
} else if (strcmp(command, "stop ota") == 0) {
if (!_board->stopOTAUpdate(reply)) {
strcpy(reply, "Error");
}
#if defined(WITH_MQTT_BRIDGE) && defined(LIGHTWEIGHT_WIFI_OTA)
else if (_prefs->bridge_enabled) {
_callbacks->setBridgeState(true);
}
#endif
} else if (strcmp(command, "clock") == 0) {
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(reply, "%02d:%02d - %d/%d/%d UTC",
dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else if (memcmp(command, "time ", 5) == 0) {
uint32_t secs = _atoi(&command[5]);
uint32_t curr = getRTCClock()->getCurrentTime();
if (secs > curr) {
getRTCClock()->setCurrentTime(secs);
_callbacks->onManualClockSet();
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC",
dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else {
strcpy(reply, "(ERR: clock cannot go backwards)");
}
} else if (strcmp(command, "neighbors") == 0) {
_callbacks->formatNeighborsReply(reply);
} else if (memcmp(command, "password ", 9) == 0 && command[9] != 0) {
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
savePrefs();
sprintf(reply, "password now: %s", _prefs->password);
} else if (memcmp(command, "get ", 4) == 0) {
handleGetCmd(sender_timestamp, command, reply);
} else if (memcmp(command, "set ", 4) == 0) {
handleSetCmd(sender_timestamp, command, reply);
} else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
bool success = _callbacks->formatFileSystem();
sprintf(reply, "File system erase: %s", success ? "OK" : "Err");
} else if (strcmp(command, "ver") == 0) {
sprintf(reply, "%s (Build: %s)",
_callbacks->getFirmwareVer(), _callbacks->getBuildDate());
} else if (strcmp(command, "board") == 0) {
sprintf(reply, "%s", _board->getManufacturerName());
} else if (memcmp(command, "region", 6) == 0
&& (command[6] == 0 || command[6] == ' ')) {
handleRegionCmd(command, reply);
#if ENV_INCLUDE_GPS == 1
} else if (strcmp(command, "gps on") == 0) {
if (_sensors->setSettingValue("gps", "1")) {
_prefs->gps_enabled = 1;
savePrefs();
strcpy(reply, _prefs->powersaving_enabled ? "on (powersaving)" : "ok");
} else {
strcpy(reply, "gps toggle not found");
}
} else if (strcmp(command, "gps off") == 0) {
if (_sensors->setSettingValue("gps", "0")) {
_prefs->gps_enabled = 0;
savePrefs();
strcpy(reply, "ok");
} else {
strcpy(reply, "gps toggle not found");
}
} else if (strcmp(command, "gps sync") == 0) {
LocationProvider* location = _sensors->getLocationProvider();
if (!_prefs->gps_enabled) {
strcpy(reply, "gps is off");
} else if (location != NULL) {
location->syncTime();
strcpy(reply, "scheduled");
} else {
strcpy(reply, "gps provider not found");
}
} else if (strcmp(command, "gps setloc") == 0) {
_prefs->node_lat = _sensors->node_lat;
_prefs->node_lon = _sensors->node_lon;
savePrefs();
strcpy(reply, "ok");
} else if (memcmp(command, "gps advert", 10) == 0
&& (command[10] == 0 || command[10] == ' ')) {
if (command[10] == 0) {
switch (_prefs->advert_loc_policy) {
case ADVERT_LOC_NONE: strcpy(reply, "> none"); break;
case ADVERT_LOC_PREFS: strcpy(reply, "> prefs"); break;
case ADVERT_LOC_SHARE: strcpy(reply, "> share"); break;
default: strcpy(reply, "error");
}
} else if (strcmp(&command[11], "none") == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_NONE;
savePrefs();
strcpy(reply, "ok");
} else if (strcmp(&command[11], "share") == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_SHARE;
savePrefs();
strcpy(reply, "ok");
} else if (strcmp(&command[11], "prefs") == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_PREFS;
savePrefs();
strcpy(reply, "ok");
} else {
strcpy(reply, "error");
}
} else if (strcmp(command, "gps") == 0) {
LocationProvider* location = _sensors->getLocationProvider();
if (location == NULL) {
strcpy(reply, "Can't find GPS");
} else {
bool enabled = location->isEnabled();
bool fix = location->isValid();
int sats = location->satellitesCount();
const char* gps_setting = _sensors->getSettingByKey("gps");
bool active = gps_setting != NULL && strcmp(gps_setting, "1") == 0;
if (_prefs->powersaving_enabled && location->getGPSPowerSaving()) {
unsigned long now = millis();
unsigned long next_off = location->getNextGPSOff();
unsigned long deadline =
next_off != 0 ? next_off : location->getNextGPSOn();
long remaining_ms = deadline == 0 ? 0 : (long)(deadline - now);
unsigned long mins =
remaining_ms > 0 ? (unsigned long)remaining_ms / 60000UL : 0;
if (next_off != 0) {
snprintf(reply, 160,
"on (powersaving, sleep in %luh %lum), %s, %s, %d sats",
mins / 60UL, mins % 60UL,
active ? "active" : "deactivated",
fix ? "fix" : "no fix", sats);
} else {
snprintf(reply, 160, "off (powersaving, wake in %luh %lum)",
mins / 60UL, mins % 60UL);
}
unsigned long last_sync = location->getLastValidTimeSync();
size_t used = strlen(reply);
if (last_sync == 0) {
snprintf(reply + used, 160 - used, ", last sync: none");
} else {
DateTime dt = DateTime(last_sync);
snprintf(reply + used, 160 - used,
", last sync: %02d:%02d - %d/%d/%d UTC",
dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
}
} else if (enabled) {
sprintf(reply, "on, %s, %s, %d sats",
active ? "active" : "deactivated",
fix ? "fix" : "no fix", sats);
} else {
strcpy(reply, "off");
}
}
#endif
} else if (strcmp(command, "sensor") == 0) {
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL)
sprintf(reply, "I2C Wire1: SDA=%s,SCL=%s\r\n",
STR(ENV_PIN_SDA), STR(ENV_PIN_SCL));
#elif defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n",
STR(PIN_BOARD_SDA), STR(PIN_BOARD_SCL));
#elif defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n",
STR(PIN_WIRE_SDA), STR(PIN_WIRE_SCL));
#else
sprintf(reply, "I2C GPIOs not defined\r\n");
#endif
#if defined(PIN_GPS_RX) && defined(PIN_GPS_TX)
sprintf(reply + strlen(reply), "GPS Serial: RX=%s, TX=%s",
STR(PIN_GPS_RX), STR(PIN_GPS_TX));
#if defined(ENV_INCLUDE_GPS) && ENV_INCLUDE_GPS > 0
sprintf(reply + strlen(reply), ". Configured");
#else
sprintf(reply + strlen(reply), ". Not configured");
#endif
#else
sprintf(reply + strlen(reply), "GPS Serial not defined");
#endif
} else if (strcmp(command, "powerlog") == 0) {
sprintf(reply, "Last reset reason: %s",
_board->getResetReasonString(_board->getResetReason()));
} else {
strcpy(reply, "Unknown command");
}
return;
#else
if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
_board->powerOff(); // doesn't return
} else if (memcmp(command, "reboot", 6) == 0) {
@@ -2346,12 +2680,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
strcpy(tmp, &command[10]);
const char *parts[5];
int num = mesh::Utils::parseTextParts(tmp, parts, 5);
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
float freq = 0.0f;
float bw = 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
int temp_timeout_mins = num > 4 ? atoi(parts[4]) : 0;
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && isValidLoRaBandwidth(bw) && temp_timeout_mins > 0) {
if (num == 5
&& mesh::cli::parseDecimalStrict(parts[0], freq)
&& mesh::cli::parseDecimalStrict(parts[1], bw)
&& freq >= 150.0f && freq <= 2500.0f
&& sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8
&& isValidLoRaBandwidth(bw) && temp_timeout_mins > 0) {
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins);
} else {
@@ -2657,6 +2996,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else {
strcpy(reply, "Unknown command");
}
#endif
}
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
@@ -2679,11 +3019,174 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
#endif
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
#if defined(PORTABLE_ESP32_MINIMAL_CLI)
// Size-constrained ESP32 images keep role-specific controls but omit the
// large general configuration parser. Radio and mesh defaults remain fixed
// by the selected build profile.
sprintf(reply, "unknown portable config: %s", config);
#if defined(PORTABLE_ESP32_RADIO_CLI)
// Portable WiFi-heavy images keep the controls needed to configure and
// diagnose their radio. The full parser remains available in the FULL image.
if (memcmp(config, "int.thresh ", 11) == 0) {
_prefs->interference_threshold = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "cad ", 4) == 0) {
const char* value = &config[4];
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: use set cad on|off");
} else {
_prefs->cad_enabled = strcmp(value, "on") == 0;
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
sprintf(reply, "OK - interval rounded to %d",
((uint32_t)_prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "repeat ", 7) == 0) {
const char* value = &config[7];
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error, must be on or off");
} else {
_prefs->disable_fwd = strcmp(value, "off") == 0;
savePrefs();
_callbacks->onRetryConfigChanged();
strcpy(reply, _prefs->disable_fwd
? "OK - repeat is now OFF" : "OK - repeat is now ON");
}
} else if (memcmp(config, "radio.rxgain ", 13) == 0) {
const char* value = &config[13];
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: use set radio.rxgain on|off");
} else {
bool enabled = strcmp(value, "on") == 0;
if (_callbacks->setRxBoostedGain(enabled)) {
_prefs->rx_boosted_gain = enabled;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: unsupported");
}
}
} else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) {
const char* value = &config[17];
if (!_board->canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: state must be on or off");
} else {
bool enabled = strcmp(value, "on") == 0;
bool changed = _board->isLoRaFemLnaEnabled() != enabled;
if (_board->setLoRaFemLnaEnabled(enabled)) {
if (changed) _callbacks->recalibrateNoiseFloor();
_prefs->radio_fem_rxgain = enabled ? 1 : 0;
savePrefs();
strcpy(reply, enabled
? "OK - LoRa FEM RX gain on" : "OK - LoRa FEM RX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
}
} else if (memcmp(config, "radio ", 6) == 0) {
strcpy(tmp, &config[6]);
const char* parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
float freq = 0.0f;
float bw = 0.0f;
uint8_t sf = 0;
uint8_t cr = 0;
if (num == 4
&& mesh::cli::parseDecimalStrict(parts[0], freq)
&& mesh::cli::parseDecimalStrict(parts[1], bw)
&& parseUint8Strict(parts[2], 5, 12, sf)
&& parseUint8Strict(parts[3], 5, 8, cr)
&& freq >= 150.0f && freq <= 2500.0f
&& sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8
&& isValidLoRaBandwidth(bw)) {
_prefs->sf = sf;
_prefs->cr = cr;
_prefs->freq = freq;
_prefs->bw = bw;
bool rxps_retuned = recalculateRxPowerSavingFromLevel(_prefs);
_callbacks->savePrefs();
strcpy(reply, rxps_retuned
? "OK - reboot to apply (rxps retuned)" : "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid radio params");
}
} else if (memcmp(config, "tx ", 3) == 0) {
_prefs->tx_power_dbm = atoi(&config[3]);
savePrefs();
_callbacks->setTxPower(_prefs->tx_power_dbm);
strcpy(reply, "OK");
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
float freq = 0.0f;
if (mesh::cli::parseDecimalStrict(&config[5], freq)
&& freq >= 150.0f && freq <= 2500.0f) {
_prefs->freq = freq;
savePrefs();
strcpy(reply, "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid frequency");
}
#ifdef WITH_BRIDGE
} else if (memcmp(config, "bridge.enabled ", 15) == 0) {
const char* value = &config[15];
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: use set bridge.enabled on|off");
} else {
_prefs->bridge_enabled = strcmp(value, "on") == 0;
_callbacks->setBridgeState(_prefs->bridge_enabled);
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "bridge.delay ", 13) == 0) {
int delay = _atoi(&config[13]);
if (delay >= 0 && delay <= 10000) {
_prefs->bridge_delay = (uint16_t)delay;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: delay must be between 0-10000 ms");
}
} else if (memcmp(config, "bridge.source ", 14) == 0) {
const char* value = &config[14];
if (strcmp(value, "rx") != 0 && strcmp(value, "tx") != 0) {
strcpy(reply, "Error: use set bridge.source rx|tx");
} else {
_prefs->bridge_pkt_src = strcmp(value, "rx") == 0;
#ifdef WITH_MQTT_BRIDGE
_mqtt_prefs.mqtt_rx_enabled = _prefs->bridge_pkt_src;
_mqtt_prefs.mqtt_tx_enabled = !_prefs->bridge_pkt_src;
#endif
savePrefs();
strcpy(reply, "OK");
}
#endif
#ifdef WITH_ESPNOW_BRIDGE
} else if (memcmp(config, "bridge.channel ", 15) == 0) {
int channel = atoi(&config[15]);
if (channel >= 1 && channel <= 14) {
_prefs->bridge_channel = (uint8_t)channel;
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: channel must be between 1-14");
}
} else if (memcmp(config, "bridge.secret ", 14) == 0) {
const char* secret = &config[14];
if (secret[0] == 0 || strlen(secret) >= sizeof(_prefs->bridge_secret)) {
sprintf(reply, "Error: secret must be 1-%u characters",
(unsigned)(sizeof(_prefs->bridge_secret) - 1));
} else {
StrHelper::strncpy(_prefs->bridge_secret, secret,
sizeof(_prefs->bridge_secret));
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
}
#endif
} else {
sprintf(reply, "unknown portable config: %s", config);
}
return;
#else
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
@@ -2695,8 +3198,9 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
return;
}
if (memcmp(config, "dutycycle ", 10) == 0) {
float dc = atof(&config[10]);
if (dc < 1 || dc > 100) {
float dc = 0.0f;
if (!mesh::cli::parseDecimalStrict(&config[10], dc)
|| dc < 1.0f || dc > 100.0f) {
strcpy(reply, "ERROR: dutycycle must be 1-100");
} else {
_prefs->airtime_factor = (100.0f / dc) - 1.0f;
@@ -2724,17 +3228,27 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error: unsupported on this platform");
#endif
} else if (memcmp(config, "af ", 3) == 0) {
_prefs->airtime_factor = atof(&config[3]);
savePrefs();
strcpy(reply, "OK");
float factor = 0.0f;
if (mesh::cli::parseDecimalStrict(&config[3], factor)) {
_prefs->airtime_factor = factor;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, invalid airtime factor");
}
} else if (memcmp(config, "int.thresh ", 11) == 0) {
_prefs->interference_threshold = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "cad ", 4) == 0) {
_prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
const char* value = &config[4];
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: use set cad on|off");
} else {
_prefs->cad_enabled = strcmp(value, "on") == 0;
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
@@ -2951,11 +3465,16 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(tmp, &config[6]);
const char *parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
float freq = 0.0f;
float bw = 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && isValidLoRaBandwidth(bw)) {
if (num == 4
&& mesh::cli::parseDecimalStrict(parts[0], freq)
&& mesh::cli::parseDecimalStrict(parts[1], bw)
&& freq >= 150.0f && freq <= 2500.0f
&& sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8
&& isValidLoRaBandwidth(bw)) {
_prefs->sf = sf;
_prefs->cr = cr;
_prefs->freq = freq;
@@ -2991,16 +3510,27 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_callbacks->addScheduledRadioParams(true, freq, bw, sf, cr, start_time, end_time, reply);
}
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs->node_lat = atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
float latitude = 0.0f;
if (mesh::cli::parseDecimalStrict(&config[4], latitude)) {
_prefs->node_lat = latitude;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, invalid latitude");
}
} else if (memcmp(config, "lon ", 4) == 0) {
_prefs->node_lon = atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
float longitude = 0.0f;
if (mesh::cli::parseDecimalStrict(&config[4], longitude)) {
_prefs->node_lon = longitude;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, invalid longitude");
}
} else if (memcmp(config, "rxdelay ", 8) == 0) {
float db = atof(&config[8]);
if (db >= 0 && db <= 20.0f) {
float db = 0.0f;
if (mesh::cli::parseDecimalStrict(&config[8], db)
&& db >= 0.0f && db <= 20.0f) {
_prefs->rx_delay_base = db;
savePrefs();
strcpy(reply, "OK");
@@ -3008,8 +3538,9 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error, must be 0-20");
}
} else if (memcmp(config, "txdelay ", 8) == 0) {
float f = atof(&config[8]);
if (f >= 0 && f <= 2.0f) {
float f = 0.0f;
if (mesh::cli::parseDecimalStrict(&config[8], f)
&& f >= 0.0f && f <= 2.0f) {
_prefs->tx_delay_factor = f;
savePrefs();
strcpy(reply, "OK");
@@ -3044,8 +3575,9 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error, max 64");
}
} else if (memcmp(config, "direct.txdelay ", 15) == 0) {
float f = atof(&config[15]);
if (f >= 0 && f <= 2.0f) {
float f = 0.0f;
if (mesh::cli::parseDecimalStrict(&config[15], f)
&& f >= 0.0f && f <= 2.0f) {
_prefs->direct_tx_delay_factor = f;
savePrefs();
strcpy(reply, "OK");
@@ -3416,9 +3948,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_callbacks->setTxPower(_prefs->tx_power_dbm);
strcpy(reply, "OK");
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
_prefs->freq = atof(&config[5]);
savePrefs();
strcpy(reply, "OK - reboot to apply");
float freq = 0.0f;
if (mesh::cli::parseDecimalStrict(&config[5], freq)
&& freq >= 150.0f && freq <= 2500.0f) {
_prefs->freq = freq;
savePrefs();
strcpy(reply, "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid frequency");
}
#ifdef WITH_BRIDGE
} else if (memcmp(config, "bridge.enabled ", 15) == 0) {
_prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
@@ -3483,8 +4021,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
}
#endif
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
_prefs->adc_multiplier = atof(&config[15]);
if (_board->setAdcMultiplier(_prefs->adc_multiplier)) {
float multiplier = 0.0f;
if (!mesh::cli::parseDecimalStrict(&config[15], multiplier)) {
strcpy(reply, "Error: invalid multiplier");
} else if (_board->setAdcMultiplier(multiplier)) {
_prefs->adc_multiplier = multiplier;
savePrefs();
if (_prefs->adc_multiplier == 0.0f) {
strcpy(reply, "OK - using default board multiplier");
@@ -3492,7 +4033,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier);
}
} else {
_prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported");
};
} else if (memcmp(config, "reboot.interval ", 16) == 0) {
@@ -3526,8 +4066,66 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
#endif
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
if (handleObserverGetCmd(sender_timestamp, config, reply)) return;
#if defined(PORTABLE_ESP32_MINIMAL_CLI)
sprintf(reply, "unknown portable config: %s", config);
#if defined(PORTABLE_ESP32_RADIO_CLI)
if (strcmp(config, "int.thresh") == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->interference_threshold);
} else if (strcmp(config, "cad") == 0) {
sprintf(reply, "> %s. # channel busy: %u",
_prefs->cad_enabled ? "on" : "off", _board->n_cad_busy);
} else if (strcmp(config, "agc.reset.interval") == 0) {
sprintf(reply, "> %d", ((uint32_t)_prefs->agc_reset_interval) * 4);
} else if (strcmp(config, "repeat") == 0) {
sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on");
} else if (strcmp(config, "radio.rxgain") == 0) {
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
} else if (strcmp(config, "radio.fem.rxgain") == 0) {
if (!_board->canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s",
_board->isLoRaFemLnaEnabled() ? "on" : "off");
}
} else if (strcmp(config, "radio") == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq));
strcpy(bw, StrHelper::ftoa3(_prefs->bw));
sprintf(reply, "> %s,%s,%d,%d",
freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
} else if (strcmp(config, "tx") == 0) {
sprintf(reply, "> %d", (int32_t)_prefs->tx_power_dbm);
} else if (strcmp(config, "freq") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
} else if (strcmp(config, "role") == 0) {
sprintf(reply, "> %s", _callbacks->getRole());
#ifdef WITH_BRIDGE
} else if (strcmp(config, "bridge.enabled") == 0) {
sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
} else if (strcmp(config, "bridge.delay") == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
} else if (strcmp(config, "bridge.source") == 0) {
sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
} else if (strcmp(config, "bridge.type") == 0) {
sprintf(reply, "> %s",
#ifdef WITH_RS232_BRIDGE
"rs232"
#elif WITH_ESPNOW_BRIDGE
"espnow"
#elif WITH_MQTT_BRIDGE
"mqtt"
#else
"none"
#endif
);
#endif
#ifdef WITH_ESPNOW_BRIDGE
} else if (strcmp(config, "bridge.channel") == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
} else if (strcmp(config, "bridge.secret") == 0) {
sprintf(reply, "> %s", _prefs->bridge_secret);
#endif
} else {
sprintf(reply, "unknown portable config: %s", config);
}
return;
#else
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
@@ -3747,6 +4345,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
"rs232"
#elif WITH_ESPNOW_BRIDGE
"espnow"
#elif WITH_MQTT_BRIDGE
"mqtt"
#else
"none"
#endif
+1
View File
@@ -476,6 +476,7 @@ class CommonCLI {
void savePrefs();
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
#ifdef WITH_MQTT_BRIDGE
bool recoverCommonPrefsFiles(FILESYSTEM* fs);
bool saveCommonPrefsImageAtomically(FILESYSTEM* fs);
void loadMQTTPrefs(FILESYSTEM* fs, MQTTPrefsAtomicStore::LegacyUpgradeGate* legacy_upgrade);
bool saveMQTTPrefs(FILESYSTEM* fs);
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <stdint.h>
// Recovery policy for a verified temp/backup preference transaction.
// The writer moves the old primary to backup only after temp is complete.
namespace CommonPrefsRecovery {
enum class Action : uint8_t {
None,
KeepPrimary,
PromoteTemp,
PromoteBackup,
DiscardTemp,
};
inline Action select(bool primary_exists, bool temp_exists, bool backup_exists) {
if (primary_exists) return Action::KeepPrimary;
if (backup_exists && temp_exists) return Action::PromoteTemp;
if (backup_exists) return Action::PromoteBackup;
if (temp_exists) return Action::DiscardTemp;
return Action::None;
}
} // namespace CommonPrefsRecovery
+274 -54
View File
@@ -364,23 +364,257 @@ bool ESP32Board::stopOTAUpdate(char reply[]) {
// this. We read the web-flasher manifest (config.json), find the `flash-update`
// (app-only) build for our own variant, refuse partition-change releases (OTA
// can't rewrite the partition table), skip if already up to date, then stream
// the .bin straight into the inactive OTA slot via HTTPUpdate.
// the .bin straight into the inactive OTA slot.
// ---------------------------------------------------------------------------
#if defined(WITH_MQTT_BRIDGE)
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <HTTPUpdate.h>
#include <Update.h>
#include <ArduinoJson.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_partition.h>
#include <strings.h>
// Embedded CA bundle (produced by board_build.embed_files). Weak so non-bundle
// builds still link; we check for presence at runtime.
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start") __attribute__((weak));
extern const uint8_t rootca_crt_bundle_end[] asm("_binary_src_certs_x509_crt_bundle_bin_end") __attribute__((weak));
struct OtaHttpResponse {
int status;
size_t content_length;
bool has_content_length;
};
static bool ota_parseHttpUrl(const char* url, bool require_tls,
char* host, size_t host_size,
char* path, size_t path_size,
uint16_t& port) {
const char* cursor;
bool tls;
if (strncmp(url, "https://", 8) == 0) {
cursor = url + 8;
tls = true;
port = 443;
} else if (strncmp(url, "http://", 7) == 0) {
cursor = url + 7;
tls = false;
port = 80;
} else {
return false;
}
if (tls != require_tls) return false;
const char* slash = strchr(cursor, '/');
const char* authority_end = slash ? slash : cursor + strlen(cursor);
const char* colon = nullptr;
for (const char* p = cursor; p < authority_end; p++) {
if (*p == ':') colon = p;
}
const char* host_end = colon ? colon : authority_end;
size_t host_len = (size_t)(host_end - cursor);
if (host_len == 0 || host_len >= host_size) return false;
memcpy(host, cursor, host_len);
host[host_len] = 0;
if (colon) {
uint32_t parsed_port = 0;
const char* p = colon + 1;
if (p == authority_end) return false;
while (p < authority_end) {
if (*p < '0' || *p > '9') return false;
parsed_port = parsed_port * 10U + (uint32_t)(*p++ - '0');
if (parsed_port > 65535U) return false;
}
if (parsed_port == 0) return false;
port = (uint16_t)parsed_port;
}
const char* source_path = slash ? slash : "/";
size_t path_len = strlen(source_path);
if (path_len >= path_size) return false;
memcpy(path, source_path, path_len + 1);
return true;
}
static bool ota_readHttpLine(Client& client, char* line, size_t line_size) {
size_t length = 0;
uint32_t deadline = millis() + 20000;
while (client.connected() || client.available()) {
while (client.available()) {
int c = client.read();
if (c < 0) break;
if (c == '\n') {
if (length && line[length - 1] == '\r') length--;
line[length] = 0;
return true;
}
if (length + 1 < line_size) line[length++] = (char)c;
}
if ((int32_t)(millis() - deadline) >= 0) break;
delay(1);
}
return false;
}
static bool ota_openHttp(Client& client, const char* url, bool require_tls,
OtaHttpResponse& response, char reply[]) {
char host[96];
char path[224];
uint16_t port;
if (!ota_parseHttpUrl(url, require_tls, host, sizeof(host),
path, sizeof(path), port)) {
strcpy(reply, require_tls ? "ERR: invalid HTTPS URL" : "ERR: invalid HTTP URL");
return false;
}
client.setTimeout(20000);
if (!client.connect(host, port)) {
strcpy(reply, "ERR: HTTP connect failed");
return false;
}
client.printf("GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: MeshCore-OTA\r\n"
"Accept: */*\r\nAccept-Encoding: identity\r\n"
"Cache-Control: no-cache\r\nConnection: close\r\n\r\n",
path, host);
char line[192];
if (!ota_readHttpLine(client, line, sizeof(line))) {
strcpy(reply, "ERR: HTTP response timeout");
client.stop();
return false;
}
char* status_text = strchr(line, ' ');
if (strncmp(line, "HTTP/", 5) != 0 || status_text == nullptr) {
strcpy(reply, "ERR: invalid HTTP response");
client.stop();
return false;
}
response.status = atoi(status_text + 1);
response.content_length = 0;
response.has_content_length = false;
bool chunked = false;
bool headers_complete = false;
while (ota_readHttpLine(client, line, sizeof(line))) {
if (line[0] == 0) {
headers_complete = true;
break;
}
if (strncasecmp(line, "Content-Length:", 15) == 0) {
const char* value = line + 15;
while (*value == ' ') value++;
char* end = nullptr;
unsigned long parsed = strtoul(value, &end, 10);
while (end != nullptr && *end == ' ') end++;
if (value == end || end == nullptr || *end != 0) {
strcpy(reply, "ERR: invalid HTTP length");
client.stop();
return false;
}
response.content_length = (size_t)parsed;
response.has_content_length = true;
} else if (strncasecmp(line, "Transfer-Encoding:", 18) == 0) {
const char* value = line + 18;
while (*value == ' ') value++;
chunked = strncasecmp(value, "chunked", 7) == 0;
}
}
if (!headers_complete) {
strcpy(reply, "ERR: incomplete HTTP headers");
client.stop();
return false;
}
if (chunked) {
strcpy(reply, "ERR: chunked HTTP unsupported");
client.stop();
return false;
}
return true;
}
static bool ota_fetchManifest(Client& client, const char* url, bool require_tls,
JsonDocument& doc, char reply[]) {
OtaHttpResponse response;
if (!ota_openHttp(client, url, require_tls, response, reply)) return false;
if (response.status != 200) {
snprintf(reply, 160, "ERR: manifest HTTP %d", response.status);
client.stop();
return false;
}
if (response.has_content_length && response.content_length > 2048) {
strcpy(reply, "ERR: manifest too large");
client.stop();
return false;
}
DeserializationError err = deserializeJson(doc, client);
client.stop();
if (err) {
snprintf(reply, 160, "ERR: manifest parse (%s)", err.c_str());
return false;
}
return true;
}
static bool ota_streamFirmware(Client& client, size_t content_length, char reply[]) {
if (content_length == 0) {
strcpy(reply, "ERR: empty firmware image");
return false;
}
if (!Update.begin(content_length, U_FLASH)) {
snprintf(reply, 160, "ERR: OTA begin: %s", Update.errorString());
return false;
}
uint8_t buffer[2048];
size_t received_total = 0;
int progress_decile = -1;
uint32_t deadline = millis() + 20000;
while (received_total < content_length) {
int available = client.available();
if (available <= 0) {
if (!client.connected()) {
strcpy(reply, "ERR: firmware download incomplete");
Update.abort();
return false;
}
if ((int32_t)(millis() - deadline) >= 0) {
strcpy(reply, "ERR: firmware download timeout");
Update.abort();
return false;
}
delay(1);
continue;
}
size_t wanted = content_length - received_total;
if (wanted > sizeof(buffer)) wanted = sizeof(buffer);
if (wanted > (size_t)available) wanted = (size_t)available;
int received = client.read(buffer, wanted);
if (received <= 0) continue;
deadline = millis() + 20000;
if (Update.write(buffer, (size_t)received) != (size_t)received) {
snprintf(reply, 160, "ERR: OTA write: %s", Update.errorString());
Update.abort();
return false;
}
received_total += (size_t)received;
int decile = (int)((received_total * 10U) / content_length);
if (decile != progress_decile) {
progress_decile = decile;
Serial.printf("OTA: %d%%\n", decile * 10);
}
}
if (!Update.end()) {
snprintf(reply, 160, "ERR: OTA finish: %s", Update.errorString());
return false;
}
return true;
}
// Extract the trailing build hash. For a filename we first drop a ".bin"
// suffix, then take the token after the last '-'. Works for both the manifest
// asset name ("...-v1.16.0-8b084d5.bin" -> "8b084d5") and the embedded
@@ -474,7 +708,7 @@ static void ota_task_entry(void* param) {
}
bool ESP32Board::otaFromManifest(const char* current_ver, bool dry_run, char reply[]) {
// The TLS handshake (cert-bundle verify) + JSON parse / HTTPUpdate use far more
// The TLS handshake (cert-bundle verify), JSON parse, and flash stream use far more
// stack than the ~8 KB loop task offers - especially when reached via the deep
// mesh-receive call chain (it overflows the loopTask canary). Run the work in a
// dedicated 24 KB-stack task and block here until it finishes. The big stack is
@@ -516,8 +750,7 @@ bool ESP32Board::otaFromManifestImpl(const char* current_ver, bool dry_run, char
// <OTA_MANIFEST_BASE>/<OTA_VARIANT>.json - a ~180 byte per-variant file, not the
// full config.json.
char murl[200];
HTTPClient http;
WiFiClientSecure mclient; // only used for the HTTPS (update) path
JsonDocument doc;
if (dry_run) {
// `ota check`: fetch over PLAIN HTTP. With no TLS handshake the fetch costs
@@ -532,51 +765,27 @@ bool ESP32Board::otaFromManifestImpl(const char* current_ver, bool dry_run, char
} else {
snprintf(murl, sizeof(murl), "%s/%s.json", OTA_MANIFEST_BASE, OTA_VARIANT);
}
if (!http.begin(murl)) {
strcpy(reply, "ERR: manifest connect failed");
WiFiClient mclient;
if (!ota_fetchManifest(mclient, murl, false, doc, reply)) {
return false;
}
} else {
// `ota update`: HTTPS. The bridge is torn down for an update so heap is free,
// and integrity matters because we're about to flash.
WiFiClientSecure mclient;
#if ESP_ARDUINO_VERSION_MAJOR >= 3
mclient.setCACertBundle(rootca_crt_bundle_start, bundle_len);
#else
mclient.setCACertBundle(rootca_crt_bundle_start);
#endif
mclient.setTimeout(15000);
snprintf(murl, sizeof(murl), "%s/%s.json", OTA_MANIFEST_BASE, OTA_VARIANT);
if (!http.begin(mclient, murl)) {
strcpy(reply, "ERR: manifest connect failed");
if (!ota_fetchManifest(mclient, murl, true, doc, reply)) {
return false;
}
}
if (!dry_run) { Serial.print("OTA: checking manifest "); Serial.println(murl); }
// Force HTTP/1.0: a CDN (e.g. Cloudflare) answers HTTP/1.1 with chunked encoding
// and no Content-Length; the raw chunked stream corrupts the parse. HTTP/1.0
// yields a Connection: close, unframed body.
http.useHTTP10(true);
http.setTimeout(20000);
int code = http.GET();
if (code != HTTP_CODE_OK) {
snprintf(reply, 160, "ERR: manifest HTTP %d", code);
http.end();
return false;
}
WiFiClient* stream = http.getStreamPtr();
stream->setTimeout(20000); // readBytes honours this, so a slow TLS link != EOF
JsonDocument doc;
DeserializationError err = deserializeJson(doc, *stream);
http.end();
if (err) {
snprintf(reply, 160, "ERR: manifest parse (%s)", err.c_str());
return false;
}
// Copy fields out before the document is reused/cleared.
char file_url[200] = {0}, avail_version[40] = {0}, avail_base[40] = {0}, avail_hash[24] = {0};
strncpy(file_url, doc["file"] | "", sizeof(file_url) - 1);
@@ -689,28 +898,39 @@ bool ESP32Board::otaFromManifestImpl(const char* current_ver, bool dry_run, char
#else
uclient.setCACertBundle(rootca_crt_bundle_start);
#endif
uclient.setTimeout(20000);
// Console progress to the USB serial (always on; MESH_DEBUG is off on the default
// observer profile). Non-capturing lambdas + a file-static decile, so the global
// httpUpdate object never holds a dangling reference after this function returns.
static int ota_progress_decile;
ota_progress_decile = -1;
httpUpdate.onProgress([](int cur, int total) {
if (total <= 0) return;
int d = (int)((int64_t)cur * 10 / total);
if (d != ota_progress_decile) { ota_progress_decile = d; Serial.printf("OTA: %d%%\n", d * 10); }
});
httpUpdate.onEnd([]() { Serial.println("OTA: write complete, rebooting..."); });
httpUpdate.rebootOnUpdate(true); // reboots into the new image on success
t_httpUpdate_return ret = httpUpdate.update(uclient, file_url);
OtaHttpResponse response;
if (!ota_openHttp(uclient, file_url, true, response, reply)) {
inhibit_sleep = false;
Serial.print("OTA: FAILED - "); Serial.println(reply);
return false;
}
if (response.status != 200) {
inhibit_sleep = false;
snprintf(reply, 160, "ERR: firmware HTTP %d", response.status);
uclient.stop();
Serial.print("OTA: FAILED - "); Serial.println(reply);
return false;
}
if (!response.has_content_length) {
inhibit_sleep = false;
strcpy(reply, "ERR: firmware size missing");
uclient.stop();
Serial.print("OTA: FAILED - "); Serial.println(reply);
return false;
}
if (!ota_streamFirmware(uclient, response.content_length, reply)) {
inhibit_sleep = false;
uclient.stop();
Serial.print("OTA: FAILED - "); Serial.println(reply);
return false;
}
// Only reached on failure (success reboots inside update()).
inhibit_sleep = false;
snprintf(reply, 160, "ERR: OTA failed (%d): %s", (int)ret,
httpUpdate.getLastErrorString().c_str());
Serial.print("OTA: FAILED - "); Serial.println(reply);
return false;
uclient.stop();
Serial.println("OTA: write complete, rebooting...");
delay(250);
esp_restart();
return true; // unreachable
#endif // OTA_MANIFEST_BASE && OTA_VARIANT
}
#else
+1 -1
View File
@@ -184,7 +184,7 @@ public:
bool stopOTAUpdate(char reply[]) override;
bool isOTAUpdateRunning() const override { return ota_server != nullptr; }
bool otaFromManifest(const char* current_ver, bool dry_run, char reply[]) override;
// Heavy body (TLS + JSON / HTTPUpdate). Runs in a dedicated large-stack task
// Heavy body (TLS + JSON + flash streaming). Runs in a dedicated large-stack task
// spawned by otaFromManifest() - public only so that task entry point can call
// it; not meant to be invoked directly.
bool otaFromManifestImpl(const char* current_ver, bool dry_run, char reply[]);
+2 -1
View File
@@ -1,4 +1,5 @@
#include "MQTTPayloadBuilder.h"
#include "TxtDataHelpers.h"
#include <math.h>
#include <stdio.h>
@@ -120,7 +121,7 @@ int MQTTPayloadBuilder::buildPacketMessage(
snprintf(len_str, sizeof(len_str), "%d", len);
snprintf(packet_type_str, sizeof(packet_type_str), "%d", packet_type);
snprintf(payload_len_str, sizeof(payload_len_str), "%d", payload_len);
snprintf(snr_str, sizeof(snr_str), "%.1f", snr);
StrHelper::ftoaFixed(snr_str, sizeof(snr_str), snr, 1);
snprintf(rssi_str, sizeof(rssi_str), "%d", rssi);
root["timestamp"] = timestamp;
+6 -3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "Mesh.h"
#include "TxtDataHelpers.h"
class StatsFormatHelper {
public:
@@ -24,11 +25,13 @@ public:
RadioDriverType& driver,
uint32_t total_air_time_ms,
uint32_t total_rx_air_time_ms) {
sprintf(reply,
"{\"noise_floor\":%d,\"last_rssi\":%d,\"last_snr\":%.2f,\"tx_air_secs\":%u,\"rx_air_secs\":%u}",
char last_snr[16];
StrHelper::ftoaFixed(last_snr, sizeof(last_snr), driver.getLastSNR(), 2);
sprintf(reply,
"{\"noise_floor\":%d,\"last_rssi\":%d,\"last_snr\":%s,\"tx_air_secs\":%u,\"rx_air_secs\":%u}",
(int16_t)radio->getNoiseFloor(),
(int16_t)driver.getLastRSSI(),
driver.getLastSNR(),
last_snr,
total_air_time_ms / 1000,
total_rx_air_time_ms / 1000
);
+48
View File
@@ -1,4 +1,6 @@
#include "TxtDataHelpers.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
void StrHelper::stripSurroundingQuotes(char* str, size_t buf_sz) {
@@ -167,6 +169,52 @@ const char* StrHelper::ftoa3(float f) {
return s;
}
bool StrHelper::ftoaFixed(char* dest, size_t dest_size, float value, uint8_t precision) {
static const uint32_t scales[] = {
1U, 10U, 100U, 1000U, 10000U, 100000U, 1000000U, 10000000U
};
if (!dest || dest_size == 0) return false;
if (!isfinite(value)) {
strzcpy(dest, "0", dest_size);
return false;
}
if (precision > 7) precision = 7;
const bool negative = signbit(value);
const float magnitude = negative ? -value : value;
if (magnitude > 4294967040.0f) {
strzcpy(dest, "0", dest_size);
return false;
}
uint32_t whole = (uint32_t)magnitude;
const uint32_t scale = scales[precision];
uint32_t fraction = 0;
if (precision > 0) {
fraction = (uint32_t)(((magnitude - (float)whole) * (float)scale) + 0.5f);
if (fraction >= scale) {
whole++;
fraction = 0;
}
}
int written;
if (precision == 0) {
written = snprintf(dest, dest_size, "%s%lu",
negative ? "-" : "", (unsigned long)whole);
} else {
written = snprintf(dest, dest_size, "%s%lu.%0*lu",
negative ? "-" : "", (unsigned long)whole,
(int)precision, (unsigned long)fraction);
}
if (written < 0 || (size_t)written >= dest_size) {
dest[dest_size - 1] = 0;
return false;
}
return true;
}
uint32_t StrHelper::fromHex(const char* src) {
uint32_t n = 0;
while (*src) {
+1
View File
@@ -17,6 +17,7 @@ public:
static void stripSurroundingQuotes(char* str, size_t buf_sz);
static const char* ftoa(float f);
static const char* ftoa3(float f); //Converts float to string with 3 decimal places
static bool ftoaFixed(char* dest, size_t dest_size, float value, uint8_t precision);
static bool isBlank(const char* str);
static uint32_t fromHex(const char* src);
};
+19 -10
View File
@@ -54,6 +54,15 @@ static constexpr size_t kNtpBuiltinFallbackCount =
static_assert(MQTTBridge::kMaxNtpServers >= 1 + (int)kNtpBuiltinFallbackCount,
"kMaxNtpServers must hold the custom primary plus all built-in fallbacks");
static void formatRadioInfo(float freq, float bw, int sf, int cr,
char* dest, size_t dest_size) {
char freq_text[20];
char bw_text[16];
StrHelper::ftoaFixed(freq_text, sizeof(freq_text), freq, 6);
StrHelper::ftoaFixed(bw_text, sizeof(bw_text), bw, 1);
snprintf(dest, dest_size, "%s,%s,%d,%d", freq_text, bw_text, sf, cr);
}
static bool ntpHostnameEquals(const char* a, const char* b) {
if (!a || !b) return false;
return strcasecmp(a, b) == 0;
@@ -2337,11 +2346,11 @@ void MQTTBridge::publishStatusToSlot(int index) {
gettimeofday(&now_tv, nullptr);
MQTTMessageBuilder::formatIsoTimestampForMqtt(now_tv.tv_sec, now_tv.tv_usec, _timezone, timestamp, sizeof(timestamp));
snprintf(radio_info, sizeof(radio_info), "%.6f,%.1f,%d,%d",
_node_info.freq ? *_node_info.freq : 0.0f,
_node_info.bw ? *_node_info.bw : 0.0f,
_node_info.sf ? *_node_info.sf : 0,
_node_info.cr ? *_node_info.cr : 0);
formatRadioInfo(_node_info.freq ? *_node_info.freq : 0.0f,
_node_info.bw ? *_node_info.bw : 0.0f,
_node_info.sf ? *_node_info.sf : 0,
_node_info.cr ? *_node_info.cr : 0,
radio_info, sizeof(radio_info));
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
@@ -3118,11 +3127,11 @@ bool MQTTBridge::publishStatus() {
gettimeofday(&now_tv, nullptr);
MQTTMessageBuilder::formatIsoTimestampForMqtt(now_tv.tv_sec, now_tv.tv_usec, _timezone, timestamp, sizeof(timestamp));
snprintf(radio_info, sizeof(radio_info), "%.6f,%.1f,%d,%d",
_node_info.freq ? *_node_info.freq : 0.0f,
_node_info.bw ? *_node_info.bw : 0.0f,
_node_info.sf ? *_node_info.sf : 0,
_node_info.cr ? *_node_info.cr : 0);
formatRadioInfo(_node_info.freq ? *_node_info.freq : 0.0f,
_node_info.bw ? *_node_info.bw : 0.0f,
_node_info.sf ? *_node_info.sf : 0,
_node_info.cr ? *_node_info.cr : 0,
radio_info, sizeof(radio_info));
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
+16 -2
View File
@@ -64,12 +64,26 @@ public:
float getLastSNR() const override { return 9.0f; }
// --- concrete RadioLibWrapper surface called directly on radio_driver ---
void setParams(float /*freq*/, float /*bw*/, uint8_t /*sf*/, uint8_t /*cr*/) {}
mesh::RadioParamApplyResult trySetParams(
float /*freq*/, float /*bw*/, uint8_t /*sf*/, uint8_t /*cr*/,
const uint32_t* /*rx_ps_timings*/ = nullptr) override {
return mesh::RadioParamApplyResult::APPLIED;
}
bool setParams(float freq, float bw, uint8_t sf, uint8_t cr,
const uint32_t* rx_ps_timings = nullptr) {
return trySetParams(freq, bw, sf, cr, rx_ps_timings)
== mesh::RadioParamApplyResult::APPLIED;
}
void powerOff() {}
void setTxPower(int8_t /*dbm*/) {}
uint32_t getRngSeed() { return _simRandom(); }
uint32_t getPacketsRecv() const { return n_recv; }
uint32_t getPacketsSent() const { return n_sent; }
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
void setRxBoostedGainMode(bool) {}
bool setRxBoostedGainMode(bool /*enabled*/) { return false; }
bool getRxBoostedGainMode() const { return false; }
uint32_t getRxPsWatchdogSoftCount() const { return 0; }
uint32_t getRxPsWatchdogHardCount() const { return 0; }
bool isWatchdogObserving() const { return false; }
bool isCalibratingNoiseFloor() const { return false; }
};
+10
View File
@@ -2,6 +2,7 @@
#include <cstdint>
#include <cmath>
#include <cstdio>
#include "Stream.h"
inline uint32_t g_mock_millis = 0;
@@ -15,3 +16,12 @@ inline uint32_t millis() {
inline void delay(uint32_t ms) {
g_mock_millis += ms;
}
inline char* ltoa(long value, char* dest, int base) {
if (base == 10) {
std::snprintf(dest, 16, "%ld", value);
} else {
dest[0] = '\0';
}
return dest;
}
@@ -40,6 +40,31 @@ TEST(CLICommandUtils, HandlesLeadingWhitespaceAndSingleWordCommands) {
EXPECT_STREQ("powersaving", single_command);
}
TEST(CLICommandUtils, ParsesStrictDecimalValues) {
float value = 0.0f;
EXPECT_TRUE(mesh::cli::parseDecimalStrict("910.525", value));
EXPECT_FLOAT_EQ(910.525f, value);
EXPECT_TRUE(mesh::cli::parseDecimalStrict(" -122.25 ", value));
EXPECT_FLOAT_EQ(-122.25f, value);
EXPECT_TRUE(mesh::cli::parseDecimalStrict("+.5", value));
EXPECT_FLOAT_EQ(0.5f, value);
EXPECT_TRUE(mesh::cli::parseDecimalStrict("7.", value));
EXPECT_FLOAT_EQ(7.0f, value);
}
TEST(CLICommandUtils, RejectsNonDecimalOrOverflowValues) {
float value = 123.0f;
EXPECT_FALSE(mesh::cli::parseDecimalStrict(nullptr, value));
EXPECT_FALSE(mesh::cli::parseDecimalStrict("", value));
EXPECT_FALSE(mesh::cli::parseDecimalStrict(".", value));
EXPECT_FALSE(mesh::cli::parseDecimalStrict("1.2x", value));
EXPECT_FALSE(mesh::cli::parseDecimalStrict("1e3", value));
EXPECT_FALSE(mesh::cli::parseDecimalStrict("4294967296", value));
EXPECT_FLOAT_EQ(123.0f, value);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
@@ -7,6 +7,7 @@
#include <vector>
#include "helpers/MQTTPayloadBuilder.h"
#include "helpers/TxtDataHelpers.h"
namespace {
@@ -134,6 +135,19 @@ TEST(MQTTPayloadBuilder, RxPacketIncludesMetricsScaledScoreAndPath) {
EXPECT_STREQ("012f", parsed_path[1].as<const char*>());
}
TEST(MQTTPayloadBuilder, FixedFloatFormattingDoesNotRequirePrintfFloat) {
char value[20];
EXPECT_TRUE(StrHelper::ftoaFixed(value, sizeof(value), 915.0f, 6));
EXPECT_STREQ("915.000000", value);
EXPECT_TRUE(StrHelper::ftoaFixed(value, sizeof(value), 10.26f, 1));
EXPECT_STREQ("10.3", value);
EXPECT_TRUE(StrHelper::ftoaFixed(value, sizeof(value), -87.25f, 2));
EXPECT_STREQ("-87.25", value);
EXPECT_FALSE(StrHelper::ftoaFixed(value, sizeof(value), std::nanf(""), 1));
EXPECT_STREQ("0", value);
}
TEST(MQTTPayloadBuilder, TxPacketOmitsReceiveOnlyMetricsAndAbsentPath) {
JsonDocument scratch;
char buffer[2048];
@@ -5,10 +5,12 @@
#include <string>
#include <vector>
#include "helpers/CommonPrefsRecovery.h"
#include "helpers/MQTTPrefsAtomicStore.h"
#include "helpers/MQTTPrefsRecovery.h"
namespace AtomicStore = MQTTPrefsAtomicStore;
namespace CommonRecovery = CommonPrefsRecovery;
namespace Recovery = MQTTPrefsRecovery;
namespace {
@@ -160,7 +162,9 @@ public:
++begin_calls;
_files.erase("/com_prefs.tmp");
_staging.clear();
_finished = false;
_open = _failure != FailurePoint::Begin;
_owns_temp = _open;
return _open;
}
@@ -178,6 +182,7 @@ public:
_open = false;
if (_failure == FailurePoint::Finish) return false;
_files["/com_prefs.tmp"] = _staging;
_finished = true;
return true;
}
@@ -186,6 +191,7 @@ public:
if (_failure == FailurePoint::Commit) return false;
_files["/com_prefs"] = _files["/com_prefs.tmp"];
_files.erase("/com_prefs.tmp");
_finished = false;
return true;
}
@@ -193,7 +199,18 @@ public:
++abort_calls;
_open = false;
_staging.clear();
_files.erase("/com_prefs.tmp");
if (_owns_temp && !_finished) _files.erase("/com_prefs.tmp");
_finished = false;
_owns_temp = false;
}
void recover() {
const CommonRecovery::Action action = CommonRecovery::select(
destinationExists(), tempExists(), false);
if (action == CommonRecovery::Action::KeepPrimary ||
action == CommonRecovery::Action::DiscardTemp) {
_files.erase("/com_prefs.tmp");
}
}
void removeNodeSource() { _files.erase("/node_prefs"); }
@@ -215,6 +232,8 @@ public:
private:
FailurePoint _failure;
bool _open = false;
bool _finished = false;
bool _owns_temp = false;
std::vector<uint8_t> _staging;
std::map<std::string, std::vector<uint8_t>> _files;
};
@@ -497,6 +516,10 @@ TEST(MQTTPrefsAtomicStore, NodePrefsMigrationFailurePreservesSourceAndNeverPrefe
for (const auto& test_case : cases) {
InMemoryCommonPrefsStore store(test_case.point);
EXPECT_EQ(test_case.expected, runCommonPrefsImage(&store));
if (test_case.point == FailurePoint::Commit) {
EXPECT_TRUE(store.tempExists()); // complete, but uncommitted first image
}
store.recover();
EXPECT_EQ(legacy_node, store.nodeSource());
EXPECT_TRUE(store.nodeSourceIsPreferred());
EXPECT_FALSE(store.destinationExists());
@@ -617,6 +640,19 @@ TEST(MQTTPrefsAtomicStore, RecoveryNeverOverwritesOpaqueNewerLayout) {
Recovery::FileState::Preserve));
}
TEST(MQTTPrefsAtomicStore, CommonPrefsRecoveryUsesOnlyVerifiedTempWithBackup) {
EXPECT_EQ(CommonRecovery::Action::KeepPrimary,
CommonRecovery::select(true, true, true));
EXPECT_EQ(CommonRecovery::Action::PromoteTemp,
CommonRecovery::select(false, true, true));
EXPECT_EQ(CommonRecovery::Action::PromoteBackup,
CommonRecovery::select(false, false, true));
EXPECT_EQ(CommonRecovery::Action::DiscardTemp,
CommonRecovery::select(false, true, false));
EXPECT_EQ(CommonRecovery::Action::None,
CommonRecovery::select(false, false, false));
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
+2
View File
@@ -22,6 +22,8 @@ build_flags = ${rak3x72.build_flags}
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
-fipa-pta
-fno-semantic-interposition
-fno-unwind-tables
-fno-asynchronous-unwind-tables
-D LFS_FLASH_TOTAL_SIZE=16384
-D ADVERT_NAME='"RAK3x72 Repeater"'
-D ADMIN_PASSWORD='"password"'
+2
View File
@@ -23,6 +23,8 @@ build_flags = ${Tiny_Relay.build_flags}
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
-fipa-pta
-fno-semantic-interposition
-fno-unwind-tables
-fno-asynchronous-unwind-tables
-D LFS_FLASH_TOTAL_SIZE=16384
-D ADVERT_NAME='"tiny_relay Repeater"'
-D ADVERT_LAT=0.0
+4
View File
@@ -23,6 +23,8 @@ build_flags = ${lora_e5.build_flags}
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
-fipa-pta
-fno-semantic-interposition
-fno-unwind-tables
-fno-asynchronous-unwind-tables
-D LFS_FLASH_TOTAL_SIZE=16384
-D LORA_TX_POWER=22
-D ADVERT_NAME='"WIO-E5 Repeater"'
@@ -39,6 +41,8 @@ build_flags = ${lora_e5.build_flags}
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
-fipa-pta
-fno-semantic-interposition
-fno-unwind-tables
-fno-asynchronous-unwind-tables
-D LFS_FLASH_TOTAL_SIZE=16384
-D MESH_ENABLE_RECENT_REPEATERS=1
-D LORA_TX_POWER=22
+2
View File
@@ -27,6 +27,8 @@ build_flags = ${lora_e5_mini.build_flags}
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
-fipa-pta
-fno-semantic-interposition
-fno-unwind-tables
-fno-asynchronous-unwind-tables
-D RADIO_LIVENESS_SOFT_ONLY=1 ; integrated STM32WL has no separate radio-reset pin
-D WIO_E5_MINI_NO_EXTERNAL_SENSORS=1 ; the dedicated sensor env retains BME280 support
-D LFS_FLASH_TOTAL_SIZE=16384