mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-01 18:08:43 +00:00
Harden observer preferences and portable controls
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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[]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user