Update MeshCore features and OTA layout handling
This commit is contained in:
1 parent
0c4619bcc3
commit
47993d178e
74 files changed
+1696
-1169
No files matched your search
@@ -83,7 +83,7 @@ Commands:
|
||||
list|-l: List firmwares available to build.
|
||||
build-firmware <target>: Build the firmware for the given build target.
|
||||
build-firmwares: Build all firmwares for all targets.
|
||||
build-firmwares-logging-matrix: Build all firmwares in standard, logging, MQTT, FULL ESP32 MQTT, and FULL ESP32 logging (no MQTT) profiles, logging each target under out/build-logs/ and continuing after failures.
|
||||
build-firmwares-logging-matrix: Build all firmwares in standard, logging, FULL ESP32 MQTT, and FULL ESP32 logging (no MQTT) profiles, logging each target under out/build-logs/ and continuing after failures. MQTT observers and ESP-NOW bridges always use FULL.
|
||||
build-companion-firmwares-logging-matrix: Build every Companion target (including Full Companion and legacy FEM variants) in each applicable standard, logging, MQTT, and expanded FULL profile.
|
||||
build-full-esp32-firmwares: Build only feature-complete ESP32 MQTT profiles with up to 254 neighbors, LoRa OTA, and expanded dual-OTA partitions.
|
||||
build-full-esp32-logging-firmwares: Build only feature-complete ESP32 profiles with up to 254 neighbors, logging, MQTT disabled, LoRa OTA, and expanded dual-OTA partitions.
|
||||
@@ -117,7 +117,7 @@ $ bash build.sh
|
||||
Build all firmwares for device targets containing the string "RAK_4631"
|
||||
$ bash build.sh build-matching-firmwares <build-match-spec>
|
||||
|
||||
Build all firmwares in standard, USB logging, MQTT observer, feature-complete ESP32, and feature-complete ESP32 logging profiles:
|
||||
Build all firmwares in standard, USB logging, feature-complete ESP32 MQTT, and feature-complete ESP32 logging profiles:
|
||||
$ bash build.sh build-firmwares-logging-matrix
|
||||
|
||||
Build only feature-complete ESP32 firmware:
|
||||
@@ -2111,6 +2111,18 @@ supports_esp32_full_build() {
|
||||
&& ! is_lora_ota_only_target "$env_name"
|
||||
}
|
||||
|
||||
requires_esp32_full_cli_profile() {
|
||||
local env_name=$1
|
||||
|
||||
# MQTT observers and ESP-NOW bridges previously depended on a reduced CLI to
|
||||
# fit the legacy application slot. Always build those roles with the expanded
|
||||
# FULL partition profile so no administration commands are removed.
|
||||
[ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" = "ESP32_PLATFORM" ] \
|
||||
&& ! is_esp32_companion_build "$env_name" \
|
||||
&& { is_mqtt_bridge_target "$env_name" \
|
||||
|| [[ "${env_name,,}" == *bridge_espnow* ]]; }
|
||||
}
|
||||
|
||||
apply_esp32_lora_ota_size_profile() {
|
||||
local env_name=$1
|
||||
|
||||
@@ -2120,35 +2132,19 @@ apply_esp32_lora_ota_size_profile() {
|
||||
|
||||
# All non-companion ESP32 artifacts must remain installable into the legacy
|
||||
# 0x10000..0x150000 app slot. The WebConfig portal is deliberately omitted.
|
||||
# WiFi/MQTT observers and lean LoRa-OTA repeaters retain the compact browser
|
||||
# updater; other radio-only roles avoid linking WiFi solely for that updater.
|
||||
# Lean LoRa-OTA repeaters retain the compact browser updater; other radio-only
|
||||
# roles avoid linking WiFi solely for that updater. MQTT observers and ESP-NOW
|
||||
# bridges are always promoted to the expanded FULL profile so they retain the
|
||||
# complete CLI and feature set.
|
||||
# Companions retain their target 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
|
||||
# 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
|
||||
append_platformio_build_unflags "-DLIGHTWEIGHT_WIFI_OTA=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"
|
||||
elif is_lora_ota_only_target "$env_name"; then
|
||||
if is_lora_ota_only_target "$env_name"; then
|
||||
# The no-external-sensors image is also the self-updatable field image. Keep
|
||||
# manual browser OTA available on every ESP32 family through the compact
|
||||
# uploader, and use the full one-byte neighbor-index range.
|
||||
# uploader, the complete role CLI, and the full one-byte neighbor-index
|
||||
# range.
|
||||
append_platformio_build_unflags "-DDISABLE_WIFI_OTA=1 -DMAX_NEIGHBOURS=50 -DMAX_NEIGHBOURS=8"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_WIFI_OTA -DLIGHTWEIGHT_WIFI_OTA=1 -DMAX_NEIGHBOURS=${ESP32_FULL_MAX_NEIGHBOURS}"
|
||||
else
|
||||
@@ -2167,7 +2163,7 @@ apply_esp32_full_size_profile() {
|
||||
fi
|
||||
|
||||
# The FULL artifact uses expanded dual-OTA slots, so restore features that
|
||||
# target or portable profiles disabled only to save application space.
|
||||
# target or legacy-slot profiles disabled only to save application space.
|
||||
append_platformio_build_unflags "-DWEBCONFIG_DISABLED=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UWEBCONFIG_DISABLED -DWIFI_OTA_SEEDER=1 -DMESHCORE_ESP32_FULL_PROFILE=1"
|
||||
|
||||
@@ -2215,12 +2211,15 @@ apply_repeater_neighbor_capacity() {
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_NEIGHBOURS=${max_neighbours}"
|
||||
}
|
||||
|
||||
apply_nrf52_lora_ota_size_profile() {
|
||||
apply_nrf52_size_profile() {
|
||||
local env_name=$1
|
||||
|
||||
if [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" != "NRF52_PLATFORM" ] \
|
||||
|| ! is_lora_ota_build "$env_name" \
|
||||
|| ! is_lora_ota_only_target "$env_name"; then
|
||||
if [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" != "NRF52_PLATFORM" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! is_nrf52_companion_radio_full_target "$env_name" \
|
||||
&& { ! is_lora_ota_build "$env_name" \
|
||||
|| ! is_lora_ota_only_target "$env_name"; }; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -2228,7 +2227,8 @@ apply_nrf52_lora_ota_size_profile() {
|
||||
# runtime software Ed25519 fallback is linked alongside CC310, that setting
|
||||
# fully expands repeated Curve25519 arithmetic and wastes tens of kilobytes.
|
||||
# Keep hardware crypto, RNG mixing, the software fallback, and board features;
|
||||
# only select the size optimizer for the constrained self-updatable image.
|
||||
# select the size optimizer for both constrained self-updating images and
|
||||
# Full Companion source images, especially their diagnostic profile.
|
||||
append_platformio_build_unflags "-Ofast"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -Os"
|
||||
}
|
||||
@@ -2501,7 +2501,7 @@ collect_esp32_artifacts() {
|
||||
# Emit the partition-table signature for OTA partition-compatibility checks.
|
||||
# Standard builds keep the env-name key used by the slim-manifest generator;
|
||||
# FULL builds use a suffix so their expanded table does not overwrite the
|
||||
# portable signature. The firmware computes the same signature at runtime.
|
||||
# legacy-slot signature. The firmware computes the same signature at runtime.
|
||||
# Best-effort: local builds without the script's deps just skip it.
|
||||
if [ -f "${build_output_dir}/partitions.bin" ]; then
|
||||
if [ "$ESP32_FULL_BUILD" = "1" ]; then
|
||||
@@ -2669,6 +2669,14 @@ build_firmware() {
|
||||
local build_status
|
||||
local -a pio_run_args=()
|
||||
|
||||
# Bash functions use dynamic scoping. These locals let one target be promoted
|
||||
# to FULL without changing the profile selected for later targets in the same
|
||||
# batch.
|
||||
local inherited_esp32_full_build=$ESP32_FULL_BUILD
|
||||
local inherited_firmware_filename_infix=$FIRMWARE_FILENAME_INFIX
|
||||
local ESP32_FULL_BUILD=$inherited_esp32_full_build
|
||||
local FIRMWARE_FILENAME_INFIX=$inherited_firmware_filename_infix
|
||||
|
||||
env_platform=$(get_platform_for_env "$env_name")
|
||||
if ! is_supported_platform "$env_platform"; then
|
||||
echo "Unsupported or unknown platform for env: $env_name"
|
||||
@@ -2676,6 +2684,17 @@ build_firmware() {
|
||||
fi
|
||||
pio_env_name=$(get_pio_build_env "$env_name")
|
||||
|
||||
if [ "$ESP32_FULL_BUILD" != "1" ] \
|
||||
&& requires_esp32_full_cli_profile "$env_name"; then
|
||||
ESP32_FULL_BUILD=1
|
||||
if [ "$FIRMWARE_FILENAME_INFIX" = "logging" ]; then
|
||||
FIRMWARE_FILENAME_INFIX="full-logging"
|
||||
else
|
||||
FIRMWARE_FILENAME_INFIX="full"
|
||||
fi
|
||||
echo "Promoting ${env_name} to FULL so the complete CLI is retained."
|
||||
fi
|
||||
|
||||
commit_hash=$(git rev-parse --short HEAD)
|
||||
firmware_build_date=$(date -u '+%d-%b-%Y')
|
||||
firmware_build_epoch=$(date -u '+%s')
|
||||
@@ -2761,7 +2780,7 @@ build_firmware() {
|
||||
apply_esp32_lora_ota_size_profile "$env_name"
|
||||
apply_esp32_full_size_profile "$env_name"
|
||||
apply_repeater_neighbor_capacity "$env_name"
|
||||
apply_nrf52_lora_ota_size_profile "$env_name"
|
||||
apply_nrf52_size_profile "$env_name"
|
||||
apply_lora_ota_no_external_sensors_profile "$env_name"
|
||||
apply_radio_overrides
|
||||
apply_firmware_profile_overrides
|
||||
@@ -3440,8 +3459,8 @@ run_logging_matrix_build_targets() {
|
||||
local target
|
||||
local standard_targets=()
|
||||
local logging_targets=()
|
||||
local filtered_logging_targets=()
|
||||
local constrained_logging_targets=()
|
||||
local mqtt_targets=()
|
||||
local original_meshdebug_override=$MESHDEBUG_OVERRIDE
|
||||
local original_packet_logging_override=$PACKET_LOGGING_OVERRIDE
|
||||
local original_mqtt_bridge_override=$MQTT_BRIDGE_OVERRIDE
|
||||
@@ -3451,6 +3470,7 @@ run_logging_matrix_build_targets() {
|
||||
local original_profile_build_workers=$PROFILE_BUILD_WORKERS
|
||||
local bluetooth_skip_count=0
|
||||
local lora_ota_only_skip_count=0
|
||||
local full_cli_logging_skip_count=0
|
||||
local logging_target_count=0
|
||||
local build_status=0
|
||||
local pass_status=0
|
||||
@@ -3464,14 +3484,13 @@ run_logging_matrix_build_targets() {
|
||||
echo "Option 3 parallelism: ${PROFILE_BUILD_WORKERS} target build(s), ${OPTION3_PIO_JOBS} PlatformIO job(s) per target."
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
if is_mqtt_bridge_target "$target"; then
|
||||
mqtt_targets+=("$target")
|
||||
else
|
||||
if ! is_mqtt_bridge_target "$target"; then
|
||||
standard_targets+=("$target")
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Profile 1/5: building ${#standard_targets[@]} standard target(s) with logging off and MQTT bridge off."
|
||||
echo "Profile 1/4: building ${#standard_targets[@]} standard target(s) with logging off and MQTT bridge off."
|
||||
echo "ESP32 ESP-NOW bridge targets in this pass are automatically promoted to FULL so they retain the complete CLI."
|
||||
ESP32_FULL_BUILD=0
|
||||
MESHDEBUG_OVERRIDE="off"
|
||||
PACKET_LOGGING_OVERRIDE="off"
|
||||
@@ -3503,6 +3522,19 @@ run_logging_matrix_build_targets() {
|
||||
echo "Skipping ${lora_ota_only_skip_count} LoRa-OTA-only target(s) for logging-on pass because logging disables LoRa OTA."
|
||||
fi
|
||||
|
||||
filtered_logging_targets=()
|
||||
for target in "${logging_targets[@]}"; do
|
||||
if requires_esp32_full_cli_profile "$target"; then
|
||||
full_cli_logging_skip_count=$((full_cli_logging_skip_count + 1))
|
||||
else
|
||||
filtered_logging_targets+=("$target")
|
||||
fi
|
||||
done
|
||||
logging_targets=("${filtered_logging_targets[@]}")
|
||||
if [ "$full_cli_logging_skip_count" -gt 0 ]; then
|
||||
echo "Deferring ${full_cli_logging_skip_count} ESP32 target(s) to the FULL logging pass so the complete CLI is retained."
|
||||
fi
|
||||
|
||||
for target in "${logging_targets[@]}"; do
|
||||
if is_logging_size_constrained_target "$target"; then
|
||||
constrained_logging_targets+=("$target")
|
||||
@@ -3512,7 +3544,7 @@ run_logging_matrix_build_targets() {
|
||||
logging_target_count=$((${#logging_targets[@]} + ${#constrained_logging_targets[@]}))
|
||||
|
||||
if [ "$logging_target_count" -gt 0 ]; then
|
||||
echo "Profile 2/5: building ${logging_target_count} standard target(s) with logging on and MQTT bridge off."
|
||||
echo "Profile 2/4: building ${logging_target_count} standard target(s) with logging on and MQTT bridge off."
|
||||
echo "Logging-on artifacts use filename form: name-logging-version"
|
||||
else
|
||||
echo "No non-Bluetooth targets remain for logging-on pass."
|
||||
@@ -3541,27 +3573,12 @@ run_logging_matrix_build_targets() {
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
fi
|
||||
|
||||
if [ ${#mqtt_targets[@]} -gt 0 ]; then
|
||||
echo "Profile 3/5: building ${#mqtt_targets[@]} MQTT bridge target(s) for direct radio-to-MQTT forwarding over WiFi, with logging off."
|
||||
MESHDEBUG_OVERRIDE="off"
|
||||
PACKET_LOGGING_OVERRIDE="off"
|
||||
MQTT_BRIDGE_OVERRIDE="on"
|
||||
MQTT_DEBUG_OVERRIDE="off"
|
||||
FIRMWARE_FILENAME_INFIX=""
|
||||
run_logged_build_targets "${mqtt_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
else
|
||||
echo "No MQTT bridge targets are configured; skipping profile 3/5."
|
||||
fi
|
||||
|
||||
run_full_esp32_profile "Profile 4/5" "off" "${targets[@]}"
|
||||
run_full_esp32_profile "Profile 3/4" "off" "${targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
|
||||
run_full_esp32_profile "Profile 5/5" "on" "${targets[@]}"
|
||||
run_full_esp32_profile "Profile 4/4" "on" "${targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
@@ -3689,7 +3706,7 @@ main() {
|
||||
echo "Skipping separate debug and MQTT prompts; FULL everything enables logging and explicitly disables MQTT."
|
||||
elif is_automatic_profile_command "${SELECTED_COMMAND_ARGS[0]}"; then
|
||||
if is_logging_matrix_command "${SELECTED_COMMAND_ARGS[0]}"; then
|
||||
echo "Skipping debug and MQTT prompts; this action builds all five profiles automatically."
|
||||
echo "Skipping debug and MQTT prompts; this action builds all four profiles automatically."
|
||||
elif is_full_esp32_logging_command "${SELECTED_COMMAND_ARGS[0]}"; then
|
||||
echo "Skipping debug and MQTT prompts; this action builds only the FULL ESP32 logging profile with MQTT disabled."
|
||||
else
|
||||
|
||||
+11
-11
@@ -167,9 +167,9 @@ stop webconfig
|
||||
set bridge.enabled on
|
||||
```
|
||||
|
||||
Some size-constrained, portable MQTT observer artifacts omit WebConfig so they
|
||||
fit the legacy ESP32 application slot. They retain the serial/remote CLI and a
|
||||
small WiFi updater. Configure those builds with the CLI.
|
||||
Current MQTT observer artifacts use the expanded FULL partition profile so
|
||||
WebConfig and the complete role CLI are retained. Install the matching merged
|
||||
image over USB once when moving a device from the legacy partition layout.
|
||||
|
||||
## WiFi companion setup
|
||||
|
||||
@@ -374,11 +374,11 @@ role.
|
||||
|
||||
| Build profile | WiFi/MQTT behavior |
|
||||
|---|---|
|
||||
| Standard | Uses the selected target's role. Ordinary portable ESP32 repeater/room-server artifacts omit WebConfig to fit the legacy app slot. Explicit MQTT and WiFi-companion targets still use WiFi. |
|
||||
| Logging | Enables USB/debug packet logging and disables the MQTT bridge. Logging output is not an MQTT uplink. |
|
||||
| MQTT | Builds explicit MQTT observer or WiFi-companion-MQTT targets with USB packet logging off. |
|
||||
| Standard | Uses the selected target's role. Ordinary legacy-slot ESP32 repeater/room-server artifacts omit WebConfig when needed to fit. ESP32 MQTT observer and ESP-NOW bridge targets are automatically promoted to FULL; WiFi-companion targets keep their companion partition profile. |
|
||||
| Logging | Enables USB/debug packet logging and disables the MQTT bridge. `get/set usb.logging` can silence or restore the live output until reboot. Logging output is not an MQTT uplink. |
|
||||
| MQTT | Builds explicit MQTT observer or WiFi-companion-MQTT targets with USB packet logging off. Non-companion ESP32 MQTT observers always use FULL expanded partitions. |
|
||||
| FULL ESP32 | Uses the board's MQTT target with logging off, expanded dual-OTA partitions, up to 254 neighbors, LoRa OTA, and full-size ESP32 features such as WebConfig where supported. Classic T-Beam MQTT observers retain their 50-entry table because their persistent discovery state exhausts internal DRAM at 254. |
|
||||
| FULL ESP32 logging | Uses the board's non-MQTT target with debug and packet logging enabled, expanded dual-OTA partitions, up to 254 neighbors, and LoRa OTA. |
|
||||
| FULL ESP32 logging | Uses the board's non-MQTT target with debug and packet logging enabled, session-only `get/set usb.logging` control, expanded dual-OTA partitions, up to 254 neighbors, and LoRa OTA. |
|
||||
| LoRa-OTA no-external-sensors | A lean repeater image with no MQTT; ESP32 builds retain the compact on-demand browser WiFi uploader and 254 neighbors. |
|
||||
|
||||
All repeater profiles use the full 254-entry neighbor table, including standard,
|
||||
@@ -388,7 +388,7 @@ persistent MQTT discovery state leaves insufficient internal-DRAM margin at 254.
|
||||
|
||||
The interactive Option 1 **FULL everything** choice selects the FULL logging
|
||||
profile: logging is enabled and MQTT is disabled. The standalone FULL ESP32
|
||||
profile and Profile 4 of the five-profile matrix use the matching MQTT target
|
||||
profile and Profile 3 of the four-profile matrix use the matching MQTT target
|
||||
instead. Both FULL profiles include LoRa OTA, WebConfig where supported, up to
|
||||
254 neighbors, and expanded dual-OTA partitions. Target-specific internal-DRAM
|
||||
limits still apply.
|
||||
@@ -432,8 +432,8 @@ their role-specific interfaces described above; they do not expose the full
|
||||
infrastructure WiFi CLI family.
|
||||
MQTT commands such as `get mqtt.status` and `set mqtt1.preset ...` still require
|
||||
an MQTT observer target. Unknown settings return `Error: unknown setting:
|
||||
<name>`. Older portable builds can instead report `Unsupported in this
|
||||
firmware` when a command was intentionally cut for space.
|
||||
<name>`. Older firmware that used the discontinued compact CLI can instead
|
||||
report `Unsupported in this firmware` when a command was cut for space.
|
||||
|
||||
Check the complete firmware filename and role. In particular:
|
||||
|
||||
@@ -467,7 +467,7 @@ Common causes are:
|
||||
- all MQTT slots disabled;
|
||||
- too many TLS/WSS slots for the available internal memory;
|
||||
- WiFi power saving being too aggressive;
|
||||
- the wrong firmware role or a portable build without the full WebConfig CLI.
|
||||
- the wrong firmware role or an older compact-CLI build without WebConfig.
|
||||
|
||||
For a WiFi companion, find its station IP in the router, connect the client to
|
||||
TCP port 5000, and use the setup AP if it cannot join the saved network. MQTT
|
||||
|
||||
+18
-40
@@ -3,7 +3,7 @@
|
||||
MeshCore command availability is determined in three layers:
|
||||
|
||||
1. **Role** - repeater, room server, sensor, companion, bridge, or KISS modem.
|
||||
2. **Build profile** - standard, portable, logging, OTA, or FULL.
|
||||
2. **Build profile** - standard, logging, OTA, or FULL.
|
||||
3. **Compiled hardware features** - WiFi, MQTT, GPS, external sensors, PSRAM,
|
||||
Ethernet, and similar optional support.
|
||||
|
||||
@@ -36,57 +36,35 @@ retain 50 because their MQTT discovery tables are constrained by internal DRAM.
|
||||
| Build/profile | Command availability |
|
||||
|---|---|
|
||||
| Standard non-MQTT repeater or room server | Keeps the normal role CLI. Size-constrained ESP32 artifacts can omit WebConfig and browser WiFi OTA, so their WebConfig/WiFi commands are unavailable. |
|
||||
| Standard logging | Logging does not remove commands by itself. It has the same CLI as the selected role/profile and adds the compiled logging behavior. |
|
||||
| LoRa-OTA (`-ota-`) | LoRa OTA adds the `ota ...` commands; it does not otherwise reduce the role CLI. ESP32 `no_external_sensors` artifacts retain the compact browser WiFi uploader and use a 254-entry neighbor table. A portable OTA artifact can still have the other portable restrictions described below. |
|
||||
| Portable MQTT observer | Keeps MQTT/WiFi commissioning, bridge control, radio essentials, update commands, basic identity/status commands, `neighbors`, `discover.neighbors`, `outpath`, and `altpath`. The large repeater administration tree is omitted to fit the legacy ESP32 application slot. |
|
||||
| Portable ESP-NOW bridge | Keeps the repeater's role-specific handlers and a reduced common configuration surface containing radio and bridge essentials, including `rxdelay`, `txdelay`, `outpath`, and `altpath`. |
|
||||
| FULL ESP32 | Uses the matching MQTT target with logging off, removes size-based CLI cuts, and restores the complete command surface supported by that role and hardware. |
|
||||
| FULL ESP32 logging | Uses the matching non-MQTT target with debug and packet logging enabled and the complete command surface supported by that role and hardware. |
|
||||
| Standard logging | Logging does not remove commands by itself. It has the same CLI as the selected role/profile, adds the compiled logging behavior, and provides session-only `get/set usb.logging` control. |
|
||||
| LoRa-OTA (`-ota-`) | LoRa OTA adds the `ota ...` commands; it does not otherwise reduce the role CLI. ESP32 `no_external_sensors` artifacts retain the compact browser WiFi uploader, the complete CLI, and a 254-entry neighbor table. |
|
||||
| ESP32 MQTT observer or ESP-NOW bridge | Always uses the expanded FULL partition profile. The build never substitutes a reduced CLI to fit the legacy application slot. |
|
||||
| FULL ESP32 | Uses the matching MQTT target with logging off and keeps the complete command surface supported by that role and hardware. |
|
||||
| FULL ESP32 logging | Uses the matching non-MQTT target with debug and packet logging enabled, session-only `get/set usb.logging` control, and the complete command surface supported by that role and hardware. |
|
||||
| `no_external_sensors` | Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands. |
|
||||
|
||||
`logging`, `OTA`, and `FULL` describe independent build features. Do not infer
|
||||
that a command is missing merely because `logging` appears in the filename.
|
||||
|
||||
## Portable MQTT observer retained surface
|
||||
## Complete CLI policy
|
||||
|
||||
The portable MQTT observer keeps these command groups:
|
||||
The compact ESP32 CLI has been removed. MQTT observers and ESP-NOW bridges are
|
||||
automatically promoted to FULL builds with expanded partitions rather than
|
||||
dropping administration commands. This keeps `tempradio`, LoRa OTA, power
|
||||
saving, RXPS, logging, statistics, sensor, ACL, routing, and advanced radio
|
||||
commands whenever the role and compiled hardware support them.
|
||||
|
||||
- lifecycle and identity: `reboot`, `poweroff`, `shutdown`, `ver`, `board`,
|
||||
`password`, and `erase` on the local console;
|
||||
- radio operation: `advert`, `advert.zerohop`, `clock`, `clock sync`, `time`,
|
||||
`memory`, `neighbors`, `discover.neighbors`, and the remote-client routing
|
||||
controls `outpath` and `altpath`;
|
||||
- browser/update control when compiled: `start ota`, `stop ota`, `ota check`,
|
||||
and `ota update`;
|
||||
- radio essentials through `get`/`set`: radio parameters, TX power, CAD,
|
||||
interference threshold, AGC reset interval, RX gain, `rxdelay`, `txdelay`,
|
||||
repeat state, and applicable FEM controls;
|
||||
- MQTT, WiFi, NTP, bridge, and alert commands implemented by the observer
|
||||
feature set;
|
||||
- region and onboard-GPS commands that fit and are compiled into the selected
|
||||
target.
|
||||
|
||||
Everything in the repeater-only administration tree that is not listed above is
|
||||
intentionally cut from this portable profile. The main omissions include ACL
|
||||
editing, flood filter/moderation/scope administration, advanced mesh-clock
|
||||
controls, recent-repeater/path administration, battery-alert/RX-watchdog
|
||||
controls, stored-log management, and external-sensor administration. Use the
|
||||
matching FULL ESP32 build when those commands are required.
|
||||
|
||||
Some observer commands have their own hardware limit:
|
||||
|
||||
- MQTT neighbor-table publishing and `discover.scopes` require the compiled
|
||||
`WITH_MQTT_NEIGHBORS` feature. PSRAM boards enable it automatically; selected
|
||||
non-PSRAM variants opt in with `MQTT_NEIGHBORS_WITHOUT_PSRAM`. The commands
|
||||
can therefore be present in either portable or FULL MQTT profiles.
|
||||
- `discover.neighbors` does **not** require MQTT or PSRAM.
|
||||
- full NTP connectivity diagnostics are omitted from the portable profile.
|
||||
Some commands still have hardware or feature limits. MQTT neighbor-table
|
||||
publishing and `discover.scopes` require the compiled `WITH_MQTT_NEIGHBORS`
|
||||
feature. PSRAM boards enable it automatically, and selected non-PSRAM variants
|
||||
opt in with `MQTT_NEIGHBORS_WITHOUT_PSRAM`. `discover.neighbors` does not
|
||||
require MQTT or PSRAM.
|
||||
|
||||
## Discovery invariant
|
||||
|
||||
`discover.neighbors` sends the zero-hop node-discovery request used to refresh
|
||||
the repeater neighbor table. It is available in every repeater build profile,
|
||||
including portable MQTT, standard, logging, OTA, FULL, and FULL logging builds.
|
||||
including standard, logging, OTA, FULL, and FULL logging builds.
|
||||
|
||||
The exact command is:
|
||||
|
||||
|
||||
+158
-159
@@ -17,11 +17,8 @@ Build columns mean:
|
||||
remove commands by itself.
|
||||
- **LoRa OTA** - the explicit `-ota-` repeater or repeater-bridge artifact. Its
|
||||
optional external-sensor drivers are removed, but onboard GPS is retained.
|
||||
- **MQTT portable** - the size-reduced ESP32 MQTT observer for the legacy
|
||||
application slot.
|
||||
- **ESP-NOW portable** - the size-reduced ESP32 ESP-NOW repeater bridge.
|
||||
- **FULL MQTT** - the expanded-partition ESP32 MQTT artifact with LoRa OTA and
|
||||
the complete parser.
|
||||
- **FULL** - the expanded-partition ESP32 artifact with LoRa OTA and the
|
||||
complete parser. MQTT observers and ESP-NOW bridges always use this profile.
|
||||
- **FULL logging** - the expanded-partition ESP32 non-MQTT artifact with
|
||||
logging, LoRa OTA, and the complete parser.
|
||||
|
||||
@@ -71,6 +68,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Statistics | [`get telemetry.temp/volt`; optional GPS history; `get/set telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS commands require a GPS provider; remote access requires administrator | Yes | Yes | Yes |
|
||||
| Logging | [`log start`; `log stop`; `log erase`](cli_commands.md#logging) | Storage-backed roles retain data; other roles can return empty data | Yes | Yes | Yes |
|
||||
| Logging | [`log`](cli_commands.md#print-the-captured-log-to-the-serial-terminal) | Local serial | Serial | Serial | Serial |
|
||||
| Logging | [`get/set usb.logging`](cli_commands.md#control-live-usb-logging) | Logging artifacts; session-only live USB output gate | No | Yes | No |
|
||||
| Radio | [`get radio`; `set radio ...`](cli_commands.md#view-or-change-this-nodes-radio-parameters) | All text CLI roles | Yes | Yes | Yes |
|
||||
| Radio | [`get tx`; `set tx <dbm>`](cli_commands.md#view-or-change-this-nodes-transmit-power) | Board TX-power limits apply | Yes | Yes | Yes |
|
||||
| Radio | [`tempradio ...`](cli_commands.md#change-the-radio-parameters-for-a-set-duration) | Full parser | Yes | Yes | Yes |
|
||||
@@ -199,160 +197,161 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
|
||||
## ESP32
|
||||
|
||||
| Area | Command | Scope | Standard | Logging | LoRa OTA | MQTT portable | ESP-NOW portable | FULL MQTT | FULL logging |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| Operational | [`reboot`](cli_commands.md#reboot-the-node) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`poweroff`; `shutdown`](cli_commands.md#power-off-the-node) | Board power-off support | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`uf2reset`](cli_commands.md#enter-the-uf2-bootloader-nrf52-only) | nRF52 only | No | No | No | No | No | No | No |
|
||||
| Operational | [`clkreboot`](cli_commands.md#reset-the-clock-and-reboot) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Operational | [`clock sync`](cli_commands.md#sync-the-clock-with-the-remote-device) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`clock`](cli_commands.md#display-current-time-in-utc) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`time <epoch_seconds>`](cli_commands.md#set-the-time-to-a-specific-timestamp) | Clock only moves forward | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`advert`](cli_commands.md#send-a-flood-advert) | Advert-capable role | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`advert.zerohop`](cli_commands.md#send-a-zero-hop-advert) | Advert-capable role | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`erase`](cli_commands.md#erasefactory-reset) | Local serial | Serial | Serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| Information | [`ver`](cli_commands.md#get-the-version) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Information | [`board`](cli_commands.md#show-the-hardware-name) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Diagnostics | [`memory`](#memory) | ESP32 heap and PSRAM summary | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Diagnostics | [`sensor`](#sensor-hardware-summary) | Hardware wiring summary | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Diagnostics | [`powerlog`](#powerlog) | Reset-reason summary | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`neighbors`](cli_commands.md#list-nearby-neighbors) | Role with a neighbor table | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`neighbor.remove <pubkey_prefix>`](cli_commands.md#remove-a-neighbor) | Role with a mutable neighbor table | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Neighbors | [`discover.neighbors`](cli_commands.md#discover-zero-hop-neighbors) | Repeater; some MQTT room servers | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`discover.scopes`](cli_commands.md#discover-neighbor-scopes-mqtt-observer-neighbors-feature) | MQTT observer with compiled neighbor support | No | No | No | Feature | No | Feature | No |
|
||||
| Statistics | [`clear stats`](cli_commands.md#clear-stats) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Statistics | [`stats-core`](cli_commands.md#stats-core) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Statistics | [`stats-radio`](cli_commands.md#stats-radio) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Statistics | [`stats-radio-diag`](#stats-radio-diag) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Statistics | [`stats-packets`](cli_commands.md#stats-packets) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Statistics | [`get telemetry.temp/volt`; optional GPS history; `get/set telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS commands require a GPS provider; remote access requires administrator | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Logging | [`log start`; `log stop`; `log erase`](cli_commands.md#logging) | Storage-backed roles retain data | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Logging | [`log`](cli_commands.md#print-the-captured-log-to-the-serial-terminal) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Radio | [`get radio`; `set radio ...`](cli_commands.md#view-or-change-this-nodes-radio-parameters) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`get tx`; `set tx <dbm>`](cli_commands.md#view-or-change-this-nodes-transmit-power) | Board TX-power limits apply | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`tempradio ...`](cli_commands.md#change-the-radio-parameters-for-a-set-duration) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Radio | [`get/set/del radioat`; `get/set/del tempradioat`](cli_commands.md#schedule-radio-parameter-changes) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Radio | [`get freq`; `set freq <mhz>`](cli_commands.md#view-or-change-this-nodes-frequency) | `set` is local serial only | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`get/set radio.rxgain`](cli_commands.md#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141) | Supported radio | Feature | Feature | Feature | Feature | Feature | Feature | Feature |
|
||||
| Radio | [`get/set radio.fem.rxgain`](cli_commands.md#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards) | Controllable LoRa FEM | Feature | Feature | Feature | Feature | Feature | Feature | Feature |
|
||||
| Radio | [`get/set radio.fem.txgain`](cli_commands.md#view-or-change-the-lora-fem-transmit-path-gain-state-on-supported-boards) | Controllable LoRa FEM | Feature | Feature | Feature | Feature | Feature | Feature | Feature |
|
||||
| Radio | [`get/set radio.rxps`; `get rxps.wd`](#radio-rxps) | Full parser and RX power-saving support | Feature | Feature | Feature | No | No | Feature | Feature |
|
||||
| System | [`get/set name`](cli_commands.md#view-or-change-this-nodes-name) | All full-parser text CLI roles | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| System | [`get/set lat`](cli_commands.md#view-or-change-this-nodes-latitude) | All full-parser text CLI roles | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| System | [`get/set lon`](cli_commands.md#view-or-change-this-nodes-longitude) | All full-parser text CLI roles | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| System | [`get/set prv.key`](cli_commands.md#view-or-change-this-nodes-identity-private-key) | `get` is local serial only | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| System | [`password <new_password>`](cli_commands.md#change-this-nodes-admin-password) | Administrator | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set guest.password`](cli_commands.md#view-or-change-this-nodes-guest-password) | Role with guest administration | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| System | [`get/set owner.info`](cli_commands.md#view-or-change-this-nodes-owner-info) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| System | [`get/set adc.multiplier`](cli_commands.md#fine-tune-the-battery-reading) | Board ADC override support | Feature | Feature | Feature | No | No | Feature | Feature |
|
||||
| System | [`send text.flood <message>`](cli_commands.md#send-a-repeater-flood-text) | Repeater | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| System | [`get/set battery.alert`; `get battery.alert.region`](cli_commands.md#view-or-change-battery-alert-state) | Repeater | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| System | [`get/set battery.alert.low`; `get/set battery.alert.critical`](cli_commands.md#view-or-change-battery-alert-thresholds) | Repeater | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| System | [`get/set rx.watchdog`](cli_commands.md#enable-or-disable-the-rx-inactivity-watchdog-repeater-only) | Repeater | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| System | [`get/set system.watchdog`](cli_commands.md#enable-or-disable-the-nrf52-system-watchdog) | nRF52 only | No | No | No | No | No | No | No |
|
||||
| System | [`get public.key`](cli_commands.md#view-this-nodes-public-key) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| System | [`get role`](cli_commands.md#view-this-nodes-configured-role) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`powersaving`; `powersaving on/off`](cli_commands.md#view-or-change-this-nodes-power-saving-flag) | Supported repeater board | Feature | Feature | Feature | No | No | Feature | Feature |
|
||||
| System | [`get/set reboot.interval`](#reboot-interval) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Clock sync | [`get/set clock.sync.*`; `clock.sync.mesh now`](cli_commands.md#estimate-and-correct-infrastructure-node-time-after-startup) | Repeater, sensor, and room server; `clock.sync.internet` needs MQTT repeater | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set repeat`](cli_commands.md#view-or-change-this-nodes-repeat-flag) | Forwarding-capable role | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set path.hash.mode`](cli_commands.md#view-or-change-this-nodes-advert-path-hash-size) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set loop.detect`](cli_commands.md#view-or-change-this-nodes-loop-detection) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set txdelay`](cli_commands.md#view-or-change-the-retransmit-delay-factor-for-flood-traffic) | All text CLI roles, including bridges | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set direct.txdelay`](cli_commands.md#view-or-change-the-retransmit-delay-factor-for-direct-traffic) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set rxdelay`](cli_commands.md#experimental-view-or-change-the-processing-delay-for-received-traffic) | All text CLI roles, including bridges | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set dutycycle`](cli_commands.md#view-or-change-the-duty-cycle-limit) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set af`](cli_commands.md#view-or-change-the-airtime-factor-duty-cycle-limit) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set int.thresh`](cli_commands.md#view-or-change-the-local-interference-threshold) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set cad`](cli_commands.md#enable-or-disable-hardware-channel-activity-detection-cad) | Radio CAD support | Feature | Feature | Feature | Feature | Feature | Feature | Feature |
|
||||
| Routing | [`get/set agc.reset.interval`](cli_commands.md#view-or-change-the-agc-reset-interval) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set radio.watchdog`](cli_commands.md#view-or-change-the-radio-watchdog-interval-mqtt-observer-only) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| Routing | [`get/set multi.acks`](cli_commands.md#enable-or-disable-multi-acks-support) | Full parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set flood.advert.interval`](cli_commands.md#view-or-change-the-flood-advert-interval) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set advert.interval`](cli_commands.md#view-or-change-the-zero-hop-advert-interval) | Full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set flood.max`](cli_commands.md#limit-the-number-of-hops-for-a-flood-message) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set flood.max.unscoped`](cli_commands.md#limit-the-number-of-hops-for-an-unscoped-flood-message) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set flood.max.advert`](cli_commands.md#limit-the-number-of-hops-for-an-advert-flood-message) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set flood.channel.data`; `get/set flood.channel.data.hops`](cli_commands.md#forward-flood-group-data-packets-on-repeaters) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.scope*`](cli_commands.md#force-a-transport-scope-onto-floods) | Repeater role handler | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.scope.require*`](cli_commands.md#require-valid-incoming-scopes-only-on-selected-channels) | Repeater role handler | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.rule*`; `get/set/del flood.filter*`; `get/set/del flood.filter.blacklist*`](cli_commands.md#change-persistent-flood-rules-in-the-field) | Repeater role handler; `flood.rule`/`flood.filter` also on FULL ESP32 room server (no blacklist) | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.moderation*`](cli_commands.md#moderate-flood-group-text-by-channel-sender-and-source-path) | Repeater role handler | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set outpath`](halo_keymind_settings.md#direct-path-overrides) | Repeater remote-client context | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set altpath`](halo_keymind_settings.md#direct-path-overrides) | Repeater remote-client context | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| ACL | [`setperm <pubkey> <permissions>`](cli_commands.md#add-update-or-remove-permissions-for-a-companion) | Repeater, room server, or sensor | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| ACL | [`get acl`](cli_commands.md#view-the-current-acl) | Local serial | Serial | Serial | Serial | No | Serial | Serial | Serial |
|
||||
| ACL | [`get/set allow.read.only`](cli_commands.md#view-or-change-this-room-servers-read-only-flag) | Room server | Yes | Yes | No | No | No | Yes | Yes |
|
||||
| Regions | [`region load`](cli_commands.md#bulk-load-region-lists); [`region save`](cli_commands.md#save-any-changes-to-regions-made-since-reboot) | Role with region storage | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Regions | [`region allowf`](cli_commands.md#allow-a-region); [`region denyf`](cli_commands.md#block-a-region) | Role with region storage | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Regions | [`region get`](cli_commands.md#show-information-for-a-region); [`region list`](cli_commands.md#view-all-regions); [`region`](cli_commands.md#dump-all-defined-regions-and-flood-permissions) | Role with region storage | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Regions | [`region home`](cli_commands.md#view-or-change-the-home-region-for-this-node); [`region default`](cli_commands.md#view-or-change-the-default-scope-region-for-this-node) | Role with region storage | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Regions | [`region put`](cli_commands.md#create-a-new-region); [`region def`](cli_commands.md#define-region-hierarchy-single-line); [`region remove`](cli_commands.md#remove-a-region) | Role with region storage | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Retry | [`get/set direct.retry`](cli_commands.md#view-or-change-direct-retry-state); [`get/set direct.retry.heard`](cli_commands.md#view-or-change-direct-retry-heard-table-gate) | Role with basic retry support | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Retry | [`get/set retry.preset`](cli_commands.md#view-or-apply-a-retry-preset) | Role with retry support | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Retry | [`get/set flood.retry.count`](cli_commands.md#view-or-change-flood-retry-count); [`get/set flood.retry.path`](cli_commands.md#view-or-change-flood-retry-path-gate); [`get/set flood.retry.group.path`](cli_commands.md#view-or-change-the-group-data-flood-retry-path-gate) | Repeater | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Retry | [`get/set flood.retry.advert`](cli_commands.md#view-or-change-flood-retry-advert-handling); [`get/set flood.retry.prefixes`](cli_commands.md#view-or-change-flood-retry-target-prefixes); [`get/set flood.retry.ignore`](cli_commands.md#view-or-change-flood-retry-ignored-prefixes) | Repeater | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Retry | [`get/set flood.retry.bridge`](cli_commands.md#view-or-change-flood-retry-bridge-mode); [`get/set flood.retry.bucket`](cli_commands.md#view-or-change-flood-retry-bridge-buckets) | Repeater bridge retry support | Feature | Feature | Feature | No | No | Feature | Feature |
|
||||
| Retry | [`get/set direct.retry.count`](cli_commands.md#view-or-change-direct-retry-count); [`get/set direct.retry.base`](cli_commands.md#view-or-change-direct-retry-base-delay); [`get/set direct.retry.step`](cli_commands.md#view-or-change-direct-retry-step-delay) | Role with retry support | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Retry | [`get/set direct.retry.margin`](cli_commands.md#view-or-change-direct-retry-snr-margin); [`get/set direct.retry.cr`](cli_commands.md#view-or-change-adaptive-direct-retry-coding-rate) | Role with retry support | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Retry | [`get/set/clear recent.repeater`; `get recent.repeaters`](cli_commands.md#view-seed-or-clear-the-recent-repeater-table) | Repeater; portable ESP-NOW retains reads only | Yes | Yes | Yes | No | Limited | Yes | Yes |
|
||||
| GPS | [`gps`; `gps on/off`](cli_commands.md#view-or-change-gps-state) | Compiled onboard GPS | Feature | Feature | Feature | Feature | Feature | Feature | Feature |
|
||||
| GPS | [`gps sync`](cli_commands.md#sync-this-nodes-clock-with-gps-time) | Compiled onboard GPS | Feature | Feature | Feature | Feature | Feature | Feature | Feature |
|
||||
| GPS | [`gps setloc`](cli_commands.md#set-this-nodes-location-based-on-the-gps-coordinates) | Compiled onboard GPS | Feature | Feature | Feature | Feature | Feature | Feature | Feature |
|
||||
| GPS | [`gps advert [none/share/prefs]`](cli_commands.md#view-or-change-the-gps-advert-policy) | Compiled onboard GPS | Feature | Feature | Feature | Feature | Feature | Feature | Feature |
|
||||
| Sensors | [`get/set telemetry.access`](cli_commands.md#view-or-change-telemetry-access-mode) | Sensor-capable full parser | Feature | Feature | Limited | No | No | Feature | Feature |
|
||||
| Sensors | [`sensor list [start]`](cli_commands.md#view-the-list-of-sensors-on-this-node) | Compiled sensor manager; portable profiles omit external sensors | Feature | Feature | Limited | No | No | Feature | Feature |
|
||||
| Sensors | [`sensor get`; `sensor set`](cli_commands.md#view-or-change-the-value-of-a-sensor) | Compiled sensor setting; portable profiles omit external sensors | Feature | Feature | Limited | No | No | Feature | Feature |
|
||||
| Sensors | [`io [r/s/t]<hex>`](#sensor-io) | Simple sensor role with board GPIO support | Feature | Feature | No | No | No | No | Feature |
|
||||
| Bridge | [`get bridge.type`](cli_commands.md#view-the-compiled-bridge-type) | Compiled bridge | Feature | Feature | Feature | Yes | Yes | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.enabled`](cli_commands.md#view-or-change-the-bridge-enabled-flag) | Compiled bridge | Feature | Feature | Feature | Yes | Yes | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.delay`](cli_commands.md#add-a-delay-to-packets-routed-through-this-bridge) | Compiled bridge | Feature | Feature | Feature | Yes | Yes | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.source`](cli_commands.md#view-or-change-the-source-of-packets-bridged-to-the-external-interface) | Compiled bridge | Feature | Feature | Feature | Yes | Yes | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.baud`](cli_commands.md#view-or-change-the-speed-of-the-bridge-rs-232-only) | RS-232 bridge | Feature | Feature | Feature | No | No | No | Feature |
|
||||
| Bridge | [`get/set bridge.channel`](cli_commands.md#view-or-change-the-channel-used-for-bridging-espnow-only) | ESP-NOW bridge | No | No | No | No | Yes | No | Feature |
|
||||
| Bridge | [`get/set bridge.secret`](cli_commands.md#set-the-esp-now-secret) | ESP-NOW bridge | No | No | No | No | Yes | No | Feature |
|
||||
| Board | [`get bootloader.ver`](cli_commands.md#view-the-bootloader-version-nrf52-only) | nRF52 only | No | No | No | No | No | No | No |
|
||||
| Board | [`get pwrmgt.support`; `get pwrmgt.source`; `get pwrmgt.bootreason`; `get pwrmgt.bootmv`](nrf52_power_management.md#cli-commands) | nRF52 only | No | No | No | No | No | No | No |
|
||||
| Ethernet | [`eth.status`](cli_commands.md#view-ethernet-connection-status) | Ethernet target | Feature | Feature | No | No | No | No | Feature |
|
||||
| Browser OTA | [`start ota [ap]`; `stop ota`](cli_commands.md#start-or-stop-an-over-the-air-ota-firmware-update) | Compiled browser uploader | No | No | Yes | Yes | No | Feature | Feature |
|
||||
| WebConfig | [`start webconfig [ap]`; `stop webconfig`; `get/set webui`](cli_commands.md#browser-configuration-portal-esp32-repeater-and-room-server) | Compiled WebConfig | No | No | No | No | No | Feature | Feature |
|
||||
| WiFi | [`get/set wifi.ssid`; `set wifi.pwd`; `get wifi.status`; `get/set wifi.powersave`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#wifi-commands) | MQTT WiFi, or standalone FULL WebConfig; standalone has no `get wifi.pwd` | No | No | No | Yes | No | Yes | Feature |
|
||||
| WiFi | [`get/set wifi.cli`](cli_commands.md#browser-configuration-portal-esp32-repeater-and-room-server) | Compiled WebConfig | No | No | No | No | No | Feature | Feature |
|
||||
| LoRa OTA | [`ota help`; `ota ?`; `ota h`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota`; `ota status`; `ota st`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota stats`](ota_user_guide.md#1-see-what-im-running-and-whether-anything-is-going-on) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota ls`; `ota neighbors`; `ota nbrs`; `ota updates`; `ota n`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota get`; `ota pull`; `ota download`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota install`; `ota apply`; `ota applydelta`](ota_protocol.md#11-cli-surface-otaclicpp) | Compatible completed update | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota rescue install <base_hash16>`](ota_protocol.md#12-apply-bootloader-contract) | Internal-flash nRF52 LoRa OTA build with failed app-side EndF validation | No | No | Feature | No | No | No | No |
|
||||
| LoRa OTA | [`ota cancel`; `ota drop`; `ota stop`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota announce`; `ota adv`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota self`; `ota id`](ota_protocol.md#11-cli-surface-otaclicpp) | Firmware with EndF trailer | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota folder`; `ota fold`](ota_protocol.md#11-cli-surface-otaclicpp) | `on` needs compiled serial or TCP folder transport | No | No | Feature | No | No | Feature | Feature |
|
||||
| LoRa OTA | [`ota config`; `ota cfg`; `ota set`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota key`; `ota keys`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota dev ...`](ota_protocol.md#11-cli-surface-otaclicpp) | Developer diagnostics | No | No | Yes | No | No | Yes | Yes |
|
||||
| MQTT | [`get/set mqttN.preset`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-slot-commands) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get/set mqttN.server`; `get/set mqttN.port`; `get/set mqttN.username`; `get/set mqttN.password`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-slot-commands) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get/set mqttN.token`; `get/set mqttN.topic`; `get/set mqttN.audience`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-slot-commands) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get mqttN.diag`](#mqtt-slot-diagnostics) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.origin`; `get/set mqtt.iata`; `get mqtt.presets`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-shared-commands) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get mqtt.stats`](#mqtt-stats) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.status`; `get/set mqtt.packets`; `get/set mqtt.raw`; `get/set mqtt.interval`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-shared-commands) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.rx`](cli_commands.md#view-or-change-mqtt-rx-packet-uplinking); [`get/set mqtt.tx`](cli_commands.md#view-or-change-mqtt-tx-packet-uplinking) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.neighbors`](cli_commands.md#view-or-change-periodic-neighbors-publishing-mqtt-observer-neighbors-feature); [`get/set mqtt.neighbors.interval`](cli_commands.md#view-or-change-the-neighbors-publish-interval-mqtt-observer-neighbors-feature) | MQTT observer with compiled neighbor support | No | No | No | Feature | No | Feature | No |
|
||||
| MQTT | [`get/set mqtt.ntp`](cli_commands.md#view-or-change-the-ntp-server-mqtt-observer-only) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get mqtt.ntp.diag`](cli_commands.md#diagnose-ntp-server-connectivity-mqtt-observer-only) | Full MQTT observer; intentionally cut from portable | No | No | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set timezone`; `get/set timezone.offset`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#timezone-commands) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.analyzer.us`; `get/set mqtt.analyzer.eu`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#migration-from-old-configuration) | Legacy MQTT aliases | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.owner`; `get/set mqtt.email`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-shared-commands) | MQTT observer; `get` is local serial only | No | No | No | Yes | No | Yes | No |
|
||||
| MQTT | [`get mqtt.config.valid`](#mqtt-config-valid) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| SNMP | [`get/set snmp`; `get/set snmp.community`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_SNMP.md#cli-commands) | MQTT target compiled with SNMP | No | No | No | No | No | Feature | No |
|
||||
| Alerts | [`get/set alert`; `get/set alert.psk`; `get/set alert.hashtag`; `get/set alert.region`; `get/set alert.wifi`; `get/set alert.mqtt`; `get/set alert.interval`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/ALERTS.md#cli) | MQTT observer | No | No | No | Yes | No | Yes | No |
|
||||
| Alerts | [`alert test [message]`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/ALERTS.md#cli) | MQTT observer with configured alert channel | No | No | No | Yes | No | Yes | No |
|
||||
| TLS | [`tls.bundletest <host>`](#tls-bundle-test) | MQTT target with embedded certificate bundle | No | No | No | Feature | No | Feature | No |
|
||||
| Manifest OTA | [`ota check`](#manifest-ota) | MQTT target with `OTA_MANIFEST_BASE` | No | No | No | Manifest | No | Manifest | No |
|
||||
| Manifest OTA | [`ota update`](#manifest-ota) | MQTT target with `OTA_MANIFEST_BASE` | No | No | No | Manifest | No | Manifest | No |
|
||||
| Area | Command | Scope | Standard | Logging | LoRa OTA | FULL | FULL logging |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Operational | [`reboot`](cli_commands.md#reboot-the-node) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`poweroff`; `shutdown`](cli_commands.md#power-off-the-node) | Board power-off support | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`uf2reset`](cli_commands.md#enter-the-uf2-bootloader-nrf52-only) | nRF52 only | No | No | No | No | No |
|
||||
| Operational | [`clkreboot`](cli_commands.md#reset-the-clock-and-reboot) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`clock sync`](cli_commands.md#sync-the-clock-with-the-remote-device) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`clock`](cli_commands.md#display-current-time-in-utc) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`time <epoch_seconds>`](cli_commands.md#set-the-time-to-a-specific-timestamp) | Clock only moves forward | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`advert`](cli_commands.md#send-a-flood-advert) | Advert-capable role | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`advert.zerohop`](cli_commands.md#send-a-zero-hop-advert) | Advert-capable role | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`erase`](cli_commands.md#erasefactory-reset) | Local serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| Information | [`ver`](cli_commands.md#get-the-version) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Information | [`board`](cli_commands.md#show-the-hardware-name) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Diagnostics | [`memory`](#memory) | ESP32 heap and PSRAM summary | Yes | Yes | Yes | Yes | Yes |
|
||||
| Diagnostics | [`sensor`](#sensor-hardware-summary) | Hardware wiring summary | Yes | Yes | Yes | Yes | Yes |
|
||||
| Diagnostics | [`powerlog`](#powerlog) | Reset-reason summary | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`neighbors`](cli_commands.md#list-nearby-neighbors) | Role with a neighbor table | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`neighbor.remove <pubkey_prefix>`](cli_commands.md#remove-a-neighbor) | Role with a mutable neighbor table | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`discover.neighbors`](cli_commands.md#discover-zero-hop-neighbors) | Repeater; some MQTT room servers | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`discover.scopes`](cli_commands.md#discover-neighbor-scopes-mqtt-observer-neighbors-feature) | MQTT observer with compiled neighbor support | No | No | No | Feature | No |
|
||||
| Statistics | [`clear stats`](cli_commands.md#clear-stats) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Statistics | [`stats-core`](cli_commands.md#stats-core) | Local serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| Statistics | [`stats-radio`](cli_commands.md#stats-radio) | Local serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| Statistics | [`stats-radio-diag`](#stats-radio-diag) | Local serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| Statistics | [`stats-packets`](cli_commands.md#stats-packets) | Local serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| Statistics | [`get telemetry.temp/volt`; optional GPS history; `get/set telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS commands require a GPS provider; remote access requires administrator | Yes | Yes | Yes | Yes | Yes |
|
||||
| Logging | [`log start`; `log stop`; `log erase`](cli_commands.md#logging) | Storage-backed roles retain data | Yes | Yes | Yes | Yes | Yes |
|
||||
| Logging | [`log`](cli_commands.md#print-the-captured-log-to-the-serial-terminal) | Local serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| Logging | [`get/set usb.logging`](cli_commands.md#control-live-usb-logging) | Logging artifacts; session-only live USB output gate | No | Yes | No | No | Yes |
|
||||
| Radio | [`get radio`; `set radio ...`](cli_commands.md#view-or-change-this-nodes-radio-parameters) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`get tx`; `set tx <dbm>`](cli_commands.md#view-or-change-this-nodes-transmit-power) | Board TX-power limits apply | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`tempradio ...`](cli_commands.md#change-the-radio-parameters-for-a-set-duration) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`get/set/del radioat`; `get/set/del tempradioat`](cli_commands.md#schedule-radio-parameter-changes) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`get freq`; `set freq <mhz>`](cli_commands.md#view-or-change-this-nodes-frequency) | `set` is local serial only | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`get/set radio.rxgain`](cli_commands.md#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141) | Supported radio | Feature | Feature | Feature | Feature | Feature |
|
||||
| Radio | [`get/set radio.fem.rxgain`](cli_commands.md#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards) | Controllable LoRa FEM | Feature | Feature | Feature | Feature | Feature |
|
||||
| Radio | [`get/set radio.fem.txgain`](cli_commands.md#view-or-change-the-lora-fem-transmit-path-gain-state-on-supported-boards) | Controllable LoRa FEM | Feature | Feature | Feature | Feature | Feature |
|
||||
| Radio | [`get/set radio.rxps`; `get rxps.wd`](#radio-rxps) | Full parser and RX power-saving support | Feature | Feature | Feature | Feature | Feature |
|
||||
| System | [`get/set name`](cli_commands.md#view-or-change-this-nodes-name) | All full-parser text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set lat`](cli_commands.md#view-or-change-this-nodes-latitude) | All full-parser text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set lon`](cli_commands.md#view-or-change-this-nodes-longitude) | All full-parser text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set prv.key`](cli_commands.md#view-or-change-this-nodes-identity-private-key) | `get` is local serial only | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`password <new_password>`](cli_commands.md#change-this-nodes-admin-password) | Administrator | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set guest.password`](cli_commands.md#view-or-change-this-nodes-guest-password) | Role with guest administration | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set owner.info`](cli_commands.md#view-or-change-this-nodes-owner-info) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set adc.multiplier`](cli_commands.md#fine-tune-the-battery-reading) | Board ADC override support | Feature | Feature | Feature | Feature | Feature |
|
||||
| System | [`send text.flood <message>`](cli_commands.md#send-a-repeater-flood-text) | Repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set battery.alert`; `get battery.alert.region`](cli_commands.md#view-or-change-battery-alert-state) | Repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set battery.alert.low`; `get/set battery.alert.critical`](cli_commands.md#view-or-change-battery-alert-thresholds) | Repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set rx.watchdog`](cli_commands.md#enable-or-disable-the-rx-inactivity-watchdog-repeater-only) | Repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get/set system.watchdog`](cli_commands.md#enable-or-disable-the-nrf52-system-watchdog) | nRF52 only | No | No | No | No | No |
|
||||
| System | [`get public.key`](cli_commands.md#view-this-nodes-public-key) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`get role`](cli_commands.md#view-this-nodes-configured-role) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| System | [`powersaving`; `powersaving on/off`](cli_commands.md#view-or-change-this-nodes-power-saving-flag) | Supported repeater board | Feature | Feature | Feature | Feature | Feature |
|
||||
| System | [`get/set reboot.interval`](#reboot-interval) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Clock sync | [`get/set clock.sync.*`; `clock.sync.mesh now`](cli_commands.md#estimate-and-correct-infrastructure-node-time-after-startup) | Repeater, sensor, and room server; `clock.sync.internet` needs MQTT repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set repeat`](cli_commands.md#view-or-change-this-nodes-repeat-flag) | Forwarding-capable role | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set path.hash.mode`](cli_commands.md#view-or-change-this-nodes-advert-path-hash-size) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set loop.detect`](cli_commands.md#view-or-change-this-nodes-loop-detection) | Repeater, full common parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set txdelay`](cli_commands.md#view-or-change-the-retransmit-delay-factor-for-flood-traffic) | All text CLI roles, including bridges | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set direct.txdelay`](cli_commands.md#view-or-change-the-retransmit-delay-factor-for-direct-traffic) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set rxdelay`](cli_commands.md#experimental-view-or-change-the-processing-delay-for-received-traffic) | All text CLI roles, including bridges | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set dutycycle`](cli_commands.md#view-or-change-the-duty-cycle-limit) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set af`](cli_commands.md#view-or-change-the-airtime-factor-duty-cycle-limit) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set int.thresh`](cli_commands.md#view-or-change-the-local-interference-threshold) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set cad`](cli_commands.md#enable-or-disable-hardware-channel-activity-detection-cad) | Radio CAD support | Feature | Feature | Feature | Feature | Feature |
|
||||
| Routing | [`get/set agc.reset.interval`](cli_commands.md#view-or-change-the-agc-reset-interval) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set radio.watchdog`](cli_commands.md#view-or-change-the-radio-watchdog-interval-mqtt-observer-only) | MQTT observer | No | No | No | Yes | No |
|
||||
| Routing | [`get/set multi.acks`](cli_commands.md#enable-or-disable-multi-acks-support) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set flood.advert.interval`](cli_commands.md#view-or-change-the-flood-advert-interval) | Repeater, full common parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set advert.interval`](cli_commands.md#view-or-change-the-zero-hop-advert-interval) | Full common parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set flood.max`](cli_commands.md#limit-the-number-of-hops-for-a-flood-message) | Repeater, full common parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set flood.max.unscoped`](cli_commands.md#limit-the-number-of-hops-for-an-unscoped-flood-message) | Repeater, full common parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set flood.max.advert`](cli_commands.md#limit-the-number-of-hops-for-an-advert-flood-message) | Repeater, full common parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set flood.channel.data`; `get/set flood.channel.data.hops`](cli_commands.md#forward-flood-group-data-packets-on-repeaters) | Repeater, full common parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.scope*`](cli_commands.md#force-a-transport-scope-onto-floods) | Repeater role handler | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.scope.require*`](cli_commands.md#require-valid-incoming-scopes-only-on-selected-channels) | Repeater role handler | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.rule*`; `get/set/del flood.filter*`; `get/set/del flood.filter.blacklist*`](cli_commands.md#change-persistent-flood-rules-in-the-field) | Repeater role handler; `flood.rule`/`flood.filter` also on FULL ESP32 room server (no blacklist) | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.moderation*`](cli_commands.md#moderate-flood-group-text-by-channel-sender-and-source-path) | Repeater role handler | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set outpath`](halo_keymind_settings.md#direct-path-overrides) | Repeater remote-client context | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set altpath`](halo_keymind_settings.md#direct-path-overrides) | Repeater remote-client context | Yes | Yes | Yes | Yes | Yes |
|
||||
| ACL | [`setperm <pubkey> <permissions>`](cli_commands.md#add-update-or-remove-permissions-for-a-companion) | Repeater, room server, or sensor | Yes | Yes | Yes | Yes | Yes |
|
||||
| ACL | [`get acl`](cli_commands.md#view-the-current-acl) | Local serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| ACL | [`get/set allow.read.only`](cli_commands.md#view-or-change-this-room-servers-read-only-flag) | Room server | Yes | Yes | No | Yes | Yes |
|
||||
| Regions | [`region load`](cli_commands.md#bulk-load-region-lists); [`region save`](cli_commands.md#save-any-changes-to-regions-made-since-reboot) | Role with region storage | Yes | Yes | Yes | Yes | Yes |
|
||||
| Regions | [`region allowf`](cli_commands.md#allow-a-region); [`region denyf`](cli_commands.md#block-a-region) | Role with region storage | Yes | Yes | Yes | Yes | Yes |
|
||||
| Regions | [`region get`](cli_commands.md#show-information-for-a-region); [`region list`](cli_commands.md#view-all-regions); [`region`](cli_commands.md#dump-all-defined-regions-and-flood-permissions) | Role with region storage | Yes | Yes | Yes | Yes | Yes |
|
||||
| Regions | [`region home`](cli_commands.md#view-or-change-the-home-region-for-this-node); [`region default`](cli_commands.md#view-or-change-the-default-scope-region-for-this-node) | Role with region storage | Yes | Yes | Yes | Yes | Yes |
|
||||
| Regions | [`region put`](cli_commands.md#create-a-new-region); [`region def`](cli_commands.md#define-region-hierarchy-single-line); [`region remove`](cli_commands.md#remove-a-region) | Role with region storage | Yes | Yes | Yes | Yes | Yes |
|
||||
| Retry | [`get/set direct.retry`](cli_commands.md#view-or-change-direct-retry-state); [`get/set direct.retry.heard`](cli_commands.md#view-or-change-direct-retry-heard-table-gate) | Role with basic retry support | Yes | Yes | Yes | Yes | Yes |
|
||||
| Retry | [`get/set retry.preset`](cli_commands.md#view-or-apply-a-retry-preset) | Role with retry support | Yes | Yes | Yes | Yes | Yes |
|
||||
| Retry | [`get/set flood.retry.count`](cli_commands.md#view-or-change-flood-retry-count); [`get/set flood.retry.path`](cli_commands.md#view-or-change-flood-retry-path-gate); [`get/set flood.retry.group.path`](cli_commands.md#view-or-change-the-group-data-flood-retry-path-gate) | Repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| Retry | [`get/set flood.retry.advert`](cli_commands.md#view-or-change-flood-retry-advert-handling); [`get/set flood.retry.prefixes`](cli_commands.md#view-or-change-flood-retry-target-prefixes); [`get/set flood.retry.ignore`](cli_commands.md#view-or-change-flood-retry-ignored-prefixes) | Repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| Retry | [`get/set flood.retry.bridge`](cli_commands.md#view-or-change-flood-retry-bridge-mode); [`get/set flood.retry.bucket`](cli_commands.md#view-or-change-flood-retry-bridge-buckets) | Repeater bridge retry support | Feature | Feature | Feature | Feature | Feature |
|
||||
| Retry | [`get/set direct.retry.count`](cli_commands.md#view-or-change-direct-retry-count); [`get/set direct.retry.base`](cli_commands.md#view-or-change-direct-retry-base-delay); [`get/set direct.retry.step`](cli_commands.md#view-or-change-direct-retry-step-delay) | Role with retry support | Yes | Yes | Yes | Yes | Yes |
|
||||
| Retry | [`get/set direct.retry.margin`](cli_commands.md#view-or-change-direct-retry-snr-margin); [`get/set direct.retry.cr`](cli_commands.md#view-or-change-adaptive-direct-retry-coding-rate) | Role with retry support | Yes | Yes | Yes | Yes | Yes |
|
||||
| Retry | [`get/set/clear recent.repeater`; `get recent.repeaters`](cli_commands.md#view-seed-or-clear-the-recent-repeater-table) | Repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| GPS | [`gps`; `gps on/off`](cli_commands.md#view-or-change-gps-state) | Compiled onboard GPS | Feature | Feature | Feature | Feature | Feature |
|
||||
| GPS | [`gps sync`](cli_commands.md#sync-this-nodes-clock-with-gps-time) | Compiled onboard GPS | Feature | Feature | Feature | Feature | Feature |
|
||||
| GPS | [`gps setloc`](cli_commands.md#set-this-nodes-location-based-on-the-gps-coordinates) | Compiled onboard GPS | Feature | Feature | Feature | Feature | Feature |
|
||||
| GPS | [`gps advert [none/share/prefs]`](cli_commands.md#view-or-change-the-gps-advert-policy) | Compiled onboard GPS | Feature | Feature | Feature | Feature | Feature |
|
||||
| Sensors | [`get/set telemetry.access`](cli_commands.md#view-or-change-telemetry-access-mode) | Sensor-capable full parser | Feature | Feature | Limited | Feature | Feature |
|
||||
| Sensors | [`sensor list [start]`](cli_commands.md#view-the-list-of-sensors-on-this-node) | Compiled sensor manager; `no_external_sensors` omits optional drivers | Feature | Feature | Limited | Feature | Feature |
|
||||
| Sensors | [`sensor get`; `sensor set`](cli_commands.md#view-or-change-the-value-of-a-sensor) | Compiled sensor setting; `no_external_sensors` omits optional drivers | Feature | Feature | Limited | Feature | Feature |
|
||||
| Sensors | [`io [r/s/t]<hex>`](#sensor-io) | Simple sensor role with board GPIO support | Feature | Feature | No | No | Feature |
|
||||
| Bridge | [`get bridge.type`](cli_commands.md#view-the-compiled-bridge-type) | Compiled bridge | Feature | Feature | Feature | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.enabled`](cli_commands.md#view-or-change-the-bridge-enabled-flag) | Compiled bridge | Feature | Feature | Feature | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.delay`](cli_commands.md#add-a-delay-to-packets-routed-through-this-bridge) | Compiled bridge | Feature | Feature | Feature | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.source`](cli_commands.md#view-or-change-the-source-of-packets-bridged-to-the-external-interface) | Compiled bridge | Feature | Feature | Feature | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.baud`](cli_commands.md#view-or-change-the-speed-of-the-bridge-rs-232-only) | RS-232 bridge | Feature | Feature | Feature | No | Feature |
|
||||
| Bridge | [`get/set bridge.channel`](cli_commands.md#view-or-change-the-channel-used-for-bridging-espnow-only) | ESP-NOW bridge | No | No | No | Feature | Feature |
|
||||
| Bridge | [`get/set bridge.secret`](cli_commands.md#set-the-esp-now-secret) | ESP-NOW bridge | No | No | No | Feature | Feature |
|
||||
| Board | [`get bootloader.ver`](cli_commands.md#view-the-bootloader-version-nrf52-only) | nRF52 only | No | No | No | No | No |
|
||||
| Board | [`get pwrmgt.support`; `get pwrmgt.source`; `get pwrmgt.bootreason`; `get pwrmgt.bootmv`](nrf52_power_management.md#cli-commands) | nRF52 only | No | No | No | No | No |
|
||||
| Ethernet | [`eth.status`](cli_commands.md#view-ethernet-connection-status) | Ethernet target | Feature | Feature | No | No | Feature |
|
||||
| Browser OTA | [`start ota [ap]`; `stop ota`](cli_commands.md#start-or-stop-an-over-the-air-ota-firmware-update) | Compiled browser uploader | No | No | Yes | Feature | Feature |
|
||||
| WebConfig | [`start webconfig [ap]`; `stop webconfig`; `get/set webui`](cli_commands.md#browser-configuration-portal-esp32-repeater-and-room-server) | Compiled WebConfig | No | No | No | Feature | Feature |
|
||||
| WiFi | [`get/set wifi.ssid`; `set wifi.pwd`; `get wifi.status`; `get/set wifi.powersave`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#wifi-commands) | MQTT WiFi, or standalone FULL WebConfig; standalone has no `get wifi.pwd` | No | No | No | Yes | Feature |
|
||||
| WiFi | [`get/set wifi.cli`](cli_commands.md#browser-configuration-portal-esp32-repeater-and-room-server) | Compiled WebConfig | No | No | No | Feature | Feature |
|
||||
| LoRa OTA | [`ota help`; `ota ?`; `ota h`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota`; `ota status`; `ota st`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota stats`](ota_user_guide.md#1-see-what-im-running-and-whether-anything-is-going-on) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota ls`; `ota neighbors`; `ota nbrs`; `ota updates`; `ota n`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota get`; `ota pull`; `ota download`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota install`; `ota apply`; `ota applydelta`](ota_protocol.md#11-cli-surface-otaclicpp) | Compatible completed update | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota rescue install <base_hash16>`](ota_protocol.md#12-apply-bootloader-contract) | Internal-flash nRF52 LoRa OTA build with failed app-side EndF validation | No | No | Feature | No | No |
|
||||
| LoRa OTA | [`ota cancel`; `ota drop`; `ota stop`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota announce`; `ota adv`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota self`; `ota id`](ota_protocol.md#11-cli-surface-otaclicpp) | Firmware with EndF trailer | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota folder`; `ota fold`](ota_protocol.md#11-cli-surface-otaclicpp) | `on` needs compiled serial or TCP folder transport | No | No | Feature | Feature | Feature |
|
||||
| LoRa OTA | [`ota config`; `ota cfg`; `ota set`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota key`; `ota keys`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
|
||||
| LoRa OTA | [`ota dev ...`](ota_protocol.md#11-cli-surface-otaclicpp) | Developer diagnostics | No | No | Yes | Yes | Yes |
|
||||
| MQTT | [`get/set mqttN.preset`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-slot-commands) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set mqttN.server`; `get/set mqttN.port`; `get/set mqttN.username`; `get/set mqttN.password`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-slot-commands) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set mqttN.token`; `get/set mqttN.topic`; `get/set mqttN.audience`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-slot-commands) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get mqttN.diag`](#mqtt-slot-diagnostics) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.origin`; `get/set mqtt.iata`; `get mqtt.presets`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-shared-commands) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get mqtt.stats`](#mqtt-stats) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.status`; `get/set mqtt.packets`; `get/set mqtt.raw`; `get/set mqtt.interval`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-shared-commands) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.rx`](cli_commands.md#view-or-change-mqtt-rx-packet-uplinking); [`get/set mqtt.tx`](cli_commands.md#view-or-change-mqtt-tx-packet-uplinking) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.neighbors`](cli_commands.md#view-or-change-periodic-neighbors-publishing-mqtt-observer-neighbors-feature); [`get/set mqtt.neighbors.interval`](cli_commands.md#view-or-change-the-neighbors-publish-interval-mqtt-observer-neighbors-feature) | MQTT observer with compiled neighbor support | No | No | No | Feature | No |
|
||||
| MQTT | [`get/set mqtt.ntp`](cli_commands.md#view-or-change-the-ntp-server-mqtt-observer-only) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get mqtt.ntp.diag`](cli_commands.md#diagnose-ntp-server-connectivity-mqtt-observer-only) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set timezone`; `get/set timezone.offset`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#timezone-commands) | MQTT observer | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.analyzer.us`; `get/set mqtt.analyzer.eu`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#migration-from-old-configuration) | Legacy MQTT aliases | No | No | No | Yes | No |
|
||||
| MQTT | [`get/set mqtt.owner`; `get/set mqtt.email`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md#mqtt-shared-commands) | MQTT observer; `get` is local serial only | No | No | No | Yes | No |
|
||||
| MQTT | [`get mqtt.config.valid`](#mqtt-config-valid) | MQTT observer | No | No | No | Yes | No |
|
||||
| SNMP | [`get/set snmp`; `get/set snmp.community`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_SNMP.md#cli-commands) | MQTT target compiled with SNMP | No | No | No | Feature | No |
|
||||
| Alerts | [`get/set alert`; `get/set alert.psk`; `get/set alert.hashtag`; `get/set alert.region`; `get/set alert.wifi`; `get/set alert.mqtt`; `get/set alert.interval`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/ALERTS.md#cli) | MQTT observer | No | No | No | Yes | No |
|
||||
| Alerts | [`alert test [message]`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/ALERTS.md#cli) | MQTT observer with configured alert channel | No | No | No | Yes | No |
|
||||
| TLS | [`tls.bundletest <host>`](#tls-bundle-test) | MQTT target with embedded certificate bundle | No | No | No | Feature | No |
|
||||
| Manifest OTA | [`ota check`](#manifest-ota) | MQTT target with `OTA_MANIFEST_BASE` | No | No | No | Manifest | No |
|
||||
| Manifest OTA | [`ota update`](#manifest-ota) | MQTT target with `OTA_MANIFEST_BASE` | No | No | No | Manifest | No |
|
||||
|
||||
## Supplemental command notes
|
||||
|
||||
|
||||
@@ -483,6 +483,23 @@ Use the separately named `-ota-` artifact when LoRa OTA is required. A
|
||||
logging with LoRa OTA and the expanded FULL feature set, while MQTT remains
|
||||
disabled.
|
||||
|
||||
### Control live USB logging
|
||||
|
||||
**Usage:**
|
||||
|
||||
```text
|
||||
get usb.logging
|
||||
set usb.logging on
|
||||
set usb.logging off
|
||||
```
|
||||
|
||||
These commands are compiled into logging artifacts and control their live USB
|
||||
debug and packet output. The switch is session-only: every reboot starts a
|
||||
logging artifact with USB logging on. Turning it off does not disable CLI
|
||||
replies, Companion protocol frames, or essential non-debug serial messages.
|
||||
It also does not change the node-storage capture controlled by `log start` and
|
||||
`log stop`.
|
||||
|
||||
### Begin capture of rx log to node storage
|
||||
**Usage:** `log start`
|
||||
|
||||
|
||||
@@ -218,7 +218,9 @@ The terminal supports Companion chat commands, including `channels`,
|
||||
`login <admin-password>` and `cmd <remote-command>`, and routed
|
||||
`trace [recipient-name-or-prefix]`, plus local `ota`, `tempradio`, and
|
||||
`normalradio` controls. ESP32 builds also provide local
|
||||
`get/set wifi.powersave`. For example:
|
||||
`get/set wifi.powersave`. Logging artifacts additionally provide session-only
|
||||
`get/set usb.logging`; turning it off suppresses live USB diagnostics without
|
||||
disabling Companion frames or terminal replies. For example:
|
||||
|
||||
```text
|
||||
channels
|
||||
|
||||
+5
-3
@@ -46,7 +46,7 @@ Both paths require:
|
||||
- An OTA-enabled build whose artifact filename contains `-ota-` on the destination. The `-ota-` stamp confirms
|
||||
that the node can discover, download, verify, and install LoRa OTA. Intermediate repeaters do **not** need an
|
||||
OTA-enabled build: current repeater firmware relays OTA packets opaquely without storing or installing them.
|
||||
Portable logging, portable MQTT, and untagged builds cannot install LoRa OTA; FULL logging OTA builds can.
|
||||
Standard logging and untagged builds cannot install LoRa OTA; FULL MQTT and FULL logging OTA builds can.
|
||||
- An OTA-enabled MeshCore source connected to the computer by USB serial, or
|
||||
an ESP32 WiFi companion/FULL source connected over WiFi as described below.
|
||||
- Overlapping `tempradio` windows on the source, destination, and every repeater needed between them.
|
||||
@@ -60,8 +60,8 @@ overlapping window.
|
||||
repeater target. The normal repeater build keeps its external-sensor support and can serve as an intermediate
|
||||
OTA relay, but it cannot download or install an update for itself. The `-ota-` sibling omits optional external
|
||||
I2C environmental sensors to preserve the update workspace, while retaining board-native features such as its
|
||||
display, buttons, battery monitoring, and integrated GPS. ESP32 `-ota-` siblings also retain the compact
|
||||
browser WiFi uploader (`start ota`) and use a 254-entry neighbor table. RP2040 and STM32 repeaters do not
|
||||
display, buttons, battery monitoring, and integrated GPS. ESP32 `-ota-` siblings also retain the lightweight
|
||||
browser WiFi uploader (`start ota`), the complete CLI, and a 254-entry neighbor table. RP2040 and STM32 repeaters do not
|
||||
currently have a safe self-apply path, but current repeater firmware can still relay OTA packets opaquely
|
||||
during TempRadio.
|
||||
|
||||
@@ -73,6 +73,8 @@ remain enabled.
|
||||
ESP32 `*-full-ota-*` artifacts retain all compiled features and enable LoRa OTA for every FULL role,
|
||||
including room servers, sensors, observers, and bridges. A FULL image requires its expanded partition table:
|
||||
install the matching merged image over USB once before installing later non-merged FULL updates over LoRa.
|
||||
ESP32 MQTT observers and ESP-NOW bridges are always emitted as FULL artifacts; compact-CLI variants are no
|
||||
longer built.
|
||||
The `*-full-ota-*` profile uses MQTT with logging off. Use a `*-full-logging-ota-*` artifact when USB debug
|
||||
and packet logging are needed instead; that diagnostic profile explicitly disables MQTT and can produce
|
||||
substantial serial output.
|
||||
|
||||
+31
-21
@@ -108,16 +108,17 @@ EndF trailer (fixed 56 bytes):
|
||||
|
||||
The "reconstructed image" referenced by the manifest is the full `BODY || EndF` (what gets flashed).
|
||||
|
||||
### ESP32 portable app-slot profile
|
||||
### ESP32 application-slot profiles
|
||||
|
||||
ESP32 companion firmware is exempt from the portable-slot limit. USB and WiFi companion artifacts retain
|
||||
LoRa OTA and carry `-ota-` in their filenames so they can seed a host folder over serial or TCP; they keep
|
||||
their target partition table rather than using the FULL profile. A small set of high-capacity classic ESP32
|
||||
companions cannot combine their configured contact, group-channel, and offline-queue capacities with LoRa
|
||||
OTA in internal DRAM. Their normal artifacts remain unchanged, and option 3 also emits `-full-ota-` and
|
||||
`-full-logging-ota-` variants with 100 contacts, 8 group channels, and a 16-frame offline queue. Except for
|
||||
the ESP32-C6 case below, every other ESP32 artifact, including room,
|
||||
sensor, and repeater roles, must fit the legacy slot from `0x10000` up to
|
||||
`-full-logging-ota-` variants with 100 contacts, 8 group channels, and a 16-frame offline queue. MQTT
|
||||
observers and ESP-NOW bridges always use FULL builds because fitting them into the legacy slot would require
|
||||
removing CLI and role features. Except for those FULL roles and the ESP32-C6 case below, non-companion ESP32
|
||||
artifacts, including room, sensor, and repeater roles, must fit the legacy slot from `0x10000` up to
|
||||
`0x150000` (`0x140000`, 1,310,720 bytes), including the 56-byte `EndF` trailer. The build checks both that
|
||||
limit and the target's actual app partition. The ESP32-C6 `no_external_sensors` OTA siblings are the narrow
|
||||
exception: the Arduino 3.x WiFi runtime cannot fit that cross-family ceiling, so those images retain their
|
||||
@@ -134,29 +135,23 @@ keeps the runtime software Ed25519 fallback from being expanded into tens of kil
|
||||
arithmetic while retaining CC310 hardware crypto, hardware RNG mixing, telemetry history, and board-native
|
||||
features.
|
||||
|
||||
Two WiFi-heavy non-companion profiles need additional reductions to remain portable. MQTT observer builds
|
||||
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. 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.
|
||||
WiFi-heavy non-companion roles are not reduced to fit the legacy application slot. `build.sh` automatically
|
||||
promotes every ESP32 MQTT observer and ESP-NOW bridge to the expanded FULL partition profile. These artifacts
|
||||
retain the complete role CLI, WebConfig where supported, display and optional sensor support, full timezone
|
||||
and TLS behavior, and the board's normal power-management implementation. The compact CLI is not compiled
|
||||
into any build. Ordinary repeater builds remain sensor-enabled; only explicitly named
|
||||
`*_lora_ota_no_external_sensors` siblings omit sensors for LoRa distribution, and those siblings retain the
|
||||
complete CLI.
|
||||
|
||||
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-*` and `*-full-logging-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 the logging-off FULL
|
||||
Option 3 in `build.sh` emits `*-full-ota-*` and `*-full-logging-ota-*` ESP32 artifacts for
|
||||
FULL-capable non-companion roles and for the constrained companion fallbacks described above. MQTT observers
|
||||
and ESP-NOW bridges are emitted only with expanded FULL partitions. Menu option 8, or
|
||||
`build-full-esp32-firmwares`, builds the logging-off FULL
|
||||
artifacts from matching MQTT targets. Menu option 9, or `build-full-esp32-logging-firmwares`, builds the
|
||||
FULL logging artifacts from matching non-MQTT targets.
|
||||
FULL builds restore WebConfig, display support, optional external sensors, and the full role CLI and feature
|
||||
@@ -745,6 +740,21 @@ ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
2. re-checks `TRAILER`, `image_hash`, `approval == "APRV"`, and that the delta's `base_hash` equals the
|
||||
running firmware's `EndF.body_hash` (recomputed by scanning for `EndF` - never trust `bank_0_size`),
|
||||
3. applies the in-place codec over the app region and boots only if the result hashes to `image_hash`.
|
||||
- **nRF52 internal staging ceiling:** the application derives the ceiling from facts available in every
|
||||
build, not a board-name list. A companion that actually links the internal ExtraFS datastore stays below
|
||||
`0xD4000`; a default linker region, QSPI secondary storage, or a role that does not mount ExtraFS can
|
||||
reclaim the unused 100 KiB through `0xED000`. The application uses the larger window only when the
|
||||
installed bootloader advertises the GPREGRET2 ceiling-handoff capability. The bootloader treats every
|
||||
unknown/legacy handoff value as
|
||||
`0xD4000`, and accepts a container only at the bottom-aligned position for the selected ceiling.
|
||||
- **nRF52 dynamic apply window:** the post-build hook records the resolved app base, linked app end,
|
||||
storage flags, and desired staging ceiling immediately before `EndF`. `motatool` reads that authenticated
|
||||
firmware record and chooses `memory_size` from the actual patch size and bottom-aligned stage address;
|
||||
firmware without the record retains the conservative `0x98000` default. Before writing `APRV`, the app
|
||||
verifies the detools header fits below the staged container, and the bootloader independently repeats
|
||||
the geometry check before its first application write. Expanded auto-sized packages require a bootloader
|
||||
with the ceiling-handoff capability; use `--inplace-memory 0x98000` when intentionally targeting an older
|
||||
bootloader and the images still fit that window.
|
||||
- **nRF52 EndF rescue:** `ota rescue install <base_hash16>` is a pre-provisioned recovery path for an
|
||||
internal-flash nRF52 application that still runs but cannot validate its own EndF identity. It refuses
|
||||
when normal EndF validation succeeds, requires the operator hash to exactly equal the staged delta's
|
||||
|
||||
@@ -92,6 +92,14 @@ set powersaving {on|off}
|
||||
Shows or changes Companion device power saving. On ESP32 this controls CPU and
|
||||
GPS idle behavior; it does not change LoRa RXPS or WiFi modem sleep.
|
||||
|
||||
```
|
||||
get usb.logging
|
||||
set usb.logging {on|off}
|
||||
```
|
||||
Shows or changes live USB debug and packet output in a Companion logging
|
||||
artifact. The setting lasts until reboot; logging starts on again at the next
|
||||
boot. Companion protocol frames and terminal replies remain enabled.
|
||||
|
||||
```
|
||||
get radio.rxps
|
||||
set radio.rxps {off|on|level 1-10 [preamble 16|32]|rx_us sleep_us}
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
#include <helpers/AtomicFileWriter.h>
|
||||
#endif
|
||||
|
||||
// Linked presence of this symbol is the authoritative signal that this firmware actually mounts the
|
||||
// internal 0xD4000 ExtraFS. OTA layout code references it weakly, so non-companion roles can reclaim the
|
||||
// reserved range even though nrf52_base defines EXTRAFS globally.
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
extern "C" __attribute__((used)) const uint8_t g_meshcore_internal_extrafs = 1u;
|
||||
#endif
|
||||
|
||||
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
||||
#define MAX_BLOBRECS 100
|
||||
#else
|
||||
|
||||
@@ -1354,7 +1354,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_prefs.tx_power_dbm = LORA_TX_POWER;
|
||||
_prefs.buzzer_quiet = DEFAULT_BUZZER_QUIET ? 1 : 0;
|
||||
_prefs.gps_enabled = 0; // GPS disabled by default
|
||||
_prefs.gps_interval = 0; // No automatic GPS updates by default
|
||||
_prefs.gps_interval = 0; // Use the default 1-second fix-processing interval
|
||||
_prefs.autoadd_config = DEFAULT_AUTOADD_CONFIG;
|
||||
_prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE;
|
||||
_prefs.radio_fem_txgain = 0;
|
||||
@@ -4665,6 +4665,11 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
|| strcmp(command, "get powersaving") == 0) {
|
||||
Serial.printf(" powersaving %s\r\n",
|
||||
_prefs.powersaving_enabled ? "on" : "off");
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
} else if (strcmp(command, "get usb.logging") == 0) {
|
||||
Serial.printf(" usb.logging %s\r\n",
|
||||
mesh::isUsbLoggingEnabled() ? "on" : "off");
|
||||
#endif
|
||||
} else if (strncmp(command, "powersaving ", 12) == 0) {
|
||||
char reply[160];
|
||||
applyAndSavePowerSaving(command + 12, reply);
|
||||
@@ -4709,6 +4714,22 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
}
|
||||
} else if (strncmp(command, "set ", 4) == 0) {
|
||||
const char* config = command + 4;
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
if (strncmp(config, "usb.logging", 11) == 0
|
||||
&& (config[11] == 0 || config[11] == ' '
|
||||
|| config[11] == '\t')) {
|
||||
const char* value = config + 11;
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
||||
Serial.print(" ERROR: use set usb.logging <on|off>\r\n");
|
||||
} else {
|
||||
const bool enabled = strcmp(value, "on") == 0;
|
||||
mesh::setUsbLoggingEnabled(enabled);
|
||||
Serial.printf(" OK - USB logging %s until reboot\r\n",
|
||||
enabled ? "on" : "off");
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
if (strncmp(config, "powersaving ", 12) == 0) {
|
||||
char reply[160];
|
||||
applyAndSavePowerSaving(config + 12, reply);
|
||||
@@ -4803,6 +4824,10 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
Serial.print("Commands:\r\n");
|
||||
Serial.print(" set {name|lat|lon|freq|tx|af} {value}\r\n");
|
||||
Serial.print(" powersaving [on|off]\r\n");
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
Serial.print(" get usb.logging\r\n");
|
||||
Serial.print(" set usb.logging <on|off>\r\n");
|
||||
#endif
|
||||
#if defined(ESP32) && defined(WIFI_SSID)
|
||||
Serial.print(" get wifi.powersave\r\n");
|
||||
Serial.print(" set wifi.powersave <none|min|max>\r\n");
|
||||
|
||||
@@ -253,11 +253,9 @@ public:
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
void applyGpsPrefs() {
|
||||
sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0");
|
||||
if (_prefs.gps_interval > 0) {
|
||||
char interval_str[12]; // Max: 24 hours = 86400 seconds (5 digits + null)
|
||||
sprintf(interval_str, "%u", _prefs.gps_interval);
|
||||
sensors.setSettingValue("gps_interval", interval_str);
|
||||
}
|
||||
char interval_str[12]; // Max: 24 hours = 86400 seconds (5 digits + null)
|
||||
sprintf(interval_str, "%u", _prefs.gps_interval);
|
||||
sensors.setSettingValue("gps_interval", interval_str);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ struct CompanionNodePrefs { // persisted to file
|
||||
uint8_t advert_loc_policy;
|
||||
uint8_t buzzer_quiet;
|
||||
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
|
||||
uint32_t gps_interval; // GPS read interval in seconds
|
||||
uint32_t gps_interval; // GPS read interval in seconds; 0 uses the 1-second default
|
||||
uint8_t autoadd_config; // bitmask for auto-add contacts config
|
||||
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
|
||||
uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
#ifdef ESP32_PLATFORM
|
||||
#include "esp_bt.h"
|
||||
#include "esp_pm.h"
|
||||
#include "esp_sleep.h"
|
||||
#if defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE
|
||||
#define COMPANION_IDF_PM_AVAILABLE 1
|
||||
#else
|
||||
#define COMPANION_IDF_PM_AVAILABLE 0
|
||||
#endif
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
@@ -174,12 +181,39 @@ static uint32_t companionNominalCpuMhz() {
|
||||
|
||||
static bool applyCompanionPowerSaving(bool enabled) {
|
||||
const uint32_t nominal_mhz = companionNominalCpuMhz();
|
||||
const uint32_t target_mhz = enabled && nominal_mhz > 80 ? 80 : nominal_mhz;
|
||||
if (!setCpuFrequencyMhz(target_mhz)) {
|
||||
Serial.printf("Device power saving failed: CPU %lu MHz is unsupported\r\n",
|
||||
(unsigned long)target_mhz);
|
||||
const uint32_t max_mhz = enabled && nominal_mhz > 80 ? 80 : nominal_mhz;
|
||||
|
||||
#if COMPANION_IDF_PM_AVAILABLE
|
||||
const uint32_t min_mhz = enabled && max_mhz > 40 ? 40 : max_mhz;
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
esp_pm_config_esp32c3_t pm_config;
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
esp_pm_config_esp32s3_t pm_config;
|
||||
#elif CONFIG_IDF_TARGET_ESP32
|
||||
esp_pm_config_esp32_t pm_config;
|
||||
#else
|
||||
esp_pm_config_t pm_config;
|
||||
#endif
|
||||
pm_config.max_freq_mhz = max_mhz;
|
||||
pm_config.min_freq_mhz = min_mhz;
|
||||
pm_config.light_sleep_enable = enabled;
|
||||
|
||||
esp_err_t pm_result = esp_pm_configure(&pm_config);
|
||||
if (pm_result != ESP_OK) {
|
||||
Serial.printf("Device power saving failed: %s\r\n",
|
||||
esp_err_to_name(pm_result));
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
// Arduino's prebuilt ESP-IDF normally has CONFIG_PM_ENABLE disabled, in
|
||||
// which case esp_pm_configure() is a stub returning ESP_ERR_NOT_SUPPORTED.
|
||||
// Keep frequency throttling functional instead of retrying that stub.
|
||||
if (!setCpuFrequencyMhz(max_mhz)) {
|
||||
Serial.printf("Device power saving failed: CPU %lu MHz is unsupported\r\n",
|
||||
(unsigned long)max_mhz);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(BLE_PIN_CODE) && !CONFIG_IDF_TARGET_ESP32C6
|
||||
esp_err_t bt_result = enabled ? esp_bt_sleep_enable()
|
||||
@@ -191,8 +225,14 @@ static bool applyCompanionPowerSaving(bool enabled) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if COMPANION_IDF_PM_AVAILABLE
|
||||
Serial.printf("Device power saving %s: CPU %lu-%lu MHz, automatic light sleep %s\r\n",
|
||||
enabled ? "on" : "off", (unsigned long)min_mhz,
|
||||
(unsigned long)max_mhz, enabled ? "on" : "off");
|
||||
#else
|
||||
Serial.printf("Device power saving %s: CPU %lu MHz\r\n",
|
||||
enabled ? "on" : "off", (unsigned long)target_mhz);
|
||||
enabled ? "on" : "off", (unsigned long)max_mhz);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1147,7 +1187,29 @@ void loop() {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
#elif defined(ESP32_PLATFORM)
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep
|
||||
#if COMPANION_IDF_PM_AVAILABLE
|
||||
// Yield long enough for ESP-IDF automatic light sleep to enter when no
|
||||
// driver holds a power-management lock.
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
#elif defined(ENABLE_USB_INTERFACE) \
|
||||
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
|
||||
&& !defined(BLE_PIN_CODE) && !defined(WIFI_SSID) \
|
||||
&& !defined(ETHERNET_ENABLED) && !defined(SERIAL_RX)
|
||||
// The stock Arduino core has no automatic light sleep. A short timer
|
||||
// slice gives native-USB-only battery builds real light sleep without
|
||||
// delaying radio, GPS, button, or newly attached USB work by more than the
|
||||
// normal 10 ms loop cadence. can_sleep already proved no USB host is up.
|
||||
if (esp_sleep_enable_timer_wakeup(10000ULL) != ESP_OK
|
||||
|| esp_light_sleep_start() != ESP_OK) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
#else
|
||||
// Connected transports need their own modem sleep and must retain the
|
||||
// normal FreeRTOS idle behavior.
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
#endif
|
||||
#elif defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
|
||||
board.sleep(0); // event-driven idle; interrupts wake the main loop
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,9 @@ void loop() {
|
||||
board.feedWatchdog();
|
||||
#endif
|
||||
modem->loop();
|
||||
// GPS acquisition, cache refresh, and delayed shutdown are driven by the
|
||||
// sensor manager loop even though KISS itself is normally host powered.
|
||||
sensors.loop();
|
||||
|
||||
if (!modem->isActuallyTransmitting() && !modem->isHostOutputBackedUp()) {
|
||||
if (!modem->isTxBusy()) {
|
||||
|
||||
@@ -1016,11 +1016,13 @@ const char *MyMesh::getLogDateTime() {
|
||||
|
||||
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
#if MESH_PACKET_LOGGING && !defined(MESH_COMPACT_PACKET_LOGGING)
|
||||
// Logging builds prefer backpressure over silently losing a packet record.
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.print(" RAW: ");
|
||||
mesh::Utils::printHex(Serial, raw, len);
|
||||
Serial.println();
|
||||
if (mesh::isUsbLoggingEnabled()) {
|
||||
// Logging builds prefer backpressure over silently losing a packet record.
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.print(" RAW: ");
|
||||
mesh::Utils::printHex(Serial, raw, len);
|
||||
Serial.println();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
@@ -9510,9 +9512,8 @@ void MyMesh::formatClockSyncStatus(const char* args, char* reply, size_t reply_l
|
||||
|
||||
#else
|
||||
|
||||
// Portable MQTT observers use their bridge's NTP source. Flash-constrained
|
||||
// repeaters can also omit mesh consensus while retaining manual clock setting
|
||||
// and scheduled radio changes.
|
||||
// Builds without mesh consensus retain manual clock setting and scheduled
|
||||
// radio changes. MQTT observers use their bridge's NTP source.
|
||||
static const char* clockSyncMeshSuppressionName(uint8_t source) {
|
||||
(void)source;
|
||||
return "unavailable";
|
||||
@@ -10408,9 +10409,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
#endif
|
||||
|
||||
#if defined(PORTABLE_MQTT_OBSERVER)
|
||||
// Neighbor refresh is a core repeater operation, not an MQTT feature. Keep
|
||||
// it ahead of the portable observer's reduced CommonCLI handoff so every
|
||||
// repeater build exposes the same command.
|
||||
// Legacy PORTABLE_MQTT_OBSERVER builds kept neighbor refresh ahead of their
|
||||
// reduced role handoff. Current build.sh profiles never define this macro.
|
||||
if (discover_neighbors_match != mesh::cli::NoArgCommandMatch::NoMatch) {
|
||||
if (discover_neighbors_match ==
|
||||
mesh::cli::NoArgCommandMatch::HasArguments) {
|
||||
@@ -10422,9 +10422,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
return;
|
||||
}
|
||||
|
||||
// Reply-path overrides are core remote-client routing controls. Keep them
|
||||
// available before the portable observer hands the remaining commands to
|
||||
// its reduced CommonCLI parser.
|
||||
// Legacy PORTABLE_MQTT_OBSERVER builds kept reply-path overrides ahead of
|
||||
// their reduced role handoff. Current build.sh profiles use FULL instead.
|
||||
if (classifyClientPathCommand(command) != CLIENT_PATH_NONE) {
|
||||
if (sender && !sender->isAdmin()) {
|
||||
bool allowed = (sender->isRegionMgr() || sender->isFilterMgr())
|
||||
@@ -10438,9 +10437,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
return;
|
||||
}
|
||||
|
||||
// The portable observer exposes its MQTT/WiFi/update controls through
|
||||
// CommonCLI. Omit the repeater's large remote-administration command tree;
|
||||
// its mesh behavior remains fixed by the selected build profile.
|
||||
// Compatibility path for manually defined legacy portable builds. Current
|
||||
// release builds use FULL and do not enter this branch.
|
||||
_cli.handleCommand(sender_timestamp, command, reply);
|
||||
return;
|
||||
#endif
|
||||
|
||||
@@ -822,6 +822,9 @@ protected:
|
||||
void applyGpsPrefs() {
|
||||
sensors.setPowerSavingEnabled(_prefs.powersaving_enabled != 0);
|
||||
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
|
||||
char interval_str[12];
|
||||
sprintf(interval_str, "%u", _prefs.gps_interval);
|
||||
sensors.setSettingValue("gps_interval", interval_str);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -264,11 +264,13 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
|
||||
|
||||
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
// Logging builds prefer backpressure over silently losing a packet record.
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.print(" RAW: ");
|
||||
mesh::Utils::printHex(Serial, raw, len);
|
||||
Serial.println();
|
||||
if (mesh::isUsbLoggingEnabled()) {
|
||||
// Logging builds prefer backpressure over silently losing a packet record.
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.print(" RAW: ");
|
||||
mesh::Utils::printHex(Serial, raw, len);
|
||||
Serial.println();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|
||||
@@ -331,6 +331,9 @@ protected:
|
||||
void applyGpsPrefs() {
|
||||
sensors.setPowerSavingEnabled(_prefs.powersaving_enabled != 0);
|
||||
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
|
||||
char interval_str[12];
|
||||
sprintf(interval_str, "%u", _prefs.gps_interval);
|
||||
sensors.setSettingValue("gps_interval", interval_str);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -818,8 +818,10 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len) {
|
||||
MESH_DEBUG_PRINT("handleIncomingMsg: unhandled msg from ");
|
||||
#ifdef MESH_DEBUG
|
||||
mesh::Utils::printHex(Serial, from.id.pub_key, PUB_KEY_SIZE);
|
||||
Serial.printf(": %s\n", data);
|
||||
if (mesh::isUsbLoggingEnabled()) {
|
||||
mesh::Utils::printHex(Serial, from.id.pub_key, PUB_KEY_SIZE);
|
||||
Serial.printf(": %s\n", data);
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
@@ -1286,3 +1288,79 @@ void SensorMesh::loop() {
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool SensorMesh::isMillisTimerDue(unsigned long timestamp) const {
|
||||
return timestamp && millisHasNowPassed(timestamp);
|
||||
}
|
||||
|
||||
uint32_t SensorMesh::limitSleepToMillisTimer(unsigned long timestamp,
|
||||
uint32_t sleep_secs) const {
|
||||
if (!timestamp || sleep_secs == 0) return sleep_secs;
|
||||
|
||||
unsigned long now = millis();
|
||||
if ((long)(now - timestamp) >= 0) return 0;
|
||||
|
||||
uint32_t remaining_secs = (timestamp - now + 999UL) / 1000UL;
|
||||
return remaining_secs < sleep_secs ? remaining_secs : sleep_secs;
|
||||
}
|
||||
|
||||
bool SensorMesh::hasPendingWork() const {
|
||||
if (_cli.hasActiveUserGpioTimer()) return true;
|
||||
if (radio_driver.isWatchdogObserving()) return true;
|
||||
if (radio_driver.isCalibratingNoiseFloor()) return true;
|
||||
if (hasQueuedWorkDue() || hasRetryWorkDue()) return true;
|
||||
if (isMillisTimerDue(next_flood_advert)
|
||||
|| isMillisTimerDue(next_local_advert)
|
||||
|| isMillisTimerDue(dirty_contacts_expiry)) return true;
|
||||
if (num_alert_tasks > 0
|
||||
&& isMillisTimerDue(alert_tasks[0]->send_expiry)) return true;
|
||||
|
||||
const bool radio_apply_backoff = radio_apply_retry_at
|
||||
&& !millisHasNowPassed(radio_apply_retry_at);
|
||||
if (!radio_apply_backoff
|
||||
&& (isMillisTimerDue(set_radio_at) || isMillisTimerDue(revert_radio_at)
|
||||
|| (saved_radio_apply_pending && !temp_radio_applied))) return true;
|
||||
|
||||
uint32_t now_secs = getRTCClock()->getCurrentTime();
|
||||
return now_secs >= last_read_time + SENSOR_READ_INTERVAL_SECS;
|
||||
}
|
||||
|
||||
uint32_t SensorMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
|
||||
if (max_secs == 0 || hasPendingWork()) return 0;
|
||||
|
||||
uint32_t sleep_secs = max_secs;
|
||||
uint32_t wake_delay_ms;
|
||||
if (getNextQueueWakeDelay(wake_delay_ms)) {
|
||||
uint32_t wake_delay_secs = (wake_delay_ms + 999UL) / 1000UL;
|
||||
if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs;
|
||||
}
|
||||
if (getNextRetryWakeDelay(wake_delay_ms)) {
|
||||
uint32_t wake_delay_secs = (wake_delay_ms + 999UL) / 1000UL;
|
||||
if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs;
|
||||
}
|
||||
|
||||
sleep_secs = limitSleepToMillisTimer(next_flood_advert, sleep_secs);
|
||||
sleep_secs = limitSleepToMillisTimer(next_local_advert, sleep_secs);
|
||||
sleep_secs = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_secs);
|
||||
if (num_alert_tasks > 0) {
|
||||
sleep_secs = limitSleepToMillisTimer(alert_tasks[0]->send_expiry,
|
||||
sleep_secs);
|
||||
}
|
||||
|
||||
const bool radio_apply_backoff = radio_apply_retry_at
|
||||
&& !millisHasNowPassed(radio_apply_retry_at);
|
||||
if (radio_apply_backoff) {
|
||||
sleep_secs = limitSleepToMillisTimer(radio_apply_retry_at, sleep_secs);
|
||||
} else {
|
||||
sleep_secs = limitSleepToMillisTimer(set_radio_at, sleep_secs);
|
||||
sleep_secs = limitSleepToMillisTimer(revert_radio_at, sleep_secs);
|
||||
}
|
||||
|
||||
uint32_t now_secs = getRTCClock()->getCurrentTime();
|
||||
uint32_t sensor_due = last_read_time + SENSOR_READ_INTERVAL_SECS;
|
||||
if (now_secs < sensor_due) {
|
||||
uint32_t sensor_delay = sensor_due - now_secs;
|
||||
if (sensor_delay < sleep_secs) sleep_secs = sensor_delay;
|
||||
}
|
||||
return sleep_secs;
|
||||
}
|
||||
@@ -66,6 +66,7 @@ public:
|
||||
const char* getRole() override { return FIRMWARE_ROLE; }
|
||||
const char* getNodeName() { return _prefs.node_name; }
|
||||
NodePrefs* getNodePrefs() { return &_prefs; }
|
||||
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
|
||||
void savePrefs(
|
||||
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common) override {
|
||||
_cli.savePrefs(_fs, scope);
|
||||
@@ -213,6 +214,10 @@ private:
|
||||
uint8_t radio_apply_failures;
|
||||
|
||||
bool applySavedRadioParams();
|
||||
bool isMillisTimerDue(unsigned long timestamp) const;
|
||||
uint32_t limitSleepToMillisTimer(unsigned long timestamp,
|
||||
uint32_t sleep_secs) const;
|
||||
bool hasPendingWork() const;
|
||||
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
||||
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
|
||||
@@ -226,6 +231,9 @@ private:
|
||||
void applyGpsPrefs() {
|
||||
sensors.setPowerSavingEnabled(_prefs.powersaving_enabled != 0);
|
||||
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
|
||||
char interval_str[12];
|
||||
sprintf(interval_str, "%u", _prefs.gps_interval);
|
||||
sensors.setSettingValue("gps_interval", interval_str);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
@@ -56,6 +56,7 @@ void halt() {
|
||||
}
|
||||
|
||||
static char command[160];
|
||||
static const unsigned long POWERSAVING_FIRST_SLEEP_SECS = 120;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
@@ -179,4 +180,21 @@ void loop() {
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
external_watchdog.loop();
|
||||
#endif
|
||||
|
||||
if (the_mesh.getNodePrefs()->powersaving_enabled
|
||||
&& !board.isUsbDataConnected()) {
|
||||
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
if (sleep_secs > 0) external_watchdog.feed();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (sleep_secs > 0) board.sleep(0);
|
||||
#else
|
||||
if (sleep_secs > 0
|
||||
&& the_mesh.millisHasNowPassed(
|
||||
POWERSAVING_FIRST_SLEEP_SECS * 1000UL)) {
|
||||
board.sleep(sleep_secs);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,8 @@ python3 mesh-america/update-provider-release.py \
|
||||
|
||||
The updater preserves the catalog's curated device names, role choices, and
|
||||
hardware guidance; validates every referenced artifact; routes URLs to the
|
||||
correct GitHub release page; and replaces legacy FULL MQTT choices with the
|
||||
matching portable MQTT observer builds.
|
||||
correct GitHub release page; and replaces legacy portable MQTT choices with the
|
||||
matching expanded-partition FULL MQTT observer builds.
|
||||
|
||||
The logging catalog likewise keeps unaffected roles on v1.17.1.1 while its
|
||||
Companion, Full Companion, and expanded Companion logging profiles track the
|
||||
|
||||
@@ -85,6 +85,7 @@ def release_tag(identity: str, args: argparse.Namespace) -> str:
|
||||
return args.logging_utility_tag if is_logging_utility(identity) else args.logging_main_tag
|
||||
if (
|
||||
"companion_radio_full" in lowered
|
||||
or "-full-ota" in lowered
|
||||
or "lora_ota" in lowered
|
||||
or "observer_mqtt" in lowered
|
||||
):
|
||||
@@ -117,7 +118,7 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
"retry tuning, Cascade defaults, and the USA/Canada 910.525 MHz / SF7 / "
|
||||
"BW62.5 / CR5 preset. This catalog contains packet-logging builds with "
|
||||
"USB debug enabled except on seven flash-constrained STM32 targets, "
|
||||
"selected non-logging utility and portable MQTT observer builds, "
|
||||
"selected non-logging utility and FULL MQTT observer builds, "
|
||||
"and expanded-partition FULL USB-logging LoRa-OTA builds. Host software "
|
||||
"can consume the USB serial log and publish it separately. Open Release "
|
||||
"notes for role, hardware, installation, and partition requirements."
|
||||
@@ -143,7 +144,20 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
old_keys = list(firmware["version"])
|
||||
old_version = next(iter(firmware["version"].values()))
|
||||
old_notes = old_version["notes"]
|
||||
identities = common.ordered_catalog_identities(firmware)
|
||||
old_identities = common.ordered_catalog_identities(firmware)
|
||||
identities: list[str] = []
|
||||
observer_entry = False
|
||||
for old_identity in old_identities:
|
||||
identity, converted_observer = common.resolve_release_identity(
|
||||
old_identity, release_files
|
||||
)
|
||||
if identity not in identities:
|
||||
identities.append(identity)
|
||||
observer_entry = (
|
||||
observer_entry
|
||||
or converted_observer
|
||||
or "observer_mqtt" in identity.lower()
|
||||
)
|
||||
paths: list[Path] = []
|
||||
for identity in identities:
|
||||
if identity not in release_files:
|
||||
@@ -187,7 +201,13 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
)
|
||||
|
||||
version_key = common.replacement_version_key(old_keys[0], args.artifact_version)
|
||||
notes = common.replace_release_version(old_notes, display_version)
|
||||
if observer_entry:
|
||||
notes = common.observer_notes(
|
||||
firmware["role"], display_version, identities, old_notes
|
||||
)
|
||||
firmware["subTitle"] = "FULL MQTT observer"
|
||||
else:
|
||||
notes = common.replace_release_version(old_notes, display_version)
|
||||
if "USA/Canada 910.525 MHz" not in notes:
|
||||
notes = notes.replace(
|
||||
"with Halo/Keymind retry tuning and Cascade defaults.",
|
||||
|
||||
@@ -137,9 +137,19 @@ def resolve_release_identity(
|
||||
if alias is not None and alias in release_files:
|
||||
return alias, False
|
||||
|
||||
# The previous catalog used expanded-partition FULL MQTT images. The new
|
||||
# standard matrix emits a portable MQTT observer under the same target name
|
||||
# without the build-only "-full-ota" filename marker.
|
||||
# Accept catalogs produced while portable MQTT observers were still
|
||||
# emitted, and migrate them to the expanded-partition FULL artifact.
|
||||
if "observer_mqtt" in old_identity.lower():
|
||||
full_identity = f"{old_identity}-full-ota"
|
||||
if full_identity in release_files:
|
||||
return full_identity, True
|
||||
if "bridge_espnow" in old_identity.lower():
|
||||
full_identity = f"{old_identity}-full-ota"
|
||||
if full_identity in release_files:
|
||||
return full_identity, False
|
||||
|
||||
# Retain compatibility with the short-lived reverse migration used by
|
||||
# older release directories.
|
||||
full_suffix = "-full-ota"
|
||||
if old_identity.endswith(full_suffix):
|
||||
portable_identity = old_identity[: -len(full_suffix)]
|
||||
@@ -172,6 +182,7 @@ def release_tag_for_identity(identity: str, args: argparse.Namespace) -> str:
|
||||
lowered = identity.lower()
|
||||
if (
|
||||
"companion_radio_full" in lowered
|
||||
or "-full-ota" in lowered
|
||||
or "lora_ota" in lowered
|
||||
or "observer_mqtt" in lowered
|
||||
):
|
||||
@@ -240,6 +251,8 @@ def replace_release_version(notes: str, display_version: str) -> str:
|
||||
def partition_warning(identities: list[str]) -> str | None:
|
||||
if not any(
|
||||
identity.lower() in LEGACY_PORTABLE_CEILING_EXCEPTIONS
|
||||
or "-full-ota" in identity.lower()
|
||||
or "-full-logging-ota" in identity.lower()
|
||||
for identity in identities
|
||||
):
|
||||
return None
|
||||
@@ -297,15 +310,15 @@ def observer_notes(
|
||||
),
|
||||
role_paragraph,
|
||||
(
|
||||
"PROFILE - Portable MQTT observer: forwards mesh observations to an "
|
||||
"on-device Wi-Fi MQTT bridge, keeps USB debug/packet logging off, and "
|
||||
"uses bridge NTP instead of mesh clock consensus. LoRa self-update is "
|
||||
"off in this compact profile."
|
||||
"PROFILE - FULL MQTT observer: uses expanded partitions, forwards mesh observations to an "
|
||||
"on-device Wi-Fi MQTT bridge, keeps USB debug/packet logging off, "
|
||||
"uses bridge NTP instead of mesh clock consensus, and retains the "
|
||||
"complete role CLI. LoRa self-update is enabled."
|
||||
),
|
||||
(
|
||||
"INSTALL - First-time setup uses Full install (the merged bootloader + "
|
||||
"firmware image). Routine upgrades use Update (the app-only image) only "
|
||||
"when the existing app partition is compatible."
|
||||
"INSTALL - Flash Full install (the merged bootloader + firmware image) "
|
||||
"over USB once to install the expanded partition table. Routine upgrades "
|
||||
"can then use Update (the app-only image) while that layout remains installed."
|
||||
),
|
||||
]
|
||||
warning = partition_warning(identities)
|
||||
@@ -320,7 +333,7 @@ def observer_notes(
|
||||
"Board selection note:"
|
||||
):
|
||||
paragraphs.append(paragraph)
|
||||
paragraphs.append("SELECTION - Portable MQTT observer.")
|
||||
paragraphs.append("SELECTION - FULL MQTT observer.")
|
||||
return "\n\n".join(paragraphs)
|
||||
|
||||
|
||||
@@ -341,8 +354,9 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
f"Keymind Cascade MeshCore {display_version} firmware with Halo/Keymind "
|
||||
"retry tuning, Cascade defaults, and the USA/Canada 910.525 MHz / SF7 / "
|
||||
"BW62.5 / CR5 preset. This catalog contains standard builds, Full "
|
||||
"Companion builds, compact LoRa-OTA builds, and portable MQTT observer "
|
||||
"builds with USB debug/packet logging off. On nRF52, Full Companion "
|
||||
"Companion builds, lean LoRa-OTA builds, and expanded-partition FULL "
|
||||
"MQTT observer and ESP-NOW bridge builds. MQTT observers keep USB "
|
||||
"debug/packet logging off. On nRF52, Full Companion "
|
||||
"replaces separate BLE and USB choices; on ESP32, Full Companion is "
|
||||
"offered next to the BLE/USB variants. Open Release notes for role, "
|
||||
"hardware, installation, and partition requirements."
|
||||
@@ -406,7 +420,7 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
resolved_identities,
|
||||
old_notes,
|
||||
)
|
||||
firmware["subTitle"] = "Portable MQTT observer"
|
||||
firmware["subTitle"] = "FULL MQTT observer"
|
||||
observer_entries += 1
|
||||
else:
|
||||
version_key = replacement_version_key(old_keys[0], args.artifact_version)
|
||||
|
||||
+6
-3
@@ -130,9 +130,12 @@ lib_ignore = PsychicMqttClient
|
||||
|
||||
; Shared LoRa OTA recipe for nRF52840 variants. nRF52840 has no A/B slot, so the update is
|
||||
; applied in place by the custom OTAFIX bootloader (in-place detools); the app only stages + verifies +
|
||||
; approves it. OtaFlashLayout_nrf52.h uses FS_START 0xD4000 as the safe staging ceiling for the supported
|
||||
; S140 v6/v7 linker scripts. The device must run the matching OTAFIX
|
||||
; bootloader for `applydelta` to actually apply. (detools.c is intentionally NOT built into the app -
|
||||
; approves it. OtaFlashLayout_nrf52.h derives the safe ceiling from the linked app region and secondary
|
||||
; storage: internal ExtraFS stays protected at 0xD4000; QSPI/no-ExtraFS roles can stage through 0xED000.
|
||||
; The EndF hook records that resolved geometry so motatool can size each in-place patch without target-name
|
||||
; heuristics. An older bootloader makes the app stage below 0xD4000; packages sized for the expanded window
|
||||
; require the updated OTAFIX bootloader (or a legacy motatool override). The device must run the matching
|
||||
; OTAFIX bootloader for `applydelta` to actually apply. (detools.c is intentionally NOT built into the app -
|
||||
; only the bootloader decodes.) The [rak4631] base defines this same recipe inline because it carries
|
||||
; board-specific extras (fix_bsec_lib.py, BSEC lib) and a fixed post-script order.
|
||||
[nrf52_lora_ota]
|
||||
|
||||
+12
-8
@@ -220,6 +220,7 @@ uint32_t Dispatcher::getCADFailMaxDuration() const {
|
||||
|
||||
#if MESH_PACKET_LOGGING
|
||||
void Dispatcher::logPacketStart(const char* direction, const Packet* packet, int len) {
|
||||
if (!isUsbLoggingEnabled()) return;
|
||||
#if defined(MESH_COMPACT_PACKET_LOGGING)
|
||||
(void)packet;
|
||||
Serial.printf("%s %d\n", direction, len);
|
||||
@@ -232,6 +233,7 @@ void Dispatcher::logPacketStart(const char* direction, const Packet* packet, int
|
||||
}
|
||||
|
||||
void Dispatcher::logPacketEnd(const Packet* packet) {
|
||||
if (!isUsbLoggingEnabled()) return;
|
||||
#if defined(MESH_COMPACT_PACKET_LOGGING)
|
||||
(void)packet;
|
||||
#else
|
||||
@@ -598,17 +600,19 @@ void Dispatcher::checkRecv() {
|
||||
}
|
||||
if (pkt) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
logPacketStart("RX", pkt, pkt->getRawLength());
|
||||
if (isUsbLoggingEnabled()) {
|
||||
logPacketStart("RX", pkt, pkt->getRawLength());
|
||||
#if !defined(MESH_COMPACT_PACKET_LOGGING)
|
||||
Serial.printf(" SNR=%d RSSI=%d score=%d time=%d", (int)pkt->getSNR(),
|
||||
(int)rssi, (int)(score * 1000), air_time);
|
||||
Serial.printf(" SNR=%d RSSI=%d score=%d time=%d", (int)pkt->getSNR(),
|
||||
(int)rssi, (int)(score * 1000), air_time);
|
||||
|
||||
static uint8_t packet_hash[MAX_HASH_SIZE];
|
||||
pkt->calculatePacketHash(packet_hash);
|
||||
Serial.print(" hash=");
|
||||
mesh::Utils::printHex(Serial, packet_hash, MAX_HASH_SIZE);
|
||||
logPacketEnd(pkt);
|
||||
static uint8_t packet_hash[MAX_HASH_SIZE];
|
||||
pkt->calculatePacketHash(packet_hash);
|
||||
Serial.print(" hash=");
|
||||
mesh::Utils::printHex(Serial, packet_hash, MAX_HASH_SIZE);
|
||||
logPacketEnd(pkt);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
logRx(pkt, pkt->getRawLength(), score); // hook for custom logging
|
||||
|
||||
|
||||
+15
-6
@@ -3,6 +3,7 @@
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <math.h>
|
||||
#include "helpers/UsbLogging.h"
|
||||
|
||||
#define MAX_HASH_SIZE 8
|
||||
#define PUB_KEY_SIZE 32
|
||||
@@ -24,23 +25,23 @@
|
||||
|
||||
#if MESH_DEBUG && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define MESH_DEBUG_PRINT(F, ...) do { if (Serial.availableForWrite() > 0) { Serial.printf("DEBUG: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define MESH_DEBUG_PRINTLN(F, ...) do { if (Serial.availableForWrite() > 0) { Serial.printf("DEBUG: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define MESH_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled() && Serial.availableForWrite() > 0) { Serial.printf("DEBUG: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define MESH_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled() && Serial.availableForWrite() > 0) { Serial.printf("DEBUG: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define MESH_DEBUG_PRINT(...) {}
|
||||
#define MESH_DEBUG_PRINTLN(...) {}
|
||||
#endif
|
||||
|
||||
#if BRIDGE_DEBUG && ARDUINO
|
||||
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { if (Serial.availableForWrite() > 0) { Serial.printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } } while(0)
|
||||
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled() && Serial.availableForWrite() > 0) { Serial.printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define BRIDGE_DEBUG_PRINTLN(...) {}
|
||||
#endif
|
||||
|
||||
#if POWERSAVING_DEBUG && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define POWERSAVING_DEBUG_PRINT(F, ...) Serial.printf("POWERSAVING: " F, ##__VA_ARGS__)
|
||||
#define POWERSAVING_DEBUG_PRINTLN(F, ...) Serial.printf("POWERSAVING: " F "\n", ##__VA_ARGS__)
|
||||
#define POWERSAVING_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("POWERSAVING: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define POWERSAVING_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("POWERSAVING: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define POWERSAVING_DEBUG_PRINT(...) {}
|
||||
#define POWERSAVING_DEBUG_PRINTLN(...) {}
|
||||
@@ -71,7 +72,15 @@ public:
|
||||
// Default no-op: boards that don't care need not implement anything.
|
||||
virtual void onBootComplete() { /* no op */ }
|
||||
virtual uint32_t getIRQGpio() { return -1; } // not supported. Returns DIO1 (SX1262) and DIO0 (SX127x)
|
||||
virtual void sleep(uint32_t secs) { /* no op */ }
|
||||
virtual void sleep(uint32_t secs) {
|
||||
(void)secs;
|
||||
#if defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
|
||||
// These platforms have no shared deep-sleep board implementation. WFI is
|
||||
// still a real CPU idle state and preserves all configured interrupt wake
|
||||
// sources, including the radio and USB.
|
||||
__asm volatile("wfi");
|
||||
#endif
|
||||
}
|
||||
virtual uint32_t getGpio() { return 0; }
|
||||
virtual void setGpio(uint32_t values) {}
|
||||
// Returns true only for physical MCU GPIOs that are safe for the user to
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
#include <Arduino.h>
|
||||
#define ALERT_DEBUG_PRINTLN(...) Serial.printf("Alert: " __VA_ARGS__); Serial.println()
|
||||
#define ALERT_DEBUG_PRINTLN(...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("Alert: " __VA_ARGS__); Serial.println(); } } while (0)
|
||||
#else
|
||||
#define ALERT_DEBUG_PRINTLN(...) do {} while (0)
|
||||
#endif
|
||||
|
||||
+44
-518
@@ -2250,238 +2250,6 @@ 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) {
|
||||
@@ -2687,7 +2455,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' ');
|
||||
const char *key = (num > 0) ? parts[0] : "";
|
||||
const char *value = (num > 1) ? parts[1] : "null";
|
||||
if (_sensors->setSettingValue(key, value)) {
|
||||
uint32_t gps_interval = 0;
|
||||
const bool is_gps_interval = strcmp(key, "gps_interval") == 0;
|
||||
const bool is_gps_toggle = strcmp(key, "gps") == 0;
|
||||
const bool valid_gps_interval = !is_gps_interval
|
||||
|| (parseUint32Strict(value, gps_interval)
|
||||
&& gps_interval <= mesh::gps::MAX_UPDATE_INTERVAL_SEC);
|
||||
const bool valid_gps_toggle = !is_gps_toggle
|
||||
|| strcmp(value, "0") == 0 || strcmp(value, "1") == 0;
|
||||
|
||||
if (!valid_gps_interval) {
|
||||
strcpy(reply, "gps_interval must be 0..86400 seconds");
|
||||
} else if (!valid_gps_toggle) {
|
||||
strcpy(reply, "gps must be 0 or 1");
|
||||
} else if (_sensors->setSettingValue(key, value)) {
|
||||
if (is_gps_interval) {
|
||||
_prefs->gps_interval = gps_interval;
|
||||
savePrefs();
|
||||
} else if (is_gps_toggle) {
|
||||
_prefs->gps_enabled = strcmp(value, "1") == 0 ? 1 : 0;
|
||||
savePrefs();
|
||||
}
|
||||
strcpy(reply, "ok");
|
||||
} else {
|
||||
strcpy(reply, "can't find custom var");
|
||||
@@ -2921,7 +2709,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
} else {
|
||||
strcpy(reply, "Unknown command");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
@@ -3093,6 +2880,22 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
|
||||
|
||||
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
if (strncmp(config, "usb.logging", 11) == 0
|
||||
&& (config[11] == 0 || config[11] == ' ' || config[11] == '\t')) {
|
||||
const char* value = &config[11];
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
if (strcmp(value, "on") == 0 || strcmp(value, "off") == 0) {
|
||||
const bool enabled = strcmp(value, "on") == 0;
|
||||
mesh::setUsbLoggingEnabled(enabled);
|
||||
snprintf(reply, 160, "OK - USB logging %s until reboot",
|
||||
enabled ? "on" : "off");
|
||||
} else {
|
||||
strcpy(reply, "Error: usage set usb.logging on|off");
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
||||
if (isGpioConfig(config)) {
|
||||
const UserGpio::SetResult result = _user_gpio.handleSet(
|
||||
@@ -3169,215 +2972,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#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.fem.txgain ", 17) == 0) {
|
||||
const char* value = &config[17];
|
||||
if (!_board->canControlLoRaFemPaGain()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
||||
strcpy(reply, "Error: state must be on or off");
|
||||
} else {
|
||||
const bool enabled = strcmp(value, "on") == 0;
|
||||
if (_board->setLoRaFemPaGainEnabled(enabled)) {
|
||||
_prefs->radio_fem_txgain = enabled ? 1 : 0;
|
||||
savePrefs();
|
||||
strcpy(reply, enabled
|
||||
? "OK - LoRa FEM TX gain on" : "OK - LoRa FEM TX gain off");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM TX 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, "rxdelay ", 8) == 0) {
|
||||
float delay = 0.0f;
|
||||
if (mesh::cli::parseDecimalStrict(&config[8], delay)
|
||||
&& delay >= 0.0f && delay <= 20.0f) {
|
||||
_prefs->rx_delay_base = delay;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-20");
|
||||
}
|
||||
} else if (memcmp(config, "txdelay ", 8) == 0) {
|
||||
float factor = 0.0f;
|
||||
if (mesh::cli::parseDecimalStrict(&config[8], factor)
|
||||
&& factor >= 0.0f && factor <= 2.0f) {
|
||||
_prefs->tx_delay_factor = factor;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-2");
|
||||
}
|
||||
} 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;
|
||||
savePrefs(PrefsSaveRouting::Scope::Both);
|
||||
#else
|
||||
savePrefs();
|
||||
#endif
|
||||
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, "Unsupported in this firmware: %s", config);
|
||||
}
|
||||
return;
|
||||
#else
|
||||
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
|
||||
strcpy(reply, "Error, unsupported on this role");
|
||||
return;
|
||||
@@ -4254,11 +3848,17 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
sprintf(reply, "unknown config: %s", config);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
if (strcmp(config, "usb.logging") == 0) {
|
||||
snprintf(reply, 160, "> %s",
|
||||
mesh::isUsbLoggingEnabled() ? "on" : "off");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
||||
if (isGpioConfig(config)) {
|
||||
_user_gpio.handleGet(config + 4, reply, 160);
|
||||
@@ -4319,79 +3919,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#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.fem.txgain") == 0) {
|
||||
if (!_board->canControlLoRaFemPaGain()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s",
|
||||
_board->isLoRaFemPaGainEnabled() ? "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, "rxdelay") == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
|
||||
} else if (strcmp(config, "txdelay") == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
|
||||
} 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, "Unsupported in this firmware: %s", config);
|
||||
}
|
||||
return;
|
||||
#else
|
||||
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
|
||||
strcpy(reply, "Error, unsupported on this role");
|
||||
return;
|
||||
@@ -4686,7 +4213,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
mesh::cli::formatUnknownSetting(reply, 160, config);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CommonCLI::handleDelCmd(char* command, char* reply) {
|
||||
|
||||
@@ -148,7 +148,7 @@ public:
|
||||
uint8_t reboot_interval = 0; // hours, 0-255 (default 0=disable)
|
||||
// Gps settings
|
||||
uint8_t gps_enabled = 0;
|
||||
uint32_t gps_interval = 0; // in seconds
|
||||
uint32_t gps_interval = 0; // seconds; 0 uses the 1-second default
|
||||
uint8_t advert_loc_policy = 0;
|
||||
uint32_t discovery_mod_timestamp = 0;
|
||||
float adc_multiplier = 0;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace gps {
|
||||
|
||||
static const uint32_t DEFAULT_UPDATE_INTERVAL_SEC = 1;
|
||||
static const uint32_t MAX_UPDATE_INTERVAL_SEC = 24UL * 60UL * 60UL;
|
||||
|
||||
inline bool parseUpdateInterval(const char* value, uint32_t& configured_sec) {
|
||||
if (value == nullptr || *value == 0) return false;
|
||||
|
||||
uint32_t parsed = 0;
|
||||
for (const char* p = value; *p; ++p) {
|
||||
if (*p < '0' || *p > '9') return false;
|
||||
uint32_t digit = (uint32_t)(*p - '0');
|
||||
if (parsed > (MAX_UPDATE_INTERVAL_SEC - digit) / 10UL) return false;
|
||||
parsed = parsed * 10UL + digit;
|
||||
}
|
||||
|
||||
configured_sec = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint32_t effectiveUpdateIntervalSec(uint32_t configured_sec) {
|
||||
return configured_sec == 0 ? DEFAULT_UPDATE_INTERVAL_SEC : configured_sec;
|
||||
}
|
||||
|
||||
inline uint32_t updateIntervalMillis(uint32_t configured_sec) {
|
||||
return effectiveUpdateIntervalSec(configured_sec) * 1000UL;
|
||||
}
|
||||
|
||||
} // namespace gps
|
||||
} // namespace mesh
|
||||
@@ -49,14 +49,10 @@ void SensorManager::updateGpsTelemetryCache(float lat, float lon, float altitude
|
||||
}
|
||||
|
||||
void SensorManager::maybeStopGpsForTelemetry(unsigned long now) {
|
||||
#if defined(PERSISTANT_GPS) || defined(FORCE_GPS_ALIVE)
|
||||
(void)now;
|
||||
#else
|
||||
if (telemetryGpsActive() && !gps_user_enabled && !gps_acquiring && !gpsTelemetryHoldActive(now)) {
|
||||
telemetryGpsStop();
|
||||
gps_next_cache_update_at = now + GPS_TELEMETRY_CACHE_INTERVAL_SEC * 1000UL;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SensorManager::beginGpsTelemetryAcquisition(unsigned long now) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <CayenneLPP.h>
|
||||
#include "GpsPowerPolicy.h"
|
||||
#include "sensors/LocationProvider.h"
|
||||
#include <string.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#define TELEM_PERM_BASE 0x01 // 'base' permission includes battery
|
||||
@@ -32,6 +34,7 @@ class SensorManager {
|
||||
unsigned long gps_hold_until = 0;
|
||||
unsigned long gps_acquire_started_at = 0;
|
||||
unsigned long gps_stable_started_at = 0;
|
||||
uint32_t gps_update_interval_sec = 0;
|
||||
|
||||
bool gpsTelemetryHoldActive(unsigned long now) const;
|
||||
bool gpsTelemetryCacheFresh(unsigned long now) const;
|
||||
@@ -50,6 +53,15 @@ protected:
|
||||
void loopGpsTelemetry(unsigned long now);
|
||||
void setGpsTelemetryUserEnabled(bool enabled);
|
||||
bool isGpsTelemetryUserEnabled() const { return gps_user_enabled; }
|
||||
bool gpsTelemetryReceiverRequired(unsigned long now) const {
|
||||
return gps_acquiring || gpsTelemetryHoldActive(now);
|
||||
}
|
||||
bool setGpsUpdateIntervalValue(const char* value) {
|
||||
return mesh::gps::parseUpdateInterval(value, gps_update_interval_sec);
|
||||
}
|
||||
uint32_t getGpsUpdateIntervalMillis() const {
|
||||
return mesh::gps::updateIntervalMillis(gps_update_interval_sec);
|
||||
}
|
||||
#endif
|
||||
|
||||
public:
|
||||
@@ -66,7 +78,17 @@ public:
|
||||
virtual int getNumSettings() const { return 0; }
|
||||
virtual const char* getSettingName(int i) const { return NULL; }
|
||||
virtual const char* getSettingValue(int i) const { return NULL; }
|
||||
virtual bool setSettingValue(const char* name, const char* value) { return false; }
|
||||
virtual bool setSettingValue(const char* name, const char* value) {
|
||||
#if ENV_INCLUDE_GPS
|
||||
if (strcmp(name, "gps_interval") == 0) {
|
||||
return setGpsUpdateIntervalValue(value);
|
||||
}
|
||||
#else
|
||||
(void)name;
|
||||
(void)value;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
virtual LocationProvider* getLocationProvider() { return NULL; }
|
||||
virtual void setPowerSavingEnabled(bool enabled) { powersaving_enabled = enabled; }
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "UsbLogging.h"
|
||||
|
||||
#if defined(ARDUINO)
|
||||
#include <atomic>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
static std::atomic<bool> usb_logging_enabled{true};
|
||||
|
||||
bool isUsbLoggingEnabled() {
|
||||
return usb_logging_enabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void setUsbLoggingEnabled(bool enabled) {
|
||||
usb_logging_enabled.store(enabled, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
// Logging artifacts compile at least one of these two diagnostics. Keep the
|
||||
// command surface out of ordinary images while providing one runtime gate for
|
||||
// every diagnostic category that writes to the USB Serial stream.
|
||||
#if defined(ARDUINO) && \
|
||||
((defined(MESH_DEBUG) && MESH_DEBUG) || \
|
||||
(defined(MESH_PACKET_LOGGING) && MESH_PACKET_LOGGING))
|
||||
#define MESH_USB_LOGGING_AVAILABLE 1
|
||||
#else
|
||||
#define MESH_USB_LOGGING_AVAILABLE 0
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO)
|
||||
namespace mesh {
|
||||
|
||||
// This is intentionally session-only. Logging artifacts start enabled after
|
||||
// every boot so a stale setting cannot make a diagnostic image look silent.
|
||||
bool isUsbLoggingEnabled();
|
||||
void setUsbLoggingEnabled(bool enabled);
|
||||
|
||||
} // namespace mesh
|
||||
#endif
|
||||
@@ -209,6 +209,7 @@ static unsigned long s_wifi_disconnect_time = 0;
|
||||
// #region agent log
|
||||
static void agentLogHeap(const char* location, const char* message, const char* hypothesisId,
|
||||
size_t free_h, size_t max_alloc, unsigned long internal_free, unsigned long spiram_free) {
|
||||
if (!mesh::isUsbLoggingEnabled()) return;
|
||||
char buf[320];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"sessionId\":\"debug-session\",\"location\":\"%s\",\"message\":\"%s\",\"hypothesisId\":\"%s\","
|
||||
|
||||
@@ -31,8 +31,8 @@ class MeshSNMPAgent; // Forward declaration
|
||||
// USB CDC-aware debug macros: only print if Serial is ready (non-blocking check)
|
||||
// Serial.availableForWrite() returns bytes available in write buffer (>0 means ready)
|
||||
// This prevents hangs when USB CDC isn't ready yet (e.g., ESP32-S3 native USB)
|
||||
#define MQTT_DEBUG_PRINT(F, ...) do { if (Serial.availableForWrite() > 0) { Serial.printf("MQTT: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define MQTT_DEBUG_PRINTLN(F, ...) do { if (Serial.availableForWrite() > 0) { Serial.printf("MQTT: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define MQTT_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled() && Serial.availableForWrite() > 0) { Serial.printf("MQTT: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define MQTT_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled() && Serial.availableForWrite() > 0) { Serial.printf("MQTT: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define MQTT_DEBUG_PRINT(...) {}
|
||||
#define MQTT_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -64,8 +64,8 @@ public:
|
||||
|
||||
#if ESPNOW_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define ESPNOW_DEBUG_PRINT(F, ...) Serial.printf("ESP-Now: " F, ##__VA_ARGS__)
|
||||
#define ESPNOW_DEBUG_PRINTLN(F, ...) Serial.printf("ESP-Now: " F "\n", ##__VA_ARGS__)
|
||||
#define ESPNOW_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("ESP-Now: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define ESPNOW_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("ESP-Now: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define ESPNOW_DEBUG_PRINT(...) {}
|
||||
#define ESPNOW_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "../BaseSerialInterface.h"
|
||||
#include "../BleTxStallWatchdog.h"
|
||||
#include "../UsbLogging.h"
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
@@ -114,8 +115,8 @@ public:
|
||||
|
||||
#if BLE_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__)
|
||||
#define BLE_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("BLE: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("BLE: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define BLE_DEBUG_PRINT(...) {}
|
||||
#define BLE_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../BaseSerialInterface.h"
|
||||
#include "../UsbLogging.h"
|
||||
#include <WiFi.h>
|
||||
|
||||
class SerialWifiInterface : public BaseSerialInterface {
|
||||
@@ -66,8 +67,8 @@ public:
|
||||
|
||||
#if WIFI_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define WIFI_DEBUG_PRINT(F, ...) Serial.printf("WiFi: " F, ##__VA_ARGS__)
|
||||
#define WIFI_DEBUG_PRINTLN(F, ...) Serial.printf("WiFi: " F "\n", ##__VA_ARGS__)
|
||||
#define WIFI_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("WiFi: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define WIFI_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("WiFi: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define WIFI_DEBUG_PRINT(...) {}
|
||||
#define WIFI_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -55,8 +55,9 @@ void TBeamBoard::scanDevices(TwoWire *w)
|
||||
uint8_t err, addr;
|
||||
int nDevices = 0;
|
||||
uint32_t start = 0;
|
||||
const bool usb_logging = mesh::isUsbLoggingEnabled();
|
||||
|
||||
Serial.println("Scanning I2C for Devices");
|
||||
if (usb_logging) Serial.println("Scanning I2C for Devices");
|
||||
for (addr = 1; addr < 127; addr++) {
|
||||
start = millis();
|
||||
w->beginTransmission(addr); delay(2);
|
||||
@@ -66,45 +67,49 @@ void TBeamBoard::scanDevices(TwoWire *w)
|
||||
switch (addr) {
|
||||
case 0x77:
|
||||
case 0x76:
|
||||
Serial.println("\tFound BME280 Sensor");
|
||||
if (usb_logging) Serial.println("\tFound BME280 Sensor");
|
||||
deviceOnline |= BME280_ONLINE;
|
||||
break;
|
||||
case 0x34:
|
||||
Serial.println("\tFound AXP192/AXP2101 PMU");
|
||||
if (usb_logging) Serial.println("\tFound AXP192/AXP2101 PMU");
|
||||
deviceOnline |= POWERMANAGE_ONLINE;
|
||||
break;
|
||||
case 0x3C:
|
||||
Serial.println("\tFound SSD1306/SH1106 display");
|
||||
if (usb_logging) Serial.println("\tFound SSD1306/SH1106 display");
|
||||
deviceOnline |= DISPLAY_ONLINE;
|
||||
break;
|
||||
case 0x51:
|
||||
Serial.println("\tFound PCF8563 RTC");
|
||||
if (usb_logging) Serial.println("\tFound PCF8563 RTC");
|
||||
deviceOnline |= PCF8563_ONLINE;
|
||||
break;
|
||||
case 0x1C:
|
||||
Serial.println("\tFound QMC6310 MAG Sensor");
|
||||
if (usb_logging) Serial.println("\tFound QMC6310 MAG Sensor");
|
||||
deviceOnline |= QMC6310_ONLINE;
|
||||
break;
|
||||
default:
|
||||
Serial.print("\tI2C device found at address 0x");
|
||||
if (addr < 16) {
|
||||
Serial.print("0");
|
||||
if (usb_logging) {
|
||||
Serial.print("\tI2C device found at address 0x");
|
||||
if (addr < 16) {
|
||||
Serial.print("0");
|
||||
}
|
||||
Serial.print(addr, HEX);
|
||||
Serial.println(" !");
|
||||
}
|
||||
Serial.print(addr, HEX);
|
||||
Serial.println(" !");
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (err == 4) {
|
||||
Serial.print("Unknow error at address 0x");
|
||||
if (addr < 16) {
|
||||
Serial.print("0");
|
||||
if (usb_logging) {
|
||||
Serial.print("Unknow error at address 0x");
|
||||
if (addr < 16) {
|
||||
Serial.print("0");
|
||||
}
|
||||
Serial.println(addr, HEX);
|
||||
}
|
||||
Serial.println(addr, HEX);
|
||||
}
|
||||
}
|
||||
if (nDevices == 0)
|
||||
Serial.println("No I2C devices found\n");
|
||||
if (!usb_logging) return;
|
||||
if (nDevices == 0) Serial.println("No I2C devices found\n");
|
||||
|
||||
Serial.println("Scan for devices is complete.");
|
||||
Serial.println("\n");
|
||||
@@ -115,6 +120,7 @@ void TBeamBoard::scanDevices(TwoWire *w)
|
||||
}
|
||||
void TBeamBoard::printPMU()
|
||||
{
|
||||
if (!mesh::isUsbLoggingEnabled()) return;
|
||||
Serial.print("isCharging:"); Serial.println(PMU->isCharging() ? "YES" : "NO");
|
||||
Serial.print("isDischarge:"); Serial.println(PMU->isDischarge() ? "YES" : "NO");
|
||||
Serial.print("isVbusIn:"); Serial.println(PMU->isVbusIn() ? "YES" : "NO");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../BaseSerialInterface.h"
|
||||
#include "../UsbLogging.h"
|
||||
|
||||
#ifndef ETHERNET_TCP_PORT
|
||||
#define ETHERNET_TCP_PORT 5000
|
||||
@@ -69,9 +70,9 @@ class SerialEthernetInterface : public BaseSerialInterface {
|
||||
|
||||
#if ETHERNET_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__)
|
||||
#define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__)
|
||||
#define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf("ETH: " name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3])
|
||||
#define ETHERNET_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("ETH: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define ETHERNET_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("ETH: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define ETHERNET_DEBUG_PRINT_IP(name, ip) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("ETH: " name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]); } } while(0)
|
||||
#else
|
||||
#define ETHERNET_DEBUG_PRINT(...) {}
|
||||
#define ETHERNET_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "../BaseSerialInterface.h"
|
||||
#include "../BleTxStallWatchdog.h"
|
||||
#include "../UsbLogging.h"
|
||||
#include "SecuritySessionTimer.h"
|
||||
#include <bluefruit.h>
|
||||
#include <atomic>
|
||||
@@ -94,8 +95,8 @@ public:
|
||||
|
||||
#if BLE_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__)
|
||||
#define BLE_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("BLE: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf("BLE: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define BLE_DEBUG_PRINT(...) {}
|
||||
#define BLE_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -416,6 +416,23 @@ void ota_reboot_to_apply() { esp_restart(); } // boots the slot armed by ota_ap
|
||||
|
||||
#elif defined(NRF52_PLATFORM) // single-slot: verify + mark APPROVED + hand off to the bootloader
|
||||
|
||||
struct InplacePatchDims {
|
||||
uint32_t memory = 0;
|
||||
uint32_t segment = 0;
|
||||
uint32_t shift = 0;
|
||||
uint32_t from = 0;
|
||||
uint32_t to = 0;
|
||||
};
|
||||
|
||||
static bool parse_inplace_patch_dims(const uint8_t* payload, uint32_t payload_len,
|
||||
InplacePatchDims& d) {
|
||||
if (!payload || payload_len < 2 || ((payload[0] >> 4) & 0x07u) != 1u) return false;
|
||||
ByteReader r(payload, payload_len);
|
||||
r.u8(); // patch type/compression header
|
||||
return r.detools_size(d.memory) && r.detools_size(d.segment) && r.detools_size(d.shift) &&
|
||||
r.detools_size(d.from) && r.detools_size(d.to) && r.ok;
|
||||
}
|
||||
|
||||
// ESP32 A/B-only entry points are unsupported on nRF52.
|
||||
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
|
||||
bool ota_apply_set_manifest(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st) { st = ApplyState(); return false; }
|
||||
@@ -424,12 +441,20 @@ bool ota_apply_commit() { return false; }
|
||||
bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "use ota_apply_mota_nrf52"); return false; }
|
||||
|
||||
void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return)
|
||||
uint8_t stage_handoff = GPREGRET2_OTA_STAGE_LEGACY;
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
if (ota_nrf52_effective_stage_ceiling() == MOTA_NRF52_STAGE_CEILING_EXPANDED)
|
||||
stage_handoff = GPREGRET2_OTA_STAGE_EXPANDED;
|
||||
#endif
|
||||
uint8_t sd_en = 0;
|
||||
sd_softdevice_is_enabled(&sd_en);
|
||||
if (sd_en) { // POWER is SD-restricted while the SoftDevice runs
|
||||
sd_power_gpregret_clr(1, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(1, stage_handoff);
|
||||
sd_power_gpregret_clr(0, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(0, GPREGRET_OTA_APPLY);
|
||||
} else {
|
||||
NRF_POWER->GPREGRET2 = stage_handoff;
|
||||
NRF_POWER->GPREGRET = GPREGRET_OTA_APPLY;
|
||||
}
|
||||
NVIC_SystemReset(); // does not return
|
||||
@@ -510,6 +535,31 @@ static bool ota_apply_mota_nrf52_impl(const uint8_t* buf, uint32_t len,
|
||||
if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
|
||||
}
|
||||
|
||||
// 4) detools geometry. memory_size is selected by motatool for this exact staged address; reject a
|
||||
// mismatched/legacy package before writing APRV so the bootloader never starts a doomed in-place apply.
|
||||
{
|
||||
InplacePatchDims d;
|
||||
if (!parse_inplace_patch_dims(m.payload, m.payload_size, d)) {
|
||||
strcpy(msg, "bad in-place patch header"); return false;
|
||||
}
|
||||
if (d.memory == 0 || d.segment != MOTA_NRF52_FLASH_PAGE || d.shift > d.memory ||
|
||||
d.shift % d.segment != 0 || d.from > d.memory - d.shift || d.to > d.memory ||
|
||||
d.to != m.image_size || (self_valid && d.from != fi.image_len)) {
|
||||
strcpy(msg, "invalid in-place patch geometry"); return false;
|
||||
}
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
const uint32_t mota_start = (uint32_t)(uintptr_t)buf;
|
||||
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
||||
if (mota_start < app_base || mota_start >= stage_ceiling ||
|
||||
d.memory > mota_start - app_base) {
|
||||
snprintf(msg, 159, "patch memory 0x%x exceeds staging at 0x%x",
|
||||
(unsigned)d.memory, (unsigned)mota_start);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// mark the staged manifest APPROVED in flash (buf is the memory-mapped staging region, so
|
||||
// m.approval is a real flash address). NOR-clear over the erased 0xFFFFFFFF -> "APRV".
|
||||
uint32_t approval_addr = (uint32_t)(uintptr_t)m.approval;
|
||||
|
||||
@@ -25,10 +25,11 @@ struct OtaBlCaps {
|
||||
bool present = false;
|
||||
uint16_t apply_abi = 0; // max .mota format_ver the bootloader can apply
|
||||
uint16_t codec_mask = 0; // bit i set => can apply codec_id i (in-place delta = bit 2)
|
||||
uint8_t storage_flags = 0; // bit 0 => raw-SD handoff/apply is supported
|
||||
uint8_t storage_flags = 0; // OTA_BL_STORAGE_* capability bits
|
||||
};
|
||||
|
||||
static const uint8_t OTA_BL_STORAGE_SD = 0x01;
|
||||
static const uint8_t OTA_BL_STORAGE_SD = 0x01;
|
||||
static const uint8_t OTA_BL_STORAGE_STAGE_CEILING = 0x02;
|
||||
|
||||
// Scan the bootloader flash region for the marker. Returns {present=false} if not found / non-nRF52.
|
||||
inline OtaBlCaps ota_bootloader_caps() {
|
||||
@@ -54,5 +55,23 @@ inline bool ota_bootloader_can_apply(uint8_t format_ver, uint8_t codec_id) {
|
||||
return c.present && c.apply_abi >= format_ver && (c.codec_mask & (1u << codec_id)) != 0;
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// Use the layout's larger ceiling only when the installed bootloader advertises the matching
|
||||
// GPREGRET2 handoff. New applications remain filesystem-safe with older OTAFIX bootloaders by retaining
|
||||
// the legacy 0xD4000 ceiling. Packages sized for the expanded window still require the newer bootloader.
|
||||
inline uint32_t ota_nrf52_effective_stage_ceiling(const OtaBlCaps& c) {
|
||||
const uint32_t desired = mota_nrf52_layout_stage_ceiling();
|
||||
if (desired == MOTA_NRF52_STAGE_CEILING_EXPANDED) {
|
||||
if (!c.present || !(c.storage_flags & OTA_BL_STORAGE_STAGE_CEILING))
|
||||
return MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
}
|
||||
return desired;
|
||||
}
|
||||
|
||||
inline uint32_t ota_nrf52_effective_stage_ceiling() {
|
||||
return ota_nrf52_effective_stage_ceiling(ota_bootloader_caps());
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -52,6 +52,26 @@ struct ByteReader {
|
||||
const uint8_t* r = p + n; n += k; return r;
|
||||
}
|
||||
void skip(uint32_t k) { if (!fits(k)) { ok = false; return; } n += k; }
|
||||
|
||||
// Positive detools header size (signed varint encoding, but flash geometry never accepts negatives).
|
||||
bool detools_size(uint32_t& out) {
|
||||
if (!fits(1)) { ok = false; return false; }
|
||||
uint8_t byte = p[n++];
|
||||
if (byte & 0x40u) { ok = false; return false; }
|
||||
uint32_t value = byte & 0x3Fu;
|
||||
uint32_t shift = 6;
|
||||
while (byte & 0x80u) {
|
||||
if (!fits(1)) { ok = false; return false; }
|
||||
byte = p[n++];
|
||||
const uint32_t bits = byte & 0x7Fu;
|
||||
if (shift >= 32 || bits > (UINT32_MAX >> shift)) { ok = false; return false; }
|
||||
value |= bits << shift;
|
||||
shift += 7;
|
||||
}
|
||||
if (value > 0x7FFFFFFFu) { ok = false; return false; }
|
||||
out = value;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -523,7 +523,12 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
else
|
||||
snprintf(reply + n, 160 - n, " | bootloader: NO SD mota-apply support (install will refuse)");
|
||||
#else
|
||||
if (bl.present) snprintf(reply + n, 160 - n, " | bootloader: apply OK (abi=%u codecs=0x%x)", bl.apply_abi, bl.codec_mask);
|
||||
if (bl.present) {
|
||||
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling(bl);
|
||||
snprintf(reply + n, 160 - n,
|
||||
" | bootloader: apply OK (abi=%u codecs=0x%x stage=%05X)",
|
||||
bl.apply_abi, bl.codec_mask, (unsigned)stage_ceiling);
|
||||
}
|
||||
else snprintf(reply + n, 160 - n, " | bootloader: NO mota-apply support (delta install will refuse)");
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
// touches a non-debug or test build.
|
||||
#if defined(OTA_DEBUG) && defined(ARDUINO)
|
||||
#include <Arduino.h>
|
||||
#define OTA_DBG(...) do { Serial.printf(__VA_ARGS__); } while (0)
|
||||
#include <helpers/UsbLogging.h>
|
||||
#define OTA_DBG(...) do { if (mesh::isUsbLoggingEnabled()) { Serial.printf(__VA_ARGS__); } } while (0)
|
||||
#else
|
||||
#define OTA_DBG(...) do {} while (0)
|
||||
#endif
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
// Shared OTA flash-layout constants for the nRF52840 single-slot delta-apply path.
|
||||
//
|
||||
// The running app occupies [APP_BASE, app_end]; the primary LittleFS (InternalFS) starts at FS_START.
|
||||
// MeshCore stages a verified+approved `.mota` in the free flash below FS_START (bottom-aligned), then
|
||||
// sets GPREGRET_OTA_APPLY and resets; the bootloader scans [APP_BASE, FS_START) for it and applies it
|
||||
// in place. APP_BASE is obtained from the linker so S140 v6 (0x26000) and v7 (0x27000) both work. The
|
||||
// bootloader independently uses DFU_BANK_0_REGION_START, which resolves to the same address.
|
||||
// The running app occupies [APP_BASE, app_end]. Internal ExtraFS, when actually used, starts at
|
||||
// 0xD4000; primary InternalFS starts at 0xED000. MeshCore derives the safe staging ceiling from the
|
||||
// linked application region and secondary-storage type, bottom-aligns the `.mota` below it, and passes
|
||||
// the choice to a capable bootloader in GPREGRET2. APP_BASE is obtained from the linker so S140 v6
|
||||
// (0x26000) and v7 (0x27000) both work. The bootloader independently uses DFU_BANK_0_REGION_START.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -17,6 +17,8 @@ static const uint32_t MOTA_NRF52_APP_BASE_S140_V6 = 0x00026000u;
|
||||
static const uint32_t MOTA_NRF52_APP_BASE_S140_V7 = 0x00027000u;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
extern "C" uint32_t __flash_arduino_start[]; // nrf52_common.ld: ORIGIN(FLASH)
|
||||
extern "C" uint32_t __flash_arduino_end[]; // nrf52_common.ld: ORIGIN(FLASH) + LENGTH(FLASH)
|
||||
extern "C" const uint8_t g_meshcore_internal_extrafs __attribute__((weak));
|
||||
inline uint32_t mota_nrf52_app_base() {
|
||||
return (uint32_t)(uintptr_t)__flash_arduino_start;
|
||||
}
|
||||
@@ -24,22 +26,22 @@ inline uint32_t mota_nrf52_app_base() {
|
||||
// Native geometry tests have no linker script; default their runtime helper to the v6 layout.
|
||||
inline uint32_t mota_nrf52_app_base() { return MOTA_NRF52_APP_BASE_S140_V6; }
|
||||
#endif
|
||||
// Staging ceiling: the lowest filesystem region above the app. nRF52840 ExtraFS linker scripts place
|
||||
// ExtraFS at 0xD4000..0xED000 (and InternalFS at 0xED000), while default scripts leave that range free.
|
||||
// Staging below 0xD4000 therefore never touches either filesystem.
|
||||
static const uint32_t MOTA_NRF52_FS_START = 0x000D4000u; // ExtraFS start (universal staging ceiling)
|
||||
// End of the normal nRF52840 application region. The SD-backed MeshTower V2
|
||||
// target can use this whole range because its staged container is off-chip.
|
||||
// InternalFS begins here and must never be erased by the bootloader.
|
||||
static const uint32_t MOTA_NRF52_APP_END = 0x000ED000u;
|
||||
static const uint32_t MOTA_NRF52_EXTRAFS_START = 0x000D4000u;
|
||||
static const uint32_t MOTA_NRF52_APP_END = 0x000ED000u;
|
||||
static const uint32_t MOTA_NRF52_STAGE_CEILING_LEGACY = MOTA_NRF52_EXTRAFS_START;
|
||||
static const uint32_t MOTA_NRF52_STAGE_CEILING_EXPANDED = MOTA_NRF52_APP_END;
|
||||
// Compatibility name for code that refers to the fixed lower boundary. New staging code must use an
|
||||
// explicit ceiling so it cannot accidentally cross an Internal ExtraFS.
|
||||
static const uint32_t MOTA_NRF52_FS_START = MOTA_NRF52_EXTRAFS_START;
|
||||
static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
|
||||
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu;
|
||||
|
||||
// In-place patches are built with --inplace-memory = this (the apply workspace, from APP_BASE up).
|
||||
// It must hold the new image (~520 KB) yet leave the staged mota room below FS_START: workspace ends
|
||||
// at APP_BASE+this = 0xBE000 (S140 v6) or 0xBF000 (v7), leaving 88/84 KB for the staged delta. The
|
||||
// bootloader also bounds writes to < the (scanned) mota start, so a mis-sized memory still fails safe.
|
||||
static const uint32_t MOTA_NRF52_INPLACE_MEMORY = 0x00098000u; // 608 KB
|
||||
// Firmware without a valid EndF and older host tooling fall back to this conservative apply workspace.
|
||||
// New motatool builds read the firmware's appended layout record and derive memory_size from the actual
|
||||
// staged-container start; the app and bootloader both validate that per-patch value before writing.
|
||||
static const uint32_t MOTA_NRF52_FALLBACK_INPLACE_MEMORY = 0x00098000u; // 608 KB
|
||||
|
||||
// Bootloader flash region (nRF52840: 39 KB ending just below the CF2/MBR-params pages). The app scans
|
||||
// this for the bootloader capability marker (OtaBlInfo.h) to know whether THIS device's bootloader can
|
||||
@@ -51,44 +53,93 @@ static const uint32_t MOTA_NRF52_BL_END = 0x000FE000u;
|
||||
// BUILD rather than silently letting a stage/apply corrupt the filesystem (user prefs) or the app.
|
||||
static_assert((MOTA_NRF52_APP_BASE_S140_V6 % MOTA_NRF52_FLASH_PAGE) == 0, "S140 v6 base must be page-aligned");
|
||||
static_assert((MOTA_NRF52_APP_BASE_S140_V7 % MOTA_NRF52_FLASH_PAGE) == 0, "S140 v7 base must be page-aligned");
|
||||
static_assert((MOTA_NRF52_FS_START % MOTA_NRF52_FLASH_PAGE) == 0, "FS_START must be page-aligned");
|
||||
static_assert(MOTA_NRF52_FS_START < MOTA_NRF52_BL_START, "staging (+FS) must end below the bootloader");
|
||||
static_assert((MOTA_NRF52_STAGE_CEILING_LEGACY % MOTA_NRF52_FLASH_PAGE) == 0,
|
||||
"legacy staging ceiling must be page-aligned");
|
||||
static_assert((MOTA_NRF52_STAGE_CEILING_EXPANDED % MOTA_NRF52_FLASH_PAGE) == 0,
|
||||
"expanded staging ceiling must be page-aligned");
|
||||
static_assert(MOTA_NRF52_STAGE_CEILING_LEGACY < MOTA_NRF52_STAGE_CEILING_EXPANDED,
|
||||
"legacy staging ceiling must precede expanded ceiling");
|
||||
static_assert(MOTA_NRF52_STAGE_CEILING_EXPANDED < MOTA_NRF52_BL_START,
|
||||
"staging and filesystems must end below the bootloader");
|
||||
static_assert(MOTA_NRF52_BL_START < MOTA_NRF52_BL_END, "bootloader region must be non-empty");
|
||||
// The in-place apply workspace [APP_BASE, APP_BASE+INPLACE_MEMORY) must end at/below the staging ceiling,
|
||||
// so an in-place apply never writes into ExtraFS/InternalFS (where user prefs live).
|
||||
static_assert(MOTA_NRF52_APP_BASE_S140_V7 + MOTA_NRF52_INPLACE_MEMORY <= MOTA_NRF52_FS_START,
|
||||
"in-place apply workspace must end at or below the staging ceiling");
|
||||
static_assert(MOTA_NRF52_APP_BASE_S140_V7 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY <=
|
||||
MOTA_NRF52_STAGE_CEILING_LEGACY,
|
||||
"fallback workspace must end at or below the legacy staging ceiling");
|
||||
|
||||
inline bool mota_nrf52_layout_valid(uint32_t app_base) {
|
||||
return (app_base % MOTA_NRF52_FLASH_PAGE) == 0 && app_base < MOTA_NRF52_FS_START &&
|
||||
(uint64_t)app_base + MOTA_NRF52_INPLACE_MEMORY <= MOTA_NRF52_FS_START;
|
||||
// Pure policy helper. Actual internal ExtraFS use always wins and keeps the legacy ceiling. Otherwise
|
||||
// either supported linker region may safely reclaim through InternalFS at 0xED000. This handles
|
||||
// repeaters, XIAO/QSPI companions, and future boards without maintaining a board-name allowlist. Unknown
|
||||
// linker geometry falls back to the legacy ceiling.
|
||||
inline uint32_t mota_nrf52_stage_ceiling_for_layout(uint32_t linked_app_end,
|
||||
bool uses_internal_extrafs) {
|
||||
if (uses_internal_extrafs) return MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
if (linked_app_end == MOTA_NRF52_APP_END || linked_app_end == MOTA_NRF52_EXTRAFS_START)
|
||||
return MOTA_NRF52_STAGE_CEILING_EXPANDED;
|
||||
return MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
}
|
||||
|
||||
inline uint32_t mota_nrf52_stage_capacity(uint32_t app_base) {
|
||||
return mota_nrf52_layout_valid(app_base)
|
||||
? MOTA_NRF52_FS_START - (app_base + MOTA_NRF52_INPLACE_MEMORY) : 0;
|
||||
inline uint32_t mota_nrf52_layout_stage_ceiling() {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// DataStore.cpp defines this symbol only for a companion that really constructs internal ExtraFS.
|
||||
// An undefined weak symbol has address zero, so other roles do not inherit nrf52_base's broad EXTRAFS
|
||||
// feature define as a false-positive storage reservation.
|
||||
const bool uses_internal_extrafs = (uintptr_t)&g_meshcore_internal_extrafs != 0u;
|
||||
return mota_nrf52_stage_ceiling_for_layout((uint32_t)(uintptr_t)__flash_arduino_end,
|
||||
uses_internal_extrafs);
|
||||
#else
|
||||
// Native callers use the explicit-ceiling helpers below; keep the no-argument fallback conservative.
|
||||
return MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool mota_nrf52_stage_ceiling_valid(uint32_t stage_ceiling) {
|
||||
return stage_ceiling == MOTA_NRF52_STAGE_CEILING_LEGACY ||
|
||||
stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED;
|
||||
}
|
||||
|
||||
inline bool mota_nrf52_layout_valid(uint32_t app_base, uint32_t stage_ceiling) {
|
||||
return mota_nrf52_stage_ceiling_valid(stage_ceiling) &&
|
||||
(app_base % MOTA_NRF52_FLASH_PAGE) == 0 && app_base < stage_ceiling;
|
||||
}
|
||||
|
||||
inline bool mota_nrf52_layout_valid(uint32_t app_base) {
|
||||
return mota_nrf52_layout_valid(app_base, mota_nrf52_layout_stage_ceiling());
|
||||
}
|
||||
|
||||
inline uint32_t mota_nrf52_stage_capacity(uint32_t app_base, uint32_t app_end,
|
||||
uint32_t stage_ceiling) {
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling) || app_end < app_base ||
|
||||
app_end > stage_ceiling || app_end > UINT32_MAX - (MOTA_NRF52_FLASH_PAGE - 1u)) return 0;
|
||||
const uint32_t first_free_page =
|
||||
(app_end + MOTA_NRF52_FLASH_PAGE - 1u) & ~(MOTA_NRF52_FLASH_PAGE - 1u);
|
||||
return first_free_page <= stage_ceiling ? stage_ceiling - first_free_page : 0;
|
||||
}
|
||||
|
||||
// Plan where to stage a received `.mota` of `total_size` bytes. It is placed bottom-aligned within the
|
||||
// highest flash page below FS_START (the bootloader scans downward from there), and it must sit
|
||||
// ENTIRELY above both the running image and the in-place decoder workspace, and below the filesystem
|
||||
// region (ExtraFS/InternalFS - where user prefs live, assumed immutable). Reserving the full workspace
|
||||
// here prevents accepting a large container that the bootloader would later reject as overlapping it.
|
||||
// highest flash page below stage_ceiling (the bootloader scans downward from there), and it must sit
|
||||
// ENTIRELY above the running image and below the filesystem region (ExtraFS/InternalFS - where user prefs
|
||||
// live, assumed immutable). The apply path separately verifies the patch's own detools memory_size ends
|
||||
// at or below this planned start before it marks the package approved.
|
||||
// Returns false (and leaves out_start untouched) if it does not fit. This is the SINGLE place the FS
|
||||
// ceiling + app-collision bounds are enforced; begin()/reopen() both go through it. Pure - no flash I/O -
|
||||
// so it is unit-tested natively in test/test_ota/test_ota_flashplan.cpp.
|
||||
inline bool mota_nrf52_stage_plan(uint32_t total_size, uint32_t app_base, uint32_t app_end,
|
||||
uint32_t& out_start) {
|
||||
if (!mota_nrf52_layout_valid(app_base) || app_end < app_base ||
|
||||
app_end > app_base + MOTA_NRF52_INPLACE_MEMORY) return false;
|
||||
const uint32_t workspace_end = app_base + MOTA_NRF52_INPLACE_MEMORY;
|
||||
const uint32_t capacity = MOTA_NRF52_FS_START - workspace_end;
|
||||
if (total_size < 13 || total_size > capacity) return false; // 13 = header(8)+trailer(5); must fit below FS
|
||||
uint32_t start = (MOTA_NRF52_FS_START - total_size) & ~(MOTA_NRF52_FLASH_PAGE - 1); // bottom-align down
|
||||
if (start < app_end || start < workspace_end) return false; // would overlap app / decoder workspace
|
||||
uint32_t stage_ceiling, uint32_t& out_start) {
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling) || app_end < app_base ||
|
||||
app_end > stage_ceiling) return false;
|
||||
const uint32_t capacity = stage_ceiling - app_base;
|
||||
if (total_size < 13 || total_size > capacity) return false; // 13 = header(8)+trailer(5)
|
||||
uint32_t start = (stage_ceiling - total_size) & ~(MOTA_NRF52_FLASH_PAGE - 1); // bottom-align down
|
||||
if (start < app_end) return false; // would overlap the running app
|
||||
out_start = start;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool mota_nrf52_stage_plan(uint32_t total_size, uint32_t app_base, uint32_t app_end,
|
||||
uint32_t& out_start) {
|
||||
return mota_nrf52_stage_plan(total_size, app_base, app_end,
|
||||
mota_nrf52_layout_stage_ceiling(), out_start);
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -68,14 +68,15 @@ bool ota_self_firmware(SelfFwInfo& out) {
|
||||
// firmware's own trailer), ignoring any staged `.mota` (which carries its own embedded EndF) higher up.
|
||||
bool ota_self_firmware(SelfFwInfo& out) {
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!mota_nrf52_layout_valid(app_base)) { out = SelfFwInfo(); return false; }
|
||||
const uint8_t* region = (const uint8_t*)(uintptr_t)app_base;
|
||||
uint32_t region_len =
|
||||
const uint32_t stage_ceiling =
|
||||
#if defined(OTA_SD_STORE)
|
||||
MOTA_NRF52_APP_END - app_base;
|
||||
MOTA_NRF52_APP_END;
|
||||
#else
|
||||
MOTA_NRF52_FS_START - app_base;
|
||||
mota_nrf52_layout_stage_ceiling();
|
||||
#endif
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling)) { out = SelfFwInfo(); return false; }
|
||||
const uint8_t* region = (const uint8_t*)(uintptr_t)app_base;
|
||||
const uint32_t region_len = stage_ceiling - app_base;
|
||||
return find_self_firmware(region, region_len, out, /*verify_body=*/true);
|
||||
}
|
||||
#else
|
||||
@@ -97,7 +98,9 @@ bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) {
|
||||
#if defined(OTA_SD_STORE)
|
||||
if (app_base >= MOTA_NRF52_APP_END || (uint64_t)app_base + off + len > MOTA_NRF52_APP_END) return false;
|
||||
#else
|
||||
if (!mota_nrf52_layout_valid(app_base) || (uint64_t)app_base + off + len > MOTA_NRF52_FS_START) return false;
|
||||
const uint32_t stage_ceiling = mota_nrf52_layout_stage_ceiling();
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling) ||
|
||||
(uint64_t)app_base + off + len > stage_ceiling) return false;
|
||||
#endif
|
||||
memcpy(buf, (const uint8_t*)(uintptr_t)(app_base + off), len);
|
||||
return true;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
|
||||
#include "OtaSelf.h"
|
||||
#include "OtaBlInfo.h"
|
||||
#include "OtaDebug.h"
|
||||
#include "OtaByteIO.h" // align_down / rd_u32le (flash-page geometry + header read)
|
||||
#include <string.h>
|
||||
@@ -11,10 +12,19 @@
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// A valid EndF gives the exact live-image extent. Rescue/bring-up builds without one retain the old
|
||||
// 608 KiB protection floor so staging cannot begin in the middle of a normally sized application.
|
||||
static bool protected_app_end(uint32_t app_base, uint32_t stage_ceiling, uint32_t& app_end) {
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling)) return false;
|
||||
app_end = app_base + MOTA_NRF52_FALLBACK_INPLACE_MEMORY;
|
||||
SelfFwInfo fi;
|
||||
if (ota_self_firmware(fi) && fi.valid) app_end = app_base + fi.image_len;
|
||||
return app_end >= app_base && app_end <= stage_ceiling;
|
||||
}
|
||||
|
||||
// Write one whole 4 KB page from `buf` to flash (erase + program, ~85 ms). `buf` is PG bytes, 0xFF-padded
|
||||
// past the container, so the program is clean. The last container page ends exactly at FS_START (both
|
||||
// FS_START and _write_start are page-aligned and the container ends <= FS_START), so a full-page write
|
||||
// never reaches into ExtraFS.
|
||||
// past the container, so the program is clean. The selected ceiling and _write_start are page-aligned,
|
||||
// and the container ends below the ceiling, so a full-page write never reaches protected storage.
|
||||
static bool flash_matches(uint32_t addr, const uint8_t* expected, uint32_t n) {
|
||||
const volatile uint8_t* actual = (const volatile uint8_t*)(uintptr_t)addr;
|
||||
for (uint32_t i = 0; i < n; i++) if (actual[i] != expected[i]) return false;
|
||||
@@ -22,8 +32,8 @@ static bool flash_matches(uint32_t addr, const uint8_t* expected, uint32_t n) {
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::flush_page(uint32_t page_idx, const uint8_t* buf) {
|
||||
if (!_io_ok || _write_start > MOTA_NRF52_FS_START - PG ||
|
||||
page_idx > (MOTA_NRF52_FS_START - PG - _write_start) / PG) {
|
||||
if (!_io_ok || _write_start > _stage_ceiling - PG ||
|
||||
page_idx > (_stage_ceiling - PG - _write_start) / PG) {
|
||||
_io_ok = false;
|
||||
return false;
|
||||
}
|
||||
@@ -35,6 +45,14 @@ bool OtaStoreFlashNrf52::flush_page(uint32_t page_idx, const uint8_t* buf) {
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t OtaStoreFlashNrf52::capacity() const {
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
||||
uint32_t app_end;
|
||||
return protected_app_end(app_base, stage_ceiling, app_end)
|
||||
? mota_nrf52_stage_capacity(app_base, app_end, stage_ceiling) : 0;
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::flush_pay() {
|
||||
return _pay_idx == 0 || flush_page(_pay_idx, _pay_page); // _pay_idx 0 == no payload page open
|
||||
}
|
||||
@@ -74,28 +92,25 @@ bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
|
||||
|
||||
// never collide with the running application image (its extent comes from its EndF trailer)
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!mota_nrf52_layout_valid(app_base)) return false;
|
||||
uint32_t app_end = app_base;
|
||||
SelfFwInfo fi;
|
||||
if (ota_self_firmware(fi) && fi.valid) {
|
||||
if ((uint64_t)app_base + fi.image_len > MOTA_NRF52_FS_START) return false;
|
||||
app_end = app_base + fi.image_len;
|
||||
}
|
||||
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
||||
uint32_t app_end;
|
||||
if (!protected_app_end(app_base, stage_ceiling, app_end)) return false;
|
||||
|
||||
// Bottom-align below FS_START and reject unless it sits above the running image AND the full detools
|
||||
// workspace (the FS/prefs-safe bounds check; pure + unit-tested in the native OTA suite).
|
||||
// Bottom-align below the selected ceiling and reject unless it sits above the running image. The
|
||||
// approval path later verifies the patch's detools workspace ends at/below this exact start.
|
||||
uint32_t start;
|
||||
if (!mota_nrf52_stage_plan(total_size, app_base, app_end, start)) return false;
|
||||
if (!mota_nrf52_stage_plan(total_size, app_base, app_end, stage_ceiling, start)) return false;
|
||||
|
||||
_write_start = start;
|
||||
_stage_ceiling = stage_ceiling;
|
||||
_total = total_size;
|
||||
memset(_meta_page, 0xFF, PG); // assemble page 0 in RAM; 0xFF = erased sentinel (unfilled leaf slots)
|
||||
memset(_trailer, 0xFF, sizeof(_trailer));
|
||||
_pay_idx = 0;
|
||||
_flushed = false;
|
||||
_io_ok = true;
|
||||
OTA_DBG("OTA flash: begin total=%u start=%08x app_end=%08x\n",
|
||||
(unsigned)total_size, (unsigned)start, (unsigned)app_end);
|
||||
OTA_DBG("OTA flash: begin total=%u start=%08x app_end=%08x ceiling=%08x\n",
|
||||
(unsigned)total_size, (unsigned)start, (unsigned)app_end, (unsigned)stage_ceiling);
|
||||
return true; // no pre-erase: each page is erased by its own (single) flush
|
||||
}
|
||||
|
||||
@@ -156,33 +171,31 @@ void OtaStoreFlashNrf52::checkpoint() {
|
||||
}
|
||||
|
||||
// Re-attach to a container already staged in flash (after a reboot), without erasing. The container is
|
||||
// bottom-aligned (begin: start = (FS_START - total) & ~(PG-1)) and flash is memory-mapped, so scan page
|
||||
// starts from just below FS_START down to the app end for MOTA_MAGIC with a self-consistent total; adopt
|
||||
// bottom-aligned (begin: start = (ceiling - total) & ~(PG-1)) and flash is memory-mapped, so scan page
|
||||
// starts from just below that ceiling down to the app end for MOTA_MAGIC with a self-consistent total; adopt
|
||||
// the first match (highest address = most recent for the common single-container case). The manager then
|
||||
// parses the loaded manifest and validates geometry/root, so a stale leftover is rejected there.
|
||||
bool OtaStoreFlashNrf52::reopen() {
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!mota_nrf52_layout_valid(app_base)) return false;
|
||||
uint32_t app_end = app_base;
|
||||
SelfFwInfo fi;
|
||||
if (ota_self_firmware(fi) && fi.valid) {
|
||||
if ((uint64_t)app_base + fi.image_len > MOTA_NRF52_FS_START) return false;
|
||||
app_end = app_base + fi.image_len;
|
||||
}
|
||||
for (uint32_t start = align_down(MOTA_NRF52_FS_START - PG, PG); start >= app_end; start -= PG) {
|
||||
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
||||
uint32_t app_end;
|
||||
if (!protected_app_end(app_base, stage_ceiling, app_end)) return false;
|
||||
for (uint32_t start = align_down(stage_ceiling - PG, PG); start >= app_end; start -= PG) {
|
||||
const uint8_t* p = (const uint8_t*)(uintptr_t)start;
|
||||
if (memcmp(p, MOTA_MAGIC, 4) != 0) continue;
|
||||
uint32_t total = rd_u32le(p + 4);
|
||||
uint32_t want; // must be valid + placed exactly where begin() would have staged it (same bounds fn)
|
||||
if (!mota_nrf52_stage_plan(total, app_base, app_end, want) || want != start) continue;
|
||||
if (!mota_nrf52_stage_plan(total, app_base, app_end, stage_ceiling, want) || want != start) continue;
|
||||
_write_start = start;
|
||||
_stage_ceiling = stage_ceiling;
|
||||
_total = total;
|
||||
memcpy(_meta_page, p, PG); // load page 0 (header+manifest+leaves) into RAM to continue
|
||||
memcpy(_trailer, p + (total - 5), 5); // recover the trailer tail (flushed at last finalize, if any)
|
||||
_pay_idx = 0;
|
||||
_flushed = false;
|
||||
_io_ok = true;
|
||||
OTA_DBG("OTA flash: reopen total=%u start=%08x\n", (unsigned)total, (unsigned)start);
|
||||
OTA_DBG("OTA flash: reopen total=%u start=%08x ceiling=%08x\n",
|
||||
(unsigned)total, (unsigned)start, (unsigned)stage_ceiling);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
#include "OtaStore.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
|
||||
// Persistent flash-backed OtaStore for nRF52840. Stages the received `.mota` in the free flash
|
||||
// below the primary LittleFS (FS_START), bottom-aligned so its trailer ends at FS_START and the
|
||||
// bootloader can scan for it. Survives reboot - the whole point - so the bootloader can apply the
|
||||
// staged delta on the next boot.
|
||||
// Persistent flash-backed OtaStore for nRF52840. Stages the received `.mota` below the selected
|
||||
// filesystem-safe ceiling, bottom-aligned so the bootloader can find it. Internal ExtraFS roles stop
|
||||
// at 0xD4000; QSPI/no-ExtraFS roles can use the otherwise-free range through 0xED000. Survives reboot -
|
||||
// the whole point - so the bootloader can apply the staged delta on the next boot.
|
||||
//
|
||||
// RAM is bounded to O(one flash page), NEVER O(mota): a 100 KB+ delta must not live in RAM.
|
||||
// - On nRF52 the flash *erase* unit is one 4 KB page and the only SoftDevice-safe writer
|
||||
@@ -36,6 +36,7 @@ class OtaStoreFlashNrf52 : public OtaStore {
|
||||
static const uint32_t PG = MOTA_NRF52_FLASH_PAGE; // 4096
|
||||
|
||||
uint32_t _write_start = 0; // flash address of container offset 0 (page-aligned)
|
||||
uint32_t _stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
uint32_t _total = 0; // staged container size (0 = none)
|
||||
bool _flushed = false; // finalize() committed everything to flash
|
||||
bool _io_ok = true; // cleared on a failed/out-of-bounds flash write or readback mismatch
|
||||
@@ -58,9 +59,12 @@ public:
|
||||
bool begin(uint32_t total_size) override;
|
||||
bool write(uint32_t offset, const uint8_t* data, uint32_t len) override;
|
||||
bool read(uint32_t offset, uint8_t* buf, uint32_t len) const override;
|
||||
uint32_t capacity() const override { return mota_nrf52_stage_capacity(mota_nrf52_app_base()); }
|
||||
uint32_t capacity() const override;
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override { _total = 0; _pay_idx = 0; _flushed = false; _io_ok = true; }
|
||||
void clear() override {
|
||||
_stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
_total = 0; _pay_idx = 0; _flushed = false; _io_ok = true;
|
||||
}
|
||||
bool set_meta_size(uint32_t meta_bytes) override { return meta_bytes <= PG; } // leaves must fit page 0
|
||||
bool finalize() override;
|
||||
void checkpoint() override; // persist page 0 (leaves) + the open payload page so a reboot can resume
|
||||
|
||||
@@ -780,6 +780,20 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val
|
||||
#if ENV_INCLUDE_GPS
|
||||
if (gps_detected && strcmp(name, "gps") == 0) {
|
||||
bool enabled = strcmp(value, "0") != 0;
|
||||
#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)
|
||||
// RAK3401 must keep 3V3_S on for its radio FEM. RAK12501/L76K has no
|
||||
// separately wired standby control, so claiming it is off would only stop
|
||||
// parsing while the receiver continued drawing full power.
|
||||
if (!enabled && serialGPSFlag) {
|
||||
setGpsTelemetryUserEnabled(true);
|
||||
return false;
|
||||
}
|
||||
if (enabled && serialGPSFlag) {
|
||||
_location->setGPSPowerSaving(false);
|
||||
setGpsTelemetryUserEnabled(true);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
bool was_active = gps_active;
|
||||
_location->setGPSPowerSaving(enabled && powersaving_enabled);
|
||||
setGpsTelemetryUserEnabled(enabled);
|
||||
@@ -788,13 +802,8 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (strcmp(name, "gps_interval") == 0) {
|
||||
uint32_t interval_seconds = atoi(value);
|
||||
gps_update_interval_sec = interval_seconds > 0 ? interval_seconds : 1;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return false; // not supported
|
||||
return SensorManager::setSettingValue(name, value);
|
||||
}
|
||||
|
||||
void EnvironmentSensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
@@ -804,6 +813,13 @@ void EnvironmentSensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
#if ENV_INCLUDE_GPS
|
||||
if (!gps_detected) return;
|
||||
bool gps_user_enabled = isGpsTelemetryUserEnabled();
|
||||
#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)
|
||||
if (serialGPSFlag) {
|
||||
_location->setGPSPowerSaving(false);
|
||||
if (!gps_active) start_gps();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
_location->setGPSPowerSaving(enabled && gps_user_enabled);
|
||||
if (!gps_user_enabled) return;
|
||||
|
||||
@@ -886,8 +902,16 @@ void EnvironmentSensorManager::rakGPSInit() {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef FORCE_GPS_ALIVE // for use with repeaters, until GPS toggle is implimented
|
||||
// Now that GPS is found and set up, set to sleep for initial state
|
||||
#ifdef FORCE_GPS_ALIVE
|
||||
if (i2cGPSFlag) {
|
||||
// The u-blox receiver can sleep while the shared rail remains available.
|
||||
stop_gps();
|
||||
} else if (serialGPSFlag) {
|
||||
// The UART L76K cannot be isolated from the RAK3401 radio power rail.
|
||||
setGpsTelemetryUserEnabled(true);
|
||||
}
|
||||
#else
|
||||
// Now that GPS is found and set up, set it to sleep for the initial state.
|
||||
stop_gps();
|
||||
#endif
|
||||
}
|
||||
@@ -986,8 +1010,10 @@ void EnvironmentSensorManager::stop_gps() {
|
||||
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
#ifdef FORCE_GPS_ALIVE
|
||||
// Keep the shared rail alive. The UART L76K cannot be put to sleep here.
|
||||
if (i2cGPSFlag) ublox_GNSS.powerSaveMode(false);
|
||||
// Keep the shared rail alive for the RAK3401 radio FEM. The I2C u-blox can
|
||||
// still enter its internal power-save mode. UART L76K builds reject GPS off
|
||||
// above because that module has no independent standby connection.
|
||||
if (i2cGPSFlag) ublox_GNSS.powerSaveMode(true);
|
||||
#else
|
||||
pinMode(gpsResetPin, OUTPUT);
|
||||
digitalWrite(gpsResetPin, LOW); // WB_IO2
|
||||
@@ -1013,8 +1039,9 @@ void EnvironmentSensorManager::loop() {
|
||||
if (powersaving_enabled && gps_detected && _location->getGPSPowerSaving()) {
|
||||
unsigned long next_off = _location->getNextGPSOff();
|
||||
unsigned long next_on = _location->getNextGPSOn();
|
||||
if (gps_active && ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
||||
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
POWERSAVING_DEBUG_PRINTLN("GPS entering sleep");
|
||||
stop_gps();
|
||||
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
||||
@@ -1055,7 +1082,7 @@ void EnvironmentSensorManager::loop() {
|
||||
#endif
|
||||
|
||||
}
|
||||
next_gps_update = now + (gps_update_interval_sec * 1000);
|
||||
next_gps_update = now + getGpsUpdateIntervalMillis();
|
||||
}
|
||||
#endif
|
||||
#if ENV_INCLUDE_BME680_BSEC
|
||||
|
||||
@@ -21,7 +21,6 @@ protected:
|
||||
|
||||
bool gps_detected = false;
|
||||
bool gps_active = false;
|
||||
uint32_t gps_update_interval_sec = 1;
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
LocationProvider* _location;
|
||||
|
||||
@@ -147,7 +147,7 @@ public :
|
||||
while (_gps_serial->available()) {
|
||||
char c = _gps_serial->read();
|
||||
#ifdef GPS_NMEA_DEBUG
|
||||
Serial.print(c);
|
||||
if (mesh::isUsbLoggingEnabled()) Serial.print(c);
|
||||
#endif
|
||||
nmea.process(c);
|
||||
}
|
||||
|
||||
@@ -15,18 +15,34 @@ using namespace mesh::ota;
|
||||
|
||||
static constexpr uint32_t APP_V6 = MOTA_NRF52_APP_BASE_S140_V6;
|
||||
static constexpr uint32_t APP_V7 = MOTA_NRF52_APP_BASE_S140_V7;
|
||||
static constexpr uint32_t CAP_V6 = MOTA_NRF52_FS_START - (APP_V6 + MOTA_NRF52_INPLACE_MEMORY); // 88 KB
|
||||
static constexpr uint32_t CAP_V7 = MOTA_NRF52_FS_START - (APP_V7 + MOTA_NRF52_INPLACE_MEMORY); // 84 KB
|
||||
static constexpr uint32_t LEGACY = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
static constexpr uint32_t EXPANDED = MOTA_NRF52_STAGE_CEILING_EXPANDED;
|
||||
static constexpr uint32_t APP_IMAGE_SIZE = 520u * 1024u;
|
||||
static constexpr uint32_t APP_END_V6 = APP_V6 + APP_IMAGE_SIZE;
|
||||
static constexpr uint32_t APP_END_V7 = APP_V7 + APP_IMAGE_SIZE;
|
||||
static constexpr uint32_t CAP_V6 = LEGACY - APP_END_V6;
|
||||
static constexpr uint32_t CAP_V7 = LEGACY - APP_END_V7;
|
||||
static constexpr uint32_t CAP_V6_EXPANDED = EXPANDED - APP_END_V6;
|
||||
static constexpr uint32_t CAP_V7_EXPANDED = EXPANDED - APP_END_V7;
|
||||
|
||||
// A typical running image (~520 KB) leaves room; the container lands strictly within (app_end, FS_START].
|
||||
TEST(OtaFlashPlan, SelectsCeilingFromLinkedLayoutAndStorage) {
|
||||
// Actual internal secondary storage is authoritative regardless of linker selection.
|
||||
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(EXPANDED, true), LEGACY);
|
||||
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(LEGACY, true), LEGACY);
|
||||
// Standard or ExtraFS linker without an internal secondary filesystem reclaims the unused 100 KiB.
|
||||
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(EXPANDED, false), EXPANDED);
|
||||
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(LEGACY, false), EXPANDED);
|
||||
// An unrecognized linker region is never permission to erase a larger window.
|
||||
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(0xE0000u, false), LEGACY);
|
||||
}
|
||||
|
||||
// A typical running image (~520 KB) leaves room; the container lands strictly above it and below ExtraFS.
|
||||
TEST(OtaFlashPlan, StagesBelowFilesystemAndAboveApp) {
|
||||
uint32_t app_end = APP_V6 + 520u * 1024u;
|
||||
uint32_t start = 0xDEADBEEF;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(64u * 1024u, APP_V6, app_end, start));
|
||||
EXPECT_GE(start, app_end); // never overlaps the running image
|
||||
EXPECT_GE(start, APP_V6 + MOTA_NRF52_INPLACE_MEMORY); // never overlaps detools workspace
|
||||
EXPECT_LE(start + 64u * 1024u, MOTA_NRF52_FS_START); // never reaches into ExtraFS/InternalFS/prefs
|
||||
EXPECT_EQ(start % MOTA_NRF52_FLASH_PAGE, 0u); // page-aligned (the flash erase unit)
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(64u * 1024u, APP_V6, APP_END_V6, LEGACY, start));
|
||||
EXPECT_GE(start, APP_END_V6); // never overlaps the running image
|
||||
EXPECT_LE(start + 64u * 1024u, LEGACY); // never reaches into ExtraFS/prefs
|
||||
EXPECT_EQ(start % MOTA_NRF52_FLASH_PAGE, 0u); // page-aligned (the flash erase unit)
|
||||
}
|
||||
|
||||
// Bottom-aligned: start is the page-aligned FS_START - total_size, so the trailer sits within the
|
||||
@@ -34,67 +50,91 @@ TEST(OtaFlashPlan, StagesBelowFilesystemAndAboveApp) {
|
||||
TEST(OtaFlashPlan, BottomAlignedBelowCeiling) {
|
||||
uint32_t start = 0;
|
||||
uint32_t total = 60000;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(total, APP_V6, APP_V6, start));
|
||||
EXPECT_EQ(start, (MOTA_NRF52_FS_START - total) & ~(MOTA_NRF52_FLASH_PAGE - 1));
|
||||
EXPECT_LE(start + total, MOTA_NRF52_FS_START);
|
||||
EXPECT_GT(start + total, MOTA_NRF52_FS_START - MOTA_NRF52_FLASH_PAGE); // within one page of the ceiling
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(total, APP_V6, APP_V6, LEGACY, start));
|
||||
EXPECT_EQ(start, (LEGACY - total) & ~(MOTA_NRF52_FLASH_PAGE - 1));
|
||||
EXPECT_LE(start + total, LEGACY);
|
||||
EXPECT_GT(start + total, LEGACY - MOTA_NRF52_FLASH_PAGE); // within one page of the ceiling
|
||||
}
|
||||
|
||||
// An exactly-capacity container fills the region above the decoder workspace; one byte more never fits.
|
||||
// An exactly-capacity container fills the page-aligned space above the app; one byte more never fits.
|
||||
TEST(OtaFlashPlan, RejectsOversizedContainer) {
|
||||
uint32_t start = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V6, APP_V6, APP_V6, start));
|
||||
EXPECT_EQ(start, APP_V6 + MOTA_NRF52_INPLACE_MEMORY);
|
||||
EXPECT_EQ(start + CAP_V6, MOTA_NRF52_FS_START);
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_V6, start));
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V6, APP_V6, APP_END_V6, LEGACY, start));
|
||||
EXPECT_EQ(start, APP_END_V6);
|
||||
EXPECT_EQ(start + CAP_V6, LEGACY);
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_END_V6, LEGACY, start));
|
||||
}
|
||||
|
||||
// A running image that exceeds detools' fixed in-place memory can never be a valid delta base.
|
||||
TEST(OtaFlashPlan, RejectsAppLargerThanWorkspace) {
|
||||
uint32_t app_end = APP_V6 + MOTA_NRF52_INPLACE_MEMORY + 1;
|
||||
// The package carries its own checked memory_size, so a valid app may exceed the old 608 KiB fallback.
|
||||
TEST(OtaFlashPlan, AcceptsAppLargerThanFallbackWhenPackageFits) {
|
||||
const uint32_t app_end = APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY + 32u * 1024u;
|
||||
uint32_t start = 0;
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(4u * 1024u, APP_V6, app_end, start));
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(16u * 1024u, APP_V6, app_end, LEGACY, start));
|
||||
EXPECT_GE(start, app_end);
|
||||
}
|
||||
|
||||
// Minimum container is header(8)+trailer(5)=13 bytes; anything smaller is not a container.
|
||||
TEST(OtaFlashPlan, RejectsUndersizedContainer) {
|
||||
uint32_t start = 0;
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(12, APP_V6, APP_V6, start));
|
||||
EXPECT_TRUE(mota_nrf52_stage_plan(13, APP_V6, APP_V6, start));
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(12, APP_V6, APP_V6, LEGACY, start));
|
||||
EXPECT_TRUE(mota_nrf52_stage_plan(13, APP_V6, APP_V6, LEGACY, start));
|
||||
}
|
||||
|
||||
// The user-preferences filesystems (ExtraFS @ 0xD4000, InternalFS @ 0xED000) are entirely ABOVE any
|
||||
// staged container AND above the in-place apply workspace - the immutability the shared-firmware hashes
|
||||
// depend on. Hard-code the FS addresses here (independent of the layout header) so a future edit to
|
||||
// FS_START that drifts into a filesystem is caught.
|
||||
// staged container. Hard-code the FS addresses here (independent of the layout header) so a future edit
|
||||
// that drifts into a filesystem is caught.
|
||||
TEST(OtaFlashPlan, PrefsRegionNeverStaged) {
|
||||
const uint32_t EXTRAFS_START = 0xD4000u; // companion ExtraFS (CustomLFS(0xD4000, 0x19000))
|
||||
const uint32_t INTERNALFS_START = 0xED000u; // primary LittleFS (holds /com_prefs)
|
||||
EXPECT_LE(MOTA_NRF52_FS_START, EXTRAFS_START); // staging ceiling at/below the first filesystem
|
||||
EXPECT_EQ(LEGACY, EXTRAFS_START);
|
||||
EXPECT_EQ(EXPANDED, INTERNALFS_START);
|
||||
EXPECT_LT(EXTRAFS_START, INTERNALFS_START);
|
||||
// the largest possible staged container still ends at the ceiling, never into a filesystem
|
||||
uint32_t start = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V6, APP_V6, APP_V6, start));
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V6, APP_V6, APP_END_V6, LEGACY, start));
|
||||
EXPECT_LE(start + CAP_V6, EXTRAFS_START);
|
||||
// and the in-place apply workspace ends below the filesystem too
|
||||
EXPECT_LE(APP_V6 + MOTA_NRF52_INPLACE_MEMORY, EXTRAFS_START);
|
||||
}
|
||||
|
||||
// S140 v7 moves the app start by one page. Runtime linker-base discovery must leave a correspondingly
|
||||
// smaller but still safe staging region rather than scanning the v6 address and missing EndF.
|
||||
TEST(OtaFlashPlan, SupportsS140V7RuntimeBase) {
|
||||
EXPECT_TRUE(mota_nrf52_layout_valid(APP_V7));
|
||||
EXPECT_EQ(mota_nrf52_stage_capacity(APP_V7), CAP_V7);
|
||||
EXPECT_TRUE(mota_nrf52_layout_valid(APP_V7, LEGACY));
|
||||
EXPECT_EQ(mota_nrf52_stage_capacity(APP_V7, APP_END_V7, LEGACY), CAP_V7);
|
||||
uint32_t start = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V7, APP_V7, APP_V7 + 520u * 1024u, start));
|
||||
EXPECT_EQ(start, APP_V7 + MOTA_NRF52_INPLACE_MEMORY);
|
||||
EXPECT_EQ(start + CAP_V7, MOTA_NRF52_FS_START);
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V7, APP_V7, APP_END_V7, LEGACY, start));
|
||||
EXPECT_EQ(start, APP_END_V7);
|
||||
EXPECT_EQ(start + CAP_V7, LEGACY);
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, ExpandedCeilingAddsExactly100KiBForV6AndV7) {
|
||||
EXPECT_EQ(EXPANDED - LEGACY, 100u * 1024u);
|
||||
EXPECT_EQ(CAP_V6_EXPANDED - CAP_V6, 100u * 1024u);
|
||||
EXPECT_EQ(CAP_V7_EXPANDED - CAP_V7, 100u * 1024u);
|
||||
EXPECT_EQ(mota_nrf52_stage_capacity(APP_V6, APP_END_V6, EXPANDED), CAP_V6_EXPANDED);
|
||||
EXPECT_EQ(mota_nrf52_stage_capacity(APP_V7, APP_END_V7, EXPANDED), CAP_V7_EXPANDED);
|
||||
|
||||
uint32_t start = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V7_EXPANDED, APP_V7, APP_END_V7, EXPANDED, start));
|
||||
EXPECT_EQ(start, APP_END_V7);
|
||||
EXPECT_EQ(start + CAP_V7_EXPANDED, EXPANDED);
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, RejectsAppOutsideSelectedRegion) {
|
||||
uint32_t start = 0x1234ABCD;
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, LEGACY + 1, LEGACY, start));
|
||||
EXPECT_EQ(start, 0x1234ABCDu);
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, RejectsUnknownCeiling) {
|
||||
uint32_t start = 0x1234ABCD;
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, APP_V6, 0xE0000u, start));
|
||||
EXPECT_EQ(start, 0x1234ABCDu);
|
||||
}
|
||||
|
||||
// out_start is only written on success - a rejected plan must not clobber the caller's variable.
|
||||
TEST(OtaFlashPlan, LeavesOutputUntouchedOnReject) {
|
||||
uint32_t start = 0x1234ABCD;
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_V6, start));
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_END_V6, LEGACY, start));
|
||||
EXPECT_EQ(start, 0x1234ABCDu);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
#include <helpers/GpsPowerPolicy.h>
|
||||
|
||||
TEST(PowerManagement, MedianRejectsOneBrownoutSample) {
|
||||
EXPECT_EQ(mesh::power::medianVoltage(3290, 3700, 3710), 3700);
|
||||
@@ -16,6 +17,29 @@ TEST(PowerManagement, BootLockRequiresValidLowBatteryReading) {
|
||||
EXPECT_FALSE(mesh::power::shouldBootLock(3000, 3300, true));
|
||||
}
|
||||
|
||||
TEST(GpsPowerPolicy, ZeroUsesOneSecondDefault) {
|
||||
uint32_t configured = 99;
|
||||
ASSERT_TRUE(mesh::gps::parseUpdateInterval("0", configured));
|
||||
EXPECT_EQ(0U, configured);
|
||||
EXPECT_EQ(1U, mesh::gps::effectiveUpdateIntervalSec(configured));
|
||||
EXPECT_EQ(1000U, mesh::gps::updateIntervalMillis(configured));
|
||||
}
|
||||
|
||||
TEST(GpsPowerPolicy, AcceptsPersistedIntervalThroughOneDay) {
|
||||
uint32_t configured = 0;
|
||||
ASSERT_TRUE(mesh::gps::parseUpdateInterval("86400", configured));
|
||||
EXPECT_EQ(86400U, configured);
|
||||
EXPECT_EQ(86400000U, mesh::gps::updateIntervalMillis(configured));
|
||||
}
|
||||
|
||||
TEST(GpsPowerPolicy, RejectsMalformedAndOutOfRangeIntervals) {
|
||||
uint32_t configured = 17;
|
||||
EXPECT_FALSE(mesh::gps::parseUpdateInterval("", configured));
|
||||
EXPECT_FALSE(mesh::gps::parseUpdateInterval("5s", configured));
|
||||
EXPECT_FALSE(mesh::gps::parseUpdateInterval("86401", configured));
|
||||
EXPECT_EQ(17U, configured);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
|
||||
+87
-2
@@ -54,10 +54,25 @@ APPROVAL_YES = b"APRV" # 41 50 52 56 = approved
|
||||
|
||||
DEFAULT_BLOCK_SIZE = 1024
|
||||
|
||||
# nRF52840 OTAFIX's detools in-place workspace. A firmware image (including EndF) must fit here, and
|
||||
# the staged .mota must begin above it. Keep in sync with OtaFlashLayout_nrf52.h / the OTAFIX bootloader.
|
||||
# Conservative fallback for firmware built before the layout record below existed. Keep in sync with
|
||||
# OtaFlashLayout_nrf52.h and motatool's format.rs.
|
||||
NRF52_INPLACE_MEMORY = 0x00098000
|
||||
|
||||
NRF52_APP_BASE_S140_V6 = 0x00026000
|
||||
NRF52_APP_BASE_S140_V7 = 0x00027000
|
||||
NRF52_EXTRAFS_START = 0x000D4000
|
||||
NRF52_APP_END = 0x000ED000
|
||||
NRF52_FLASH_PAGE = 4096
|
||||
|
||||
# A validated nRF52 firmware carries this record immediately before EndF. It lets an offline packager
|
||||
# derive the actual app base and staging ceiling from the built artifact, without a board-name allowlist.
|
||||
# magic(8) | version(1) | flags(1) | record_len(2) | app_base(4) | linked_app_end(4) | stage_ceiling(4)
|
||||
NRF52_LAYOUT_MAGIC = b"mOTALay1"
|
||||
NRF52_LAYOUT_VERSION = 1
|
||||
NRF52_LAYOUT_LEN = 24
|
||||
NRF52_LAYOUT_FLAG_SD = 0x01
|
||||
NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS = 0x02
|
||||
|
||||
# MeshTower V2's SD-backed OTA target keeps the staged .mota off-chip, so the application may use the
|
||||
# complete S140 v6 application region up to InternalFS instead of leaving room for internal staging.
|
||||
# This is deliberately target-specific: other nRF52 OTA builds still need NRF52_INPLACE_MEMORY above.
|
||||
@@ -154,6 +169,76 @@ class FwIdent:
|
||||
hw_id: str = "" # readable hardware tag (brick-safety), e.g. "RAK4631"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Nrf52Layout:
|
||||
"""Resolved flash geometry embedded immediately before an nRF52 firmware's EndF trailer."""
|
||||
app_base: int
|
||||
linked_app_end: int
|
||||
stage_ceiling: int
|
||||
flags: int = 0
|
||||
|
||||
@property
|
||||
def sd_backed(self) -> bool:
|
||||
return bool(self.flags & NRF52_LAYOUT_FLAG_SD)
|
||||
|
||||
|
||||
def nrf52_stage_ceiling_for_layout(linked_app_end: int, uses_internal_extrafs: bool) -> int:
|
||||
"""Select a safe staging ceiling from linker geometry and actual secondary-storage type."""
|
||||
if uses_internal_extrafs:
|
||||
return NRF52_EXTRAFS_START
|
||||
if linked_app_end in (NRF52_EXTRAFS_START, NRF52_APP_END):
|
||||
return NRF52_APP_END
|
||||
return NRF52_EXTRAFS_START
|
||||
|
||||
|
||||
def build_nrf52_layout(layout: Nrf52Layout) -> bytes:
|
||||
if layout.app_base not in (NRF52_APP_BASE_S140_V6, NRF52_APP_BASE_S140_V7):
|
||||
raise ValueError(f"unsupported nRF52 app base 0x{layout.app_base:X}")
|
||||
if layout.linked_app_end not in (NRF52_EXTRAFS_START, NRF52_APP_END):
|
||||
raise ValueError(f"unsupported nRF52 linked app end 0x{layout.linked_app_end:X}")
|
||||
if layout.stage_ceiling not in (NRF52_EXTRAFS_START, NRF52_APP_END):
|
||||
raise ValueError(f"unsupported nRF52 staging ceiling 0x{layout.stage_ceiling:X}")
|
||||
if not (layout.app_base < layout.linked_app_end <= NRF52_APP_END):
|
||||
raise ValueError("invalid nRF52 app region")
|
||||
if layout.flags & ~(NRF52_LAYOUT_FLAG_SD | NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS):
|
||||
raise ValueError(f"unsupported nRF52 layout flags 0x{layout.flags:X}")
|
||||
expected_ceiling = (NRF52_APP_END if layout.sd_backed else
|
||||
nrf52_stage_ceiling_for_layout(
|
||||
layout.linked_app_end,
|
||||
bool(layout.flags & NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS)))
|
||||
if layout.stage_ceiling != expected_ceiling:
|
||||
raise ValueError(f"nRF52 staging ceiling 0x{layout.stage_ceiling:X} is inconsistent with layout flags")
|
||||
return struct.pack("<8sBBHIII", NRF52_LAYOUT_MAGIC, NRF52_LAYOUT_VERSION, layout.flags,
|
||||
NRF52_LAYOUT_LEN, layout.app_base, layout.linked_app_end,
|
||||
layout.stage_ceiling)
|
||||
|
||||
|
||||
def ensure_nrf52_layout(body: bytes, layout: Nrf52Layout) -> bytes:
|
||||
"""Append or replace the fixed layout record on an EndF-free firmware body."""
|
||||
record = build_nrf52_layout(layout)
|
||||
if len(body) >= NRF52_LAYOUT_LEN and body[-NRF52_LAYOUT_LEN:-NRF52_LAYOUT_LEN + 8] == NRF52_LAYOUT_MAGIC:
|
||||
body = body[:-NRF52_LAYOUT_LEN]
|
||||
return body + record
|
||||
|
||||
|
||||
def parse_nrf52_layout(image: bytes) -> Optional[Nrf52Layout]:
|
||||
"""Read a validated layout record from an EndF-trailed firmware; old firmware returns None."""
|
||||
if not has_endf(image) or len(image) < ENDF_LEN + NRF52_LAYOUT_LEN:
|
||||
return None
|
||||
record = image[-ENDF_LEN - NRF52_LAYOUT_LEN:-ENDF_LEN]
|
||||
try:
|
||||
magic, version, flags, record_len, app_base, linked_end, ceiling = struct.unpack(
|
||||
"<8sBBHIII", record)
|
||||
if magic != NRF52_LAYOUT_MAGIC or version != NRF52_LAYOUT_VERSION or record_len != NRF52_LAYOUT_LEN:
|
||||
return None
|
||||
layout = Nrf52Layout(app_base, linked_end, ceiling, flags)
|
||||
if build_nrf52_layout(layout) != record:
|
||||
return None
|
||||
return layout
|
||||
except (ValueError, struct.error):
|
||||
return None
|
||||
|
||||
|
||||
def build_endf(body: bytes, ident: Optional["FwIdent"] = None) -> bytes:
|
||||
"""The fixed 56-byte EndF trailer for a firmware BODY (identity zero-filled if not given)."""
|
||||
ident = ident or FwIdent()
|
||||
|
||||
+45
-20
@@ -66,6 +66,24 @@ def _board_maximum_size(build_env):
|
||||
return None
|
||||
|
||||
|
||||
def _source_filter_text():
|
||||
"""Resolved source-filter text for detecting code that is actually part of this environment."""
|
||||
srcf = ""
|
||||
for getter in (lambda: env.GetProjectOption("build_src_filter", ""), # noqa: F821
|
||||
lambda: env.GetProjectOption("src_filter", ""), # noqa: F821
|
||||
lambda: env.get("SRC_FILTER", "")): # noqa: F821
|
||||
try:
|
||||
value = getter()
|
||||
srcf += " " + (" ".join(map(str, value)) if isinstance(value, (list, tuple)) else str(value))
|
||||
except Exception:
|
||||
pass
|
||||
return srcf
|
||||
|
||||
|
||||
def _builds_companion_radio() -> bool:
|
||||
return "examples/companion_radio" in _source_filter_text().replace("\\", "/")
|
||||
|
||||
|
||||
def _version_from_headers():
|
||||
"""FIRMWARE_VERSION is a header ``#define`` in the example (upstream MeshCore convention), not a -D, so
|
||||
_cppdef() can't see it and the EndF version would otherwise default to 0. Read it from the source WITHOUT
|
||||
@@ -90,15 +108,7 @@ def _version_from_headers():
|
||||
return vals
|
||||
|
||||
# 1) restrict to the example(s) this env compiles (build_src_filter -> examples/<name>)
|
||||
srcf = ""
|
||||
for getter in (lambda: env.GetProjectOption("build_src_filter", ""), # noqa: F821
|
||||
lambda: env.GetProjectOption("src_filter", ""), # noqa: F821
|
||||
lambda: env.get("SRC_FILTER", "")): # noqa: F821
|
||||
try:
|
||||
v = getter()
|
||||
srcf += " " + (" ".join(map(str, v)) if isinstance(v, (list, tuple)) else str(v))
|
||||
except Exception:
|
||||
pass
|
||||
srcf = _source_filter_text()
|
||||
ex_dirs = set(re.findall(r"examples[/\\]([A-Za-z0-9_]+)", srcf))
|
||||
hdrs = []
|
||||
for d in ex_dirs:
|
||||
@@ -155,33 +165,48 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f
|
||||
if not segs:
|
||||
print("EndF: empty .hex, skipping"); return
|
||||
app_start, app_end = segs[0] # first (lowest) segment = the application image
|
||||
body = bytes(ih.tobinarray(start=app_start, size=app_end - app_start))
|
||||
raw_body = bytes(ih.tobinarray(start=app_start, size=app_end - app_start))
|
||||
if ml.has_endf(raw_body):
|
||||
print(f"EndF: already present in {os.path.basename(path)} (no change)"); return
|
||||
|
||||
app_region_size = _board_maximum_size(env)
|
||||
if app_region_size is None:
|
||||
raise RuntimeError("nRF52 linked application region is unavailable")
|
||||
linked_app_end = app_start + app_region_size
|
||||
internal_extrafs = (_cppdef("EXTRAFS") is not None and _cppdef("QSPIFLASH") is None
|
||||
and _builds_companion_radio())
|
||||
sd_backed = _cppdef("OTA_SD_STORE") is not None
|
||||
stage_ceiling = (ml.NRF52_APP_END if sd_backed else
|
||||
ml.nrf52_stage_ceiling_for_layout(linked_app_end, internal_extrafs))
|
||||
layout_flags = ((ml.NRF52_LAYOUT_FLAG_SD if sd_backed else 0) |
|
||||
(ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS if internal_extrafs else 0))
|
||||
layout = ml.Nrf52Layout(app_start, linked_app_end, stage_ceiling, layout_flags)
|
||||
body = ml.ensure_nrf52_layout(raw_body, layout)
|
||||
ident = _firmware_ident()
|
||||
out, h8 = ml.ensure_endf(body, ident)
|
||||
sd_backed = _cppdef("OTA_SD_STORE") is not None
|
||||
seeder_only = _cppdef("OTA_SEEDER_ONLY") is not None
|
||||
if seeder_only:
|
||||
# A source-only full Companion never stages or applies an update to
|
||||
# itself, so it does not need to fit in OTAFIX's in-place workspace.
|
||||
# It must still fit the board/env's resolved flash application region.
|
||||
image_limit = _board_maximum_size(env)
|
||||
if image_limit is None:
|
||||
raise RuntimeError("nRF52 seeder-only application limit is unavailable")
|
||||
image_limit = app_region_size
|
||||
limit_name = "application"
|
||||
else:
|
||||
image_limit = ml.NRF52_SD_APP_MEMORY if sd_backed else ml.NRF52_INPLACE_MEMORY
|
||||
limit_name = "SD application" if sd_backed else "in-place"
|
||||
# Dynamic in-place patches carry their own memory_size and are checked against the staged
|
||||
# container before approval. The firmware itself only needs to remain inside both its linked
|
||||
# application region and the selected filesystem-safe ceiling.
|
||||
image_limit = min(linked_app_end, stage_ceiling) - app_start
|
||||
limit_name = "nRF52 OTA application"
|
||||
if image_limit is not None and len(out) > image_limit:
|
||||
raise RuntimeError(f"nRF52 OTA image is {len(out)} bytes; {limit_name} limit is "
|
||||
f"{image_limit} bytes")
|
||||
if len(out) == len(body):
|
||||
print(f"EndF: already present in {os.path.basename(path)} (no change)"); return
|
||||
trailer = out[len(body):] # the EndF trailer (56 bytes with identity)
|
||||
for i, b in enumerate(trailer):
|
||||
suffix = out[len(raw_body):] # layout record + EndF identity trailer
|
||||
for i, b in enumerate(suffix):
|
||||
ih[app_end + i] = b # write it right after the app's last byte
|
||||
ih.write_hex_file(path)
|
||||
print(f"EndF: appended to {os.path.basename(path)} at 0x{app_end:X} "
|
||||
f"(app=0x{app_start:X}.. body_len={len(body)} body_hash={h8.hex()} "
|
||||
f"stage=0x{stage_ceiling:X} "
|
||||
f"target={ident.target_id:#010x} hw='{ident.hw_id}' fw={ident.fw_version:#010x})")
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +105,38 @@ def test_endf_identity():
|
||||
assert len(z) == len(body) + ml.ENDF_LEN and ml.parse_endf_ident(z) == ml.FwIdent(0, 0, "")
|
||||
|
||||
|
||||
def test_nrf52_layout_record_roundtrip_and_policy():
|
||||
assert (ml.nrf52_stage_ceiling_for_layout(ml.NRF52_APP_END, True)
|
||||
== ml.NRF52_EXTRAFS_START)
|
||||
assert ml.nrf52_stage_ceiling_for_layout(ml.NRF52_APP_END, False) == ml.NRF52_APP_END
|
||||
assert (ml.nrf52_stage_ceiling_for_layout(ml.NRF52_EXTRAFS_START, True)
|
||||
== ml.NRF52_EXTRAFS_START)
|
||||
assert (ml.nrf52_stage_ceiling_for_layout(ml.NRF52_EXTRAFS_START, False)
|
||||
== ml.NRF52_APP_END)
|
||||
assert ml.nrf52_stage_ceiling_for_layout(0xE0000, False) == ml.NRF52_EXTRAFS_START
|
||||
|
||||
layout = ml.Nrf52Layout(ml.NRF52_APP_BASE_S140_V7, ml.NRF52_EXTRAFS_START,
|
||||
ml.NRF52_APP_END, 0)
|
||||
body = ml.ensure_nrf52_layout(_fw(4, 2048), layout)
|
||||
image, _ = ml.ensure_endf(body, ml.FwIdent(hw_id="Xiao_nrf52"))
|
||||
assert ml.parse_nrf52_layout(image) == layout
|
||||
# Re-running the record step replaces the tail instead of duplicating it.
|
||||
assert ml.ensure_nrf52_layout(body, layout) == body
|
||||
assert ml.parse_nrf52_layout(ml.ensure_endf(_fw(5, 2048))[0]) is None
|
||||
internal = ml.Nrf52Layout(
|
||||
ml.NRF52_APP_BASE_S140_V7, ml.NRF52_EXTRAFS_START,
|
||||
ml.NRF52_EXTRAFS_START, ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS)
|
||||
internal_image, _ = ml.ensure_endf(ml.ensure_nrf52_layout(_fw(6, 2048), internal))
|
||||
assert ml.parse_nrf52_layout(internal_image) == internal
|
||||
try:
|
||||
ml.build_nrf52_layout(ml.Nrf52Layout(
|
||||
ml.NRF52_APP_BASE_S140_V7, ml.NRF52_EXTRAFS_START,
|
||||
ml.NRF52_EXTRAFS_START, 0))
|
||||
assert False, "inconsistent layout record accepted"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# --- merkle ----------------------------------------------------------------
|
||||
|
||||
def test_merkle_single_block():
|
||||
|
||||
@@ -56,16 +56,12 @@ void SolarSensorManager::stop_gps() {
|
||||
|
||||
bool SolarSensorManager::begin() {
|
||||
Serial1.begin(9600);
|
||||
|
||||
// We'll consider GPS detected if we see any data on Serial1
|
||||
gps_detected = (Serial1.available() > 0);
|
||||
|
||||
if (gps_detected) {
|
||||
MESH_DEBUG_PRINTLN("GPS detected");
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("No GPS detected");
|
||||
}
|
||||
|
||||
// GPS is onboard. An immediate UART availability check races its first NMEA
|
||||
// sentence and permanently hid the GPS setting on most cold boots.
|
||||
gps_detected = true;
|
||||
gps_active = false;
|
||||
_location->stop();
|
||||
MESH_DEBUG_PRINTLN("Onboard GPS available");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -82,8 +78,9 @@ void SolarSensorManager::loop() {
|
||||
if (powersaving_enabled && gps_detected && _location->getGPSPowerSaving()) {
|
||||
unsigned long next_off = _location->getNextGPSOff();
|
||||
unsigned long next_on = _location->getNextGPSOn();
|
||||
if (gps_active && ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
||||
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
POWERSAVING_DEBUG_PRINTLN("GPS entering sleep");
|
||||
stop_gps();
|
||||
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
||||
@@ -103,7 +100,7 @@ void SolarSensorManager::loop() {
|
||||
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
||||
}
|
||||
next_gps_update = now + 1000;
|
||||
next_gps_update = now + getGpsUpdateIntervalMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +130,7 @@ bool SolarSensorManager::setSettingValue(const char* name, const char* value) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false; // not supported
|
||||
return SensorManager::setSettingValue(name, value);
|
||||
}
|
||||
|
||||
void SolarSensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
|
||||
@@ -87,8 +87,9 @@ void HWTSensorManager::loop() {
|
||||
if (powersaving_enabled && _location->getGPSPowerSaving()) {
|
||||
unsigned long next_off = _location->getNextGPSOff();
|
||||
unsigned long next_on = _location->getNextGPSOn();
|
||||
if (gps_active && ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
||||
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
POWERSAVING_DEBUG_PRINTLN("GPS entering sleep");
|
||||
stop_gps();
|
||||
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
||||
@@ -108,7 +109,7 @@ void HWTSensorManager::loop() {
|
||||
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
||||
}
|
||||
next_gps_update = now + 1000;
|
||||
next_gps_update = now + getGpsUpdateIntervalMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +135,7 @@ bool HWTSensorManager::setSettingValue(const char* name, const char* value) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false; // not supported
|
||||
return SensorManager::setSettingValue(name, value);
|
||||
}
|
||||
|
||||
void HWTSensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
|
||||
@@ -36,24 +36,37 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
}
|
||||
|
||||
|
||||
void MASensorManager::armGpsPowerSavingCycle() {
|
||||
if (!powersaving_enabled || !_location->getGPSPowerSaving()) return;
|
||||
_location->syncTime();
|
||||
_location->setNextGPSOn(0);
|
||||
_location->setNextSleep();
|
||||
}
|
||||
|
||||
void MASensorManager::start_gps() {
|
||||
if(!gps_active) {
|
||||
MESH_DEBUG_PRINTLN("starting GPS");
|
||||
gps_active = true;
|
||||
}
|
||||
if (gps_active) return;
|
||||
MESH_DEBUG_PRINTLN("starting GPS");
|
||||
_location->begin();
|
||||
_location->reset();
|
||||
gps_active = true;
|
||||
armGpsPowerSavingCycle();
|
||||
}
|
||||
|
||||
void MASensorManager::stop_gps() {
|
||||
if(gps_active) {
|
||||
MESH_DEBUG_PRINTLN("stopping GPS");
|
||||
gps_active = false;
|
||||
if (!gps_active) return;
|
||||
MESH_DEBUG_PRINTLN("stopping GPS");
|
||||
gps_active = false;
|
||||
if (powersaving_enabled && _location->getGPSPowerSaving()) {
|
||||
_location->stopTimeSync();
|
||||
_location->setNextGPSOff(0);
|
||||
_location->setNextWake();
|
||||
}
|
||||
_location->stop();
|
||||
}
|
||||
|
||||
bool MASensorManager::begin() {
|
||||
Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX);
|
||||
Serial1.begin(9600);
|
||||
delay(500);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -66,6 +79,20 @@ void MASensorManager::loop() {
|
||||
static unsigned long next_gps_update = 0;
|
||||
unsigned long now = millis();
|
||||
loopGpsTelemetry(now);
|
||||
|
||||
if (powersaving_enabled && _location->getGPSPowerSaving()) {
|
||||
unsigned long next_off = _location->getNextGPSOff();
|
||||
unsigned long next_on = _location->getNextGPSOn();
|
||||
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
||||
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
stop_gps();
|
||||
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
||||
|| _location->waitingTimeSync())) {
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
|
||||
if (gps_active) _location->loop();
|
||||
if ((long)(now - next_gps_update) >= 0 && gps_active) {
|
||||
if(_location->isValid()) {
|
||||
@@ -75,7 +102,7 @@ void MASensorManager::loop() {
|
||||
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
||||
}
|
||||
next_gps_update = now + 1000;
|
||||
next_gps_update = now + getGpsUpdateIntervalMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,8 +119,30 @@ const char* MASensorManager::getSettingValue(int i) const {
|
||||
}
|
||||
bool MASensorManager::setSettingValue(const char* name, const char* value) {
|
||||
if(strcmp(name, "gps") == 0) {
|
||||
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
|
||||
bool enabled = strcmp(value, "0") != 0;
|
||||
bool was_active = gps_active;
|
||||
_location->setGPSPowerSaving(enabled && powersaving_enabled);
|
||||
setGpsTelemetryUserEnabled(enabled);
|
||||
if (enabled && powersaving_enabled && was_active) {
|
||||
armGpsPowerSavingCycle();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false; // not supported
|
||||
return SensorManager::setSettingValue(name, value);
|
||||
}
|
||||
|
||||
void MASensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
if (powersaving_enabled == enabled) return;
|
||||
powersaving_enabled = enabled;
|
||||
|
||||
bool gps_user_enabled = isGpsTelemetryUserEnabled();
|
||||
_location->setGPSPowerSaving(enabled && gps_user_enabled);
|
||||
if (!gps_user_enabled) return;
|
||||
|
||||
if (enabled) {
|
||||
if (gps_active) armGpsPowerSavingCycle();
|
||||
else start_gps();
|
||||
} else if (!gps_active) {
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class MASensorManager : public SensorManager {
|
||||
bool gps_active = false;
|
||||
LocationProvider * _location;
|
||||
|
||||
void armGpsPowerSavingCycle();
|
||||
void start_gps();
|
||||
void stop_gps();
|
||||
bool telemetryGpsDetected() const override { return true; }
|
||||
@@ -33,6 +34,8 @@ public:
|
||||
const char* getSettingName(int i) const override;
|
||||
const char* getSettingValue(int i) const override;
|
||||
bool setSettingValue(const char* name, const char* value) override;
|
||||
void setPowerSavingEnabled(bool enabled) override;
|
||||
LocationProvider* getLocationProvider() override { return _location; }
|
||||
};
|
||||
|
||||
extern MeshadventurerBoard board;
|
||||
|
||||
@@ -25,7 +25,15 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
void MeshTrackerX1SensorManager::armGpsPowerSavingCycle() {
|
||||
if (!powersaving_enabled || !_nmea->getGPSPowerSaving()) return;
|
||||
_nmea->syncTime();
|
||||
_nmea->setNextGPSOn(0);
|
||||
_nmea->setNextSleep();
|
||||
}
|
||||
|
||||
void MeshTrackerX1SensorManager::start_gps() {
|
||||
if (gps_active) return;
|
||||
gps_active = true;
|
||||
// this init sequence comes from seeed examples and deals with all gps pins
|
||||
pinMode(GPS_EN, OUTPUT);
|
||||
@@ -44,10 +52,19 @@ void MeshTrackerX1SensorManager::start_gps() {
|
||||
digitalWrite(GPS_SLEEP_INT, HIGH);
|
||||
pinMode(GPS_RTC_INT, OUTPUT);
|
||||
digitalWrite(GPS_RTC_INT, LOW);
|
||||
_nmea->begin();
|
||||
armGpsPowerSavingCycle();
|
||||
}
|
||||
|
||||
void MeshTrackerX1SensorManager::sleep_gps() {
|
||||
if (!gps_active) return;
|
||||
gps_active = false;
|
||||
if (powersaving_enabled && _nmea->getGPSPowerSaving()) {
|
||||
_nmea->stopTimeSync();
|
||||
_nmea->setNextGPSOff(0);
|
||||
_nmea->setNextWake();
|
||||
}
|
||||
_nmea->stop();
|
||||
digitalWrite(GPS_VRTC_EN, HIGH); // keep RTC alive for faster fix on wake
|
||||
digitalWrite(GPS_EN, LOW);
|
||||
digitalWrite(GPS_RESET, LOW);
|
||||
@@ -57,6 +74,7 @@ void MeshTrackerX1SensorManager::sleep_gps() {
|
||||
|
||||
void MeshTrackerX1SensorManager::stop_gps() {
|
||||
gps_active = false;
|
||||
_nmea->stop();
|
||||
digitalWrite(GPS_VRTC_EN, LOW);
|
||||
digitalWrite(GPS_EN, LOW);
|
||||
digitalWrite(GPS_RESET, LOW);
|
||||
@@ -67,6 +85,12 @@ void MeshTrackerX1SensorManager::stop_gps() {
|
||||
bool MeshTrackerX1SensorManager::begin() {
|
||||
// init GPS
|
||||
Serial1.begin(GPS_BAUD_RATE);
|
||||
pinMode(GPS_VRTC_EN, OUTPUT);
|
||||
pinMode(GPS_EN, OUTPUT);
|
||||
pinMode(GPS_RESET, OUTPUT);
|
||||
pinMode(GPS_SLEEP_INT, OUTPUT);
|
||||
pinMode(GPS_RTC_INT, OUTPUT);
|
||||
stop_gps();
|
||||
|
||||
// init SPA06-003 barometer
|
||||
baro_ok = spa06.begin(SPA06_003_DEFAULT_ADDR, &Wire) || spa06.begin(0x76, &Wire);
|
||||
@@ -82,9 +106,7 @@ bool MeshTrackerX1SensorManager::begin() {
|
||||
}
|
||||
|
||||
bool MeshTrackerX1SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
|
||||
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
|
||||
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
|
||||
}
|
||||
queryGpsTelemetry(requester_permissions, telemetry);
|
||||
if (requester_permissions & TELEM_PERM_ENVIRONMENT && baro_ok) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, spa06.readTemperature());
|
||||
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, spa06.readPressure());
|
||||
@@ -93,18 +115,33 @@ bool MeshTrackerX1SensorManager::querySensors(uint8_t requester_permissions, Cay
|
||||
}
|
||||
|
||||
void MeshTrackerX1SensorManager::loop() {
|
||||
static long next_gps_update = 0;
|
||||
static unsigned long next_gps_update = 0;
|
||||
unsigned long now = millis();
|
||||
loopGpsTelemetry(now);
|
||||
|
||||
if (powersaving_enabled && _nmea->getGPSPowerSaving()) {
|
||||
unsigned long next_off = _nmea->getNextGPSOff();
|
||||
unsigned long next_on = _nmea->getNextGPSOn();
|
||||
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
||||
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_nmea->waitingTimeSync())) {
|
||||
sleep_gps();
|
||||
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
||||
|| _nmea->waitingTimeSync())) {
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
|
||||
_nmea->loop();
|
||||
if (gps_active) _nmea->loop();
|
||||
|
||||
if (millis() > next_gps_update) {
|
||||
if ((long)(now - next_gps_update) >= 0) {
|
||||
if (gps_active && _nmea->isValid()) {
|
||||
node_lat = ((double)_nmea->getLatitude())/1000000.;
|
||||
node_lon = ((double)_nmea->getLongitude())/1000000.;
|
||||
node_altitude = ((double)_nmea->getAltitude()) / 1000.0;
|
||||
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
|
||||
}
|
||||
next_gps_update = millis() + 1000;
|
||||
next_gps_update = now + getGpsUpdateIntervalMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,18 +152,36 @@ const char* MeshTrackerX1SensorManager::getSettingName(int i) const {
|
||||
}
|
||||
const char* MeshTrackerX1SensorManager::getSettingValue(int i) const {
|
||||
if (i == 0) {
|
||||
return gps_active ? "1" : "0";
|
||||
return isGpsTelemetryUserEnabled() ? "1" : "0";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
bool MeshTrackerX1SensorManager::setSettingValue(const char* name, const char* value) {
|
||||
if (strcmp(name, "gps") == 0) {
|
||||
if (strcmp(value, "0") == 0) {
|
||||
sleep_gps(); // sleep for faster fix !
|
||||
} else {
|
||||
start_gps();
|
||||
bool enabled = strcmp(value, "0") != 0;
|
||||
bool was_active = gps_active;
|
||||
_nmea->setGPSPowerSaving(enabled && powersaving_enabled);
|
||||
setGpsTelemetryUserEnabled(enabled);
|
||||
if (enabled && powersaving_enabled && was_active) {
|
||||
armGpsPowerSavingCycle();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false; // not supported
|
||||
return SensorManager::setSettingValue(name, value);
|
||||
}
|
||||
|
||||
void MeshTrackerX1SensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
if (powersaving_enabled == enabled) return;
|
||||
powersaving_enabled = enabled;
|
||||
|
||||
bool gps_user_enabled = isGpsTelemetryUserEnabled();
|
||||
_nmea->setGPSPowerSaving(enabled && gps_user_enabled);
|
||||
if (!gps_user_enabled) return;
|
||||
|
||||
if (enabled) {
|
||||
if (gps_active) armGpsPowerSavingCycle();
|
||||
else start_gps();
|
||||
} else if (!gps_active) {
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,14 @@ class MeshTrackerX1SensorManager: public SensorManager {
|
||||
Adafruit_SPA06_003 spa06;
|
||||
LocationProvider * _nmea;
|
||||
|
||||
void armGpsPowerSavingCycle();
|
||||
void start_gps();
|
||||
void sleep_gps();
|
||||
void stop_gps();
|
||||
bool telemetryGpsDetected() const override { return true; }
|
||||
bool telemetryGpsActive() const override { return gps_active; }
|
||||
void telemetryGpsStart() override { start_gps(); }
|
||||
void telemetryGpsStop() override { sleep_gps(); }
|
||||
public:
|
||||
MeshTrackerX1SensorManager(LocationProvider &nmea): _nmea(&nmea) { }
|
||||
bool begin() override;
|
||||
@@ -31,7 +36,8 @@ public:
|
||||
const char* getSettingName(int i) const override;
|
||||
const char* getSettingValue(int i) const override;
|
||||
bool setSettingValue(const char* name, const char* value) override;
|
||||
LocationProvider* getLocationProvider() { return _nmea; }
|
||||
void setPowerSavingEnabled(bool enabled) override;
|
||||
LocationProvider* getLocationProvider() override { return _nmea; }
|
||||
};
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
@@ -25,43 +25,41 @@ bool radio_init() {
|
||||
return radio.std_init(&SPI);
|
||||
}
|
||||
|
||||
void NanoG2UltraSensorManager::armGpsPowerSavingCycle() {
|
||||
if (!powersaving_enabled || !_location->getGPSPowerSaving()) return;
|
||||
_location->syncTime();
|
||||
_location->setNextGPSOn(0);
|
||||
_location->setNextSleep();
|
||||
}
|
||||
|
||||
void NanoG2UltraSensorManager::start_gps() {
|
||||
if (gps_active) return;
|
||||
MESH_DEBUG_PRINTLN("Starting GPS");
|
||||
if (!gps_active) {
|
||||
digitalWrite(PIN_GPS_STANDBY, HIGH); // Wake GPS from standby
|
||||
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
|
||||
Serial1.begin(9600);
|
||||
MESH_DEBUG_PRINTLN("Waiting for gps to power up");
|
||||
delay(1000);
|
||||
gps_active = true;
|
||||
}
|
||||
digitalWrite(PIN_GPS_STANDBY, HIGH); // Wake GPS from standby
|
||||
_location->begin();
|
||||
gps_active = true;
|
||||
armGpsPowerSavingCycle();
|
||||
}
|
||||
|
||||
void NanoG2UltraSensorManager::stop_gps() {
|
||||
if (!gps_active) return;
|
||||
MESH_DEBUG_PRINTLN("Stopping GPS");
|
||||
if (gps_active) {
|
||||
digitalWrite(PIN_GPS_STANDBY, LOW); // sleep GPS
|
||||
gps_active = false;
|
||||
gps_active = false;
|
||||
if (powersaving_enabled && _location->getGPSPowerSaving()) {
|
||||
_location->stopTimeSync();
|
||||
_location->setNextGPSOff(0);
|
||||
_location->setNextWake();
|
||||
}
|
||||
_location->stop();
|
||||
digitalWrite(PIN_GPS_STANDBY, LOW); // sleep GPS
|
||||
}
|
||||
|
||||
bool NanoG2UltraSensorManager::begin() {
|
||||
digitalWrite(PIN_GPS_STANDBY, HIGH); // Wake GPS from standby
|
||||
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
||||
digitalWrite(PIN_GPS_STANDBY, LOW); // Known onboard GPS starts asleep
|
||||
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
|
||||
Serial1.begin(9600);
|
||||
MESH_DEBUG_PRINTLN("Checking GPS switch state");
|
||||
delay(1000);
|
||||
|
||||
// Check initial switch state to determine if GPS should be active
|
||||
if (Serial1.available() > 0) {
|
||||
MESH_DEBUG_PRINTLN("GPS was on at boot, GPS enabled");
|
||||
start_gps();
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("GPS was not on at boot, GPS disabled");
|
||||
}
|
||||
|
||||
gps_active = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -75,6 +73,19 @@ void NanoG2UltraSensorManager::loop() {
|
||||
unsigned long now = millis();
|
||||
loopGpsTelemetry(now);
|
||||
|
||||
if (powersaving_enabled && _location->getGPSPowerSaving()) {
|
||||
unsigned long next_off = _location->getNextGPSOff();
|
||||
unsigned long next_on = _location->getNextGPSOn();
|
||||
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
||||
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
stop_gps();
|
||||
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
||||
|| _location->waitingTimeSync())) {
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
|
||||
if (!gps_active) {
|
||||
return; // GPS is not active, skip further processing
|
||||
}
|
||||
@@ -92,7 +103,7 @@ void NanoG2UltraSensorManager::loop() {
|
||||
MESH_DEBUG_PRINTLN("INVALID location, waiting for fix");
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("GPS satellites: %d", _location->satellitesCount());
|
||||
next_gps_update = now + 1000;
|
||||
next_gps_update = now + getGpsUpdateIntervalMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +124,32 @@ const char *NanoG2UltraSensorManager::getSettingValue(int i) const {
|
||||
|
||||
bool NanoG2UltraSensorManager::setSettingValue(const char *name, const char *value) {
|
||||
if (strcmp(name, "gps") == 0) {
|
||||
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
|
||||
bool enabled = strcmp(value, "0") != 0;
|
||||
bool was_active = gps_active;
|
||||
_location->setGPSPowerSaving(enabled && powersaving_enabled);
|
||||
setGpsTelemetryUserEnabled(enabled);
|
||||
if (enabled && powersaving_enabled && was_active) {
|
||||
armGpsPowerSavingCycle();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false; // not supported
|
||||
return SensorManager::setSettingValue(name, value);
|
||||
}
|
||||
|
||||
void NanoG2UltraSensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
if (powersaving_enabled == enabled) return;
|
||||
powersaving_enabled = enabled;
|
||||
|
||||
bool gps_user_enabled = isGpsTelemetryUserEnabled();
|
||||
_location->setGPSPowerSaving(enabled && gps_user_enabled);
|
||||
if (!gps_user_enabled) return;
|
||||
|
||||
if (enabled) {
|
||||
if (gps_active) armGpsPowerSavingCycle();
|
||||
else start_gps();
|
||||
} else if (!gps_active) {
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
|
||||
@@ -18,6 +18,7 @@ class NanoG2UltraSensorManager : public SensorManager {
|
||||
bool gps_active = false;
|
||||
LocationProvider *_location;
|
||||
|
||||
void armGpsPowerSavingCycle();
|
||||
void start_gps();
|
||||
void stop_gps();
|
||||
bool telemetryGpsDetected() const override { return true; }
|
||||
@@ -34,6 +35,8 @@ public:
|
||||
const char *getSettingName(int i) const override;
|
||||
const char *getSettingValue(int i) const override;
|
||||
bool setSettingValue(const char *name, const char *value) override;
|
||||
void setPowerSavingEnabled(bool enabled) override;
|
||||
LocationProvider* getLocationProvider() override { return _location; }
|
||||
};
|
||||
|
||||
extern NanoG2Ultra board;
|
||||
|
||||
@@ -188,7 +188,7 @@ board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${Station_G2.build_flags}
|
||||
; Portable MQTT observers use CLI setup and the lightweight WiFi updater.
|
||||
; The base observer disables LoRa OTA; build.sh promotes it to FULL and restores OTA.
|
||||
-UENABLE_OTA
|
||||
-D DISABLE_LORA_OTA=1
|
||||
-UMESH_DEBUG
|
||||
@@ -276,6 +276,10 @@ build_flags =
|
||||
-UENV_INCLUDE_MLX90614
|
||||
-UENV_INCLUDE_VL53L0X
|
||||
-UENV_INCLUDE_BME680
|
||||
; Use the ESP32-S3 ROM formatter so this full BLE companion stays within
|
||||
; the Station G2 legacy 0x140000 application partition without dropping UI,
|
||||
; GPS, or any additional external-sensor support.
|
||||
-D PORTABLE_ESP32_ROM_NANO_FORMAT=1
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
@@ -334,7 +338,7 @@ board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${Station_G2.build_flags}
|
||||
; Portable MQTT observers use CLI setup and the lightweight WiFi updater.
|
||||
; The base observer disables LoRa OTA; build.sh promotes it to FULL and restores OTA.
|
||||
-UENABLE_OTA
|
||||
-D DISABLE_LORA_OTA=1
|
||||
-UMESH_DEBUG
|
||||
|
||||
@@ -158,6 +158,14 @@ void T1000SensorManager::stop_gps() {
|
||||
bool T1000SensorManager::begin() {
|
||||
// init GPS
|
||||
Serial1.begin(115200);
|
||||
pinMode(GPS_VRTC_EN, OUTPUT);
|
||||
pinMode(GPS_EN, OUTPUT);
|
||||
pinMode(GPS_RESET, OUTPUT);
|
||||
pinMode(GPS_SLEEP_INT, OUTPUT);
|
||||
pinMode(GPS_RTC_INT, OUTPUT);
|
||||
pinMode(GPS_RESETB, OUTPUT);
|
||||
gps_active = true;
|
||||
stop_gps();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -179,8 +187,9 @@ void T1000SensorManager::loop() {
|
||||
if (powersaving_enabled && _nmea->getGPSPowerSaving()) {
|
||||
unsigned long next_off = _nmea->getNextGPSOff();
|
||||
unsigned long next_on = _nmea->getNextGPSOn();
|
||||
if (gps_active && ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_nmea->waitingTimeSync())) {
|
||||
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
||||
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_nmea->waitingTimeSync())) {
|
||||
POWERSAVING_DEBUG_PRINTLN("GPS entering sleep");
|
||||
stop_gps();
|
||||
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
||||
@@ -200,7 +209,7 @@ void T1000SensorManager::loop() {
|
||||
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
|
||||
//Serial.printf("lat %f lon %f\r\n", _lat, _lon);
|
||||
}
|
||||
next_gps_update = now + 1000;
|
||||
next_gps_update = now + getGpsUpdateIntervalMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +235,7 @@ bool T1000SensorManager::setSettingValue(const char* name, const char* value) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false; // not supported
|
||||
return SensorManager::setSettingValue(name, value);
|
||||
}
|
||||
|
||||
void T1000SensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
|
||||
@@ -29,18 +29,29 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
void ThinkNodeM1SensorManager::armGpsPowerSavingCycle() {
|
||||
if (!powersaving_enabled || !_location->getGPSPowerSaving()) return;
|
||||
_location->syncTime();
|
||||
_location->setNextGPSOn(0);
|
||||
_location->setNextSleep();
|
||||
}
|
||||
|
||||
void ThinkNodeM1SensorManager::start_gps() {
|
||||
if (!gps_active) {
|
||||
gps_active = true;
|
||||
_location->begin();
|
||||
}
|
||||
if (gps_active) return;
|
||||
gps_active = true;
|
||||
_location->begin();
|
||||
armGpsPowerSavingCycle();
|
||||
}
|
||||
|
||||
void ThinkNodeM1SensorManager::stop_gps() {
|
||||
if (gps_active) {
|
||||
gps_active = false;
|
||||
_location->stop();
|
||||
if (!gps_active) return;
|
||||
gps_active = false;
|
||||
if (powersaving_enabled && _location->getGPSPowerSaving()) {
|
||||
_location->stopTimeSync();
|
||||
_location->setNextGPSOff(0);
|
||||
_location->setNextWake();
|
||||
}
|
||||
_location->stop();
|
||||
}
|
||||
|
||||
bool ThinkNodeM1SensorManager::begin() {
|
||||
@@ -55,7 +66,7 @@ bool ThinkNodeM1SensorManager::begin() {
|
||||
|
||||
// Check initial switch state to determine if GPS should be active
|
||||
if (last_gps_switch_state == HIGH) { // Switch is HIGH when ON
|
||||
setGpsTelemetryUserEnabled(true);
|
||||
setSettingValue("gps", "1");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -82,16 +93,29 @@ void ThinkNodeM1SensorManager::loop() {
|
||||
|
||||
if (current_switch_state == HIGH) { // Switch is ON
|
||||
MESH_DEBUG_PRINTLN("GPS switch ON");
|
||||
setGpsTelemetryUserEnabled(true);
|
||||
setSettingValue("gps", "1");
|
||||
} else { // Switch is OFF
|
||||
MESH_DEBUG_PRINTLN("GPS switch OFF");
|
||||
setGpsTelemetryUserEnabled(false);
|
||||
setSettingValue("gps", "0");
|
||||
}
|
||||
}
|
||||
|
||||
last_switch_check = now;
|
||||
}
|
||||
|
||||
if (powersaving_enabled && _location->getGPSPowerSaving()) {
|
||||
unsigned long next_off = _location->getNextGPSOff();
|
||||
unsigned long next_on = _location->getNextGPSOn();
|
||||
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
||||
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
||||
|| !_location->waitingTimeSync())) {
|
||||
stop_gps();
|
||||
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
||||
|| _location->waitingTimeSync())) {
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
|
||||
if (!gps_active) {
|
||||
return; // GPS is not active, skip further processing
|
||||
}
|
||||
@@ -106,7 +130,7 @@ void ThinkNodeM1SensorManager::loop() {
|
||||
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
||||
}
|
||||
next_gps_update = now + 1000;
|
||||
next_gps_update = now + getGpsUpdateIntervalMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +151,30 @@ const char* ThinkNodeM1SensorManager::getSettingValue(int i) const {
|
||||
|
||||
bool ThinkNodeM1SensorManager::setSettingValue(const char* name, const char* value) {
|
||||
if (strcmp(name, "gps") == 0) {
|
||||
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
|
||||
bool enabled = strcmp(value, "0") != 0;
|
||||
bool was_active = gps_active;
|
||||
_location->setGPSPowerSaving(enabled && powersaving_enabled);
|
||||
setGpsTelemetryUserEnabled(enabled);
|
||||
if (enabled && powersaving_enabled && was_active) {
|
||||
armGpsPowerSavingCycle();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false; // not supported
|
||||
return SensorManager::setSettingValue(name, value);
|
||||
}
|
||||
|
||||
void ThinkNodeM1SensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
if (powersaving_enabled == enabled) return;
|
||||
powersaving_enabled = enabled;
|
||||
|
||||
bool gps_user_enabled = isGpsTelemetryUserEnabled();
|
||||
_location->setGPSPowerSaving(enabled && gps_user_enabled);
|
||||
if (!gps_user_enabled) return;
|
||||
|
||||
if (enabled) {
|
||||
if (gps_active) armGpsPowerSavingCycle();
|
||||
else start_gps();
|
||||
} else if (!gps_active) {
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class ThinkNodeM1SensorManager : public SensorManager {
|
||||
bool last_gps_switch_state = false;
|
||||
LocationProvider* _location;
|
||||
|
||||
void armGpsPowerSavingCycle();
|
||||
void start_gps();
|
||||
void stop_gps();
|
||||
bool telemetryGpsDetected() const override { return true; }
|
||||
@@ -34,6 +35,7 @@ public:
|
||||
const char* getSettingName(int i) const override;
|
||||
const char* getSettingValue(int i) const override;
|
||||
bool setSettingValue(const char* name, const char* value) override;
|
||||
void setPowerSavingEnabled(bool enabled) override;
|
||||
};
|
||||
|
||||
extern ThinkNodeM1Board board;
|
||||
|
||||
@@ -41,6 +41,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D LORA_TX_POWER=22
|
||||
build_src_filter = ${nrf52_base.build_src_filter}
|
||||
+<helpers/*.cpp>
|
||||
+<helpers/sensors/EnvironmentSensorManager.cpp>
|
||||
+<ThinkNodeM8Board.cpp>
|
||||
+<../variants/thinknode_m8>
|
||||
lib_deps =
|
||||
|
||||
Reference in new issue
Block a user