Update MeshCore features and OTA layout handling

This commit is contained in:
mikecarper committed 2026-08-18 23:17:42 -07:00
1 parent 0c4619bcc3
commit 47993d178e
74 files changed
+1696 -1169

No files matched your search

+74 -57
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+17
View File
@@ -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`
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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}
+7
View File
@@ -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
+26 -1
View File
@@ -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");
+3 -5
View File
@@ -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
+1 -1
View File
@@ -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
+68 -6
View File
@@ -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
}
+3
View File
@@ -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()) {
+15 -17
View File
@@ -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
+3
View File
@@ -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
+7 -5
View File
@@ -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
+3
View File
@@ -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
+80 -2
View File
@@ -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;
}
+8
View File
@@ -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
};
+18
View File
@@ -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
}
}
+2 -2
View File
@@ -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.",
+28 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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) {
+1 -1
View File
@@ -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;
+35
View File
@@ -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
-4
View File
@@ -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) {
+23 -1
View File
@@ -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; }
+19
View File
@@ -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
+23
View File
@@ -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
+1
View File
@@ -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\","
+2 -2
View File
@@ -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(...) {}
+2 -2
View File
@@ -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(...) {}
+3 -2
View File
@@ -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(...) {}
+3 -2
View File
@@ -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(...) {}
+23 -17
View File
@@ -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(...) {}
+3 -2
View File
@@ -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(...) {}
+50
View File
@@ -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;
+21 -2
View File
@@ -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
+20
View File
@@ -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
+6 -1
View File
@@ -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
+2 -1
View File
@@ -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
+93 -42
View File
@@ -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
+9 -6
View File
@@ -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;
+42 -29
View File
@@ -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;
+10 -6
View File
@@ -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);
}
+77 -37
View File
@@ -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
View File
@@ -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
View File
@@ -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})")
+32
View File
@@ -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():
+11 -14
View File
@@ -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) {
+5 -4
View File
@@ -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) {
+60 -11
View File
@@ -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();
}
}
+3
View File
@@ -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;
+68 -13
View File
@@ -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();
}
}
+7 -1
View File
@@ -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
+59 -26
View File
@@ -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() {
+3
View File
@@ -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;
+6 -2
View File
@@ -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
+13 -4
View File
@@ -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) {
+59 -13
View File
@@ -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();
}
}
+2
View File
@@ -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;
+1
View File
@@ -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 =